From 07d5af1969485f30b1ac2f3d2381e8276820dfcc Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 8 Sep 2020 00:04:13 +0000 Subject: [PATCH 001/514] [ci skip] Translation update --- .../accuweather/translations/fr.json | 4 +++ .../accuweather/translations/no.json | 3 +- .../accuweather/translations/sensor.fr.json | 9 +++++ .../accuweather/translations/uk.json | 1 + .../components/acmeda/translations/no.json | 3 +- .../components/adguard/translations/no.json | 1 + .../components/agent_dvr/translations/no.json | 3 +- .../components/airly/translations/no.json | 3 +- .../components/airly/translations/pt.json | 4 ++- .../components/airvisual/translations/no.json | 1 + .../components/almond/translations/no.json | 3 +- .../components/arcam_fmj/translations/no.json | 3 +- .../components/atag/translations/nl.json | 3 +- .../components/atag/translations/no.json | 6 ++-- .../components/auth/translations/et.json | 7 ++++ .../components/auth/translations/no.json | 3 +- .../components/avri/translations/no.json | 6 ++-- .../components/axis/translations/no.json | 1 + .../azure_devops/translations/fr.json | 8 +++++ .../binary_sensor/translations/nb.json | 5 +++ .../binary_sensor/translations/no.json | 6 ++++ .../components/blebox/translations/no.json | 3 +- .../components/blink/translations/fr.json | 2 ++ .../components/bond/translations/fr.json | 10 ++++++ .../components/braviatv/translations/no.json | 3 +- .../components/broadlink/translations/pt.json | 15 ++++++++ .../components/bsblan/translations/no.json | 3 +- .../cert_expiry/translations/no.json | 3 +- .../components/climate/translations/no.json | 1 + .../components/control4/translations/fr.json | 21 +++++++++++ .../components/control4/translations/uk.json | 1 + .../components/cover/translations/fr.json | 4 ++- .../components/deconz/translations/no.json | 9 ++++- .../components/demo/translations/no.json | 3 +- .../devolo_home_control/translations/no.json | 6 ++-- .../components/dexcom/translations/no.json | 1 + .../components/directv/translations/no.json | 1 + .../components/dsmr/translations/ca.json | 7 ++++ .../components/dsmr/translations/fr.json | 7 ++++ .../components/dunehd/translations/no.json | 3 +- .../components/eafm/translations/fr.json | 1 + .../components/elgato/translations/no.json | 3 +- .../emulated_roku/translations/et.json | 4 ++- .../components/enocean/translations/no.json | 3 +- .../components/esphome/translations/no.json | 3 +- .../components/flo/translations/fr.json | 22 ++++++++++++ .../forked_daapd/translations/no.json | 3 +- .../components/freebox/translations/no.json | 6 ++-- .../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 + .../components/gogogate2/translations/ca.json | 2 +- .../components/gogogate2/translations/no.json | 2 +- .../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/fr.json | 21 +++++++++++ .../homeassistant/translations/nb.json | 3 ++ .../homeassistant/translations/no.json | 3 ++ .../homeassistant/translations/pt.json | 3 ++ .../homekit_controller/translations/pl.json | 3 ++ .../homematicip_cloud/translations/ca.json | 2 +- .../homematicip_cloud/translations/pt.json | 1 + .../huawei_lte/translations/fr.json | 1 + .../huawei_lte/translations/no.json | 1 + .../huawei_lte/translations/pt.json | 1 + .../components/hue/translations/et.json | 7 ++++ .../components/hue/translations/no.json | 3 +- .../humidifier/translations/fr.json | 8 +++++ .../components/insteon/translations/ca.json | 16 ++++----- .../components/insteon/translations/fr.json | 7 +++- .../components/insteon/translations/pt.json | 14 +++++++- .../components/ipp/translations/no.json | 1 + .../components/iqvia/translations/fr.json | 3 ++ .../components/iqvia/translations/no.json | 3 +- .../components/kodi/translations/pt.json | 9 +++-- .../components/konnected/translations/no.json | 3 +- .../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/no.json | 3 +- .../meteo_france/translations/pt.json | 3 +- .../components/metoffice/translations/fr.json | 3 ++ .../components/mikrotik/translations/no.json | 1 + .../components/mill/translations/fr.json | 18 ++++++++++ .../components/monoprice/translations/no.json | 1 + .../components/mqtt/translations/no.json | 2 ++ .../nightscout/translations/fr.json | 4 +++ .../components/nut/translations/no.json | 2 ++ .../components/onvif/translations/no.json | 3 +- .../opentherm_gw/translations/no.json | 4 ++- .../opentherm_gw/translations/pt.json | 1 + .../openweathermap/translations/ca.json | 35 +++++++++++++++++++ .../openweathermap/translations/fr.json | 8 ++++- .../openweathermap/translations/no.json | 35 +++++++++++++++++++ .../openweathermap/translations/pt.json | 29 +++++++++++++++ .../ovo_energy/translations/fr.json | 6 ++++ .../panasonic_viera/translations/no.json | 6 +++- .../components/person/translations/nb.json | 3 +- .../components/person/translations/no.json | 3 +- .../components/pi_hole/translations/fr.json | 1 + .../components/pi_hole/translations/no.json | 1 + .../components/plant/translations/nb.json | 1 + .../components/plant/translations/no.json | 6 ++++ .../components/plex/translations/no.json | 10 ++++-- .../components/plugwise/translations/ca.json | 10 ++++++ .../components/plugwise/translations/fr.json | 10 ++++++ .../components/plugwise/translations/no.json | 14 +++++++- .../components/plugwise/translations/pt.json | 9 +++++ .../components/plugwise/translations/ru.json | 10 ++++++ .../plugwise/translations/zh-Hant.json | 10 ++++++ .../components/poolsense/translations/no.json | 3 +- .../components/ps4/translations/no.json | 13 ++++--- .../rainmachine/translations/no.json | 3 +- .../components/remote/translations/ca.json | 15 ++++++++ .../components/remote/translations/en.json | 15 ++++++++ .../components/remote/translations/fr.json | 15 ++++++++ .../components/remote/translations/pt.json | 15 ++++++++ .../components/remote/translations/ru.json | 15 ++++++++ .../components/rfxtrx/translations/fr.json | 7 ++++ .../components/risco/translations/pt.json | 1 + .../components/roku/translations/no.json | 3 +- .../components/samsungtv/translations/no.json | 3 +- .../components/scene/translations/no.json | 3 ++ .../components/script/translations/no.json | 3 +- .../sensor/translations/es-419.json | 6 ++++ .../components/sensor/translations/nb.json | 3 +- .../components/sensor/translations/no.json | 3 +- .../components/sentry/translations/no.json | 3 +- .../simplisafe/translations/fr.json | 5 +++ .../components/smappee/translations/fr.json | 6 ++++ .../components/soma/translations/no.json | 6 ++-- .../components/sonarr/translations/no.json | 5 ++- .../components/songpal/translations/no.json | 1 + .../components/spider/translations/fr.json | 12 +++++++ .../squeezebox/translations/fr.json | 5 +++ .../squeezebox/translations/no.json | 5 ++- .../components/starline/translations/no.json | 4 ++- .../switch/translations/es-419.json | 6 ++++ .../synology_dsm/translations/no.json | 8 +++-- .../components/tibber/translations/fr.json | 1 + .../components/tibber/translations/no.json | 6 ++-- .../components/tibber/translations/pt.json | 13 +++++++ .../totalconnect/translations/no.json | 3 +- .../components/tradfri/translations/et.json | 11 ++++++ .../transmission/translations/no.json | 1 + .../components/tuya/translations/no.json | 3 +- .../twentemilieu/translations/no.json | 3 +- .../components/unifi/translations/fr.json | 1 + .../components/unifi/translations/no.json | 1 + .../components/volumio/translations/fr.json | 19 ++++++++++ .../components/wolflink/translations/fr.json | 20 +++++++++++ .../xiaomi_aqara/translations/ca.json | 2 +- .../xiaomi_aqara/translations/no.json | 7 +++- .../xiaomi_aqara/translations/pt.json | 5 +++ .../xiaomi_miio/translations/no.json | 3 +- .../components/zha/translations/fr.json | 7 ++++ .../components/zha/translations/no.json | 3 +- .../components/zone/translations/no.json | 3 +- 166 files changed, 852 insertions(+), 92 deletions(-) create mode 100644 homeassistant/components/accuweather/translations/sensor.fr.json create mode 100644 homeassistant/components/auth/translations/et.json create mode 100644 homeassistant/components/azure_devops/translations/fr.json create mode 100644 homeassistant/components/control4/translations/fr.json create mode 100644 homeassistant/components/dsmr/translations/ca.json create mode 100644 homeassistant/components/dsmr/translations/fr.json create mode 100644 homeassistant/components/flo/translations/fr.json create mode 100644 homeassistant/components/hassio/translations/nb.json create mode 100644 homeassistant/components/hassio/translations/no.json create mode 100644 homeassistant/components/hlk_sw16/translations/fr.json create mode 100644 homeassistant/components/homeassistant/translations/nb.json create mode 100644 homeassistant/components/homeassistant/translations/no.json create mode 100644 homeassistant/components/homeassistant/translations/pt.json create mode 100644 homeassistant/components/lovelace/translations/nb.json create mode 100644 homeassistant/components/lovelace/translations/no.json create mode 100644 homeassistant/components/mill/translations/fr.json create mode 100644 homeassistant/components/openweathermap/translations/ca.json create mode 100644 homeassistant/components/openweathermap/translations/no.json create mode 100644 homeassistant/components/openweathermap/translations/pt.json create mode 100644 homeassistant/components/rfxtrx/translations/fr.json create mode 100644 homeassistant/components/scene/translations/no.json create mode 100644 homeassistant/components/tibber/translations/pt.json create mode 100644 homeassistant/components/tradfri/translations/et.json create mode 100644 homeassistant/components/volumio/translations/fr.json create mode 100644 homeassistant/components/wolflink/translations/fr.json diff --git a/homeassistant/components/accuweather/translations/fr.json b/homeassistant/components/accuweather/translations/fr.json index e5ed21357e0..33001cf5b84 100644 --- a/homeassistant/components/accuweather/translations/fr.json +++ b/homeassistant/components/accuweather/translations/fr.json @@ -3,6 +3,10 @@ "abort": { "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_api_key": "Cl\u00e9 API invalide" + }, "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/no.json b/homeassistant/components/accuweather/translations/no.json index f0ab46267b2..c6cbc82bc2c 100644 --- a/homeassistant/components/accuweather/translations/no.json +++ b/homeassistant/components/accuweather/translations/no.json @@ -16,7 +16,8 @@ "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." + "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": "" } } }, diff --git a/homeassistant/components/accuweather/translations/sensor.fr.json b/homeassistant/components/accuweather/translations/sensor.fr.json new file mode 100644 index 00000000000..cd0a04eceee --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.fr.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "En baisse", + "rising": "En hausse", + "steady": "Stable" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/uk.json b/homeassistant/components/accuweather/translations/uk.json index 8c3f282b350..a399c8f0694 100644 --- a/homeassistant/components/accuweather/translations/uk.json +++ b/homeassistant/components/accuweather/translations/uk.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "api_key": "", "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457" diff --git a/homeassistant/components/acmeda/translations/no.json b/homeassistant/components/acmeda/translations/no.json index 5364fc683eb..66335077cfb 100644 --- a/homeassistant/components/acmeda/translations/no.json +++ b/homeassistant/components/acmeda/translations/no.json @@ -11,5 +11,6 @@ "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 725ca4a7a32..a772988c042 100644 --- a/homeassistant/components/adguard/translations/no.json +++ b/homeassistant/components/adguard/translations/no.json @@ -16,6 +16,7 @@ "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/agent_dvr/translations/no.json b/homeassistant/components/agent_dvr/translations/no.json index 247654fdde9..3fcbb8f1617 100644 --- a/homeassistant/components/agent_dvr/translations/no.json +++ b/homeassistant/components/agent_dvr/translations/no.json @@ -10,7 +10,8 @@ "step": { "user": { "data": { - "host": "Vert" + "host": "Vert", + "port": "" }, "title": "Konfigurere Agent DVR" } diff --git a/homeassistant/components/airly/translations/no.json b/homeassistant/components/airly/translations/no.json index 6222dddfe28..09e77a311eb 100644 --- a/homeassistant/components/airly/translations/no.json +++ b/homeassistant/components/airly/translations/no.json @@ -15,7 +15,8 @@ "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)" + "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": "" } } } diff --git a/homeassistant/components/airly/translations/pt.json b/homeassistant/components/airly/translations/pt.json index c7081cd694a..ae35beabf6b 100644 --- a/homeassistant/components/airly/translations/pt.json +++ b/homeassistant/components/airly/translations/pt.json @@ -3,9 +3,11 @@ "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 b9be5498560..8fcf00a6714 100644 --- a/homeassistant/components/airvisual/translations/no.json +++ b/homeassistant/components/airvisual/translations/no.json @@ -29,6 +29,7 @@ "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/almond/translations/no.json b/homeassistant/components/almond/translations/no.json index 6e5c90b69e2..3a6a89a8340 100644 --- a/homeassistant/components/almond/translations/no.json +++ b/homeassistant/components/almond/translations/no.json @@ -7,7 +7,8 @@ }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til \u00e5 koble til Almond levert av Hass.io add-on: {addon}?" + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til Almond levert av Hass.io add-on: {addon}?", + "title": "" }, "pick_implementation": { "title": "Velg godkjenningsmetode" diff --git a/homeassistant/components/arcam_fmj/translations/no.json b/homeassistant/components/arcam_fmj/translations/no.json index b067d24dc44..14d55224119 100644 --- a/homeassistant/components/arcam_fmj/translations/no.json +++ b/homeassistant/components/arcam_fmj/translations/no.json @@ -12,7 +12,8 @@ }, "user": { "data": { - "host": "Vert" + "host": "Vert", + "port": "" }, "description": "Vennligst skriv inn vertsnavnet eller IP-adressen til enheten." } diff --git a/homeassistant/components/atag/translations/nl.json b/homeassistant/components/atag/translations/nl.json index ac6477ec4d2..077beb65871 100644 --- a/homeassistant/components/atag/translations/nl.json +++ b/homeassistant/components/atag/translations/nl.json @@ -16,5 +16,6 @@ "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 aa2f7d1b3b8..a0e428f286a 100644 --- a/homeassistant/components/atag/translations/no.json +++ b/homeassistant/components/atag/translations/no.json @@ -11,10 +11,12 @@ "user": { "data": { "email": "E-post (valgfritt)", - "host": "Vert" + "host": "Vert", + "port": "" }, "title": "Koble til enheten" } } - } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/auth/translations/et.json b/homeassistant/components/auth/translations/et.json new file mode 100644 index 00000000000..290f4ee12a9 --- /dev/null +++ b/homeassistant/components/auth/translations/et.json @@ -0,0 +1,7 @@ +{ + "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 d19140ee218..ea0f1baa067 100644 --- a/homeassistant/components/auth/translations/no.json +++ b/homeassistant/components/auth/translations/no.json @@ -28,7 +28,8 @@ "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 4fb4490ac88..5d7f77113b9 100644 --- a/homeassistant/components/avri/translations/no.json +++ b/homeassistant/components/avri/translations/no.json @@ -15,8 +15,10 @@ "house_number_extension": "Utvidelse av husnummer", "zip_code": "Postnummer" }, - "description": "Skriv inn adressen din" + "description": "Skriv inn adressen din", + "title": "" } } - } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/no.json b/homeassistant/components/axis/translations/no.json index 70836d88239..039e6138753 100644 --- a/homeassistant/components/axis/translations/no.json +++ b/homeassistant/components/axis/translations/no.json @@ -18,6 +18,7 @@ "data": { "host": "Vert", "password": "Passord", + "port": "", "username": "Brukernavn" }, "title": "Sett opp Axis enhet" diff --git a/homeassistant/components/azure_devops/translations/fr.json b/homeassistant/components/azure_devops/translations/fr.json new file mode 100644 index 00000000000..7d50110a24e --- /dev/null +++ b/homeassistant/components/azure_devops/translations/fr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9", + "reauth_successful": "Jeton d'acc\u00e8s mis \u00e0 jour avec succ\u00e8s" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/nb.json b/homeassistant/components/binary_sensor/translations/nb.json index 8b143f7499a..76c56713646 100644 --- a/homeassistant/components/binary_sensor/translations/nb.json +++ b/homeassistant/components/binary_sensor/translations/nb.json @@ -9,6 +9,7 @@ "on": "Lavt" }, "cold": { + "off": "", "on": "Kald" }, "connectivity": { @@ -55,6 +56,10 @@ "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 25b7c165c11..b78a50a8628 100644 --- a/homeassistant/components/binary_sensor/translations/no.json +++ b/homeassistant/components/binary_sensor/translations/no.json @@ -99,6 +99,7 @@ "on": "Lavt" }, "cold": { + "off": "", "on": "Kald" }, "connectivity": { @@ -118,6 +119,7 @@ "on": "Oppdaget" }, "heat": { + "off": "", "on": "Varm" }, "lock": { @@ -144,6 +146,10 @@ "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 925f680107e..239d1fb03c6 100644 --- a/homeassistant/components/blebox/translations/no.json +++ b/homeassistant/components/blebox/translations/no.json @@ -13,7 +13,8 @@ "step": { "user": { "data": { - "host": "IP adresse" + "host": "IP adresse", + "port": "" }, "description": "Konfigurer BleBox-en til \u00e5 integreres med Home Assistant.", "title": "Konfigurere BleBox-enheten" diff --git a/homeassistant/components/blink/translations/fr.json b/homeassistant/components/blink/translations/fr.json index 80468b36409..83aaad902a1 100644 --- a/homeassistant/components/blink/translations/fr.json +++ b/homeassistant/components/blink/translations/fr.json @@ -4,6 +4,8 @@ "already_configured": "P\u00e9riph\u00e9rique d\u00e9j\u00e0 configur\u00e9" }, "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_access_token": "Jeton d'acc\u00e8s non valide", "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/bond/translations/fr.json b/homeassistant/components/bond/translations/fr.json index 74beceeccd9..cabbb73a370 100644 --- a/homeassistant/components/bond/translations/fr.json +++ b/homeassistant/components/bond/translations/fr.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, "error": { "cannot_connect": "Echec de connexion", "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, + "flow_title": "Bond : {bond_id} ({h\u00f4te})", "step": { + "confirm": { + "data": { + "access_token": "Jeton d'acc\u00e8s" + }, + "description": "Voulez-vous configurer {bond_id} ?" + }, "user": { "data": { "access_token": "Token d'acc\u00e8s", diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json index ad5907b4678..cd687d8f2d0 100644 --- a/homeassistant/components/braviatv/translations/no.json +++ b/homeassistant/components/braviatv/translations/no.json @@ -21,7 +21,8 @@ "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." + "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": "" } } }, diff --git a/homeassistant/components/broadlink/translations/pt.json b/homeassistant/components/broadlink/translations/pt.json index c1b7a83d443..bf246b55b36 100644 --- a/homeassistant/components/broadlink/translations/pt.json +++ b/homeassistant/components/broadlink/translations/pt.json @@ -11,6 +11,21 @@ }, "flow_title": "{name} ({model} em {host})", "step": { + "auth": { + "title": "Autenticar no dispositivo" + }, + "finish": { + "data": { + "name": "Nome" + }, + "title": "Escolha um nome para o dispositivo" + }, + "reset": { + "title": "Desbloqueie o dispositivo" + }, + "unlock": { + "title": "Desbloqueie o dispositivo (opcional)" + }, "user": { "data": { "host": "Servidor" diff --git a/homeassistant/components/bsblan/translations/no.json b/homeassistant/components/bsblan/translations/no.json index 319fbb771af..040349997f4 100644 --- a/homeassistant/components/bsblan/translations/no.json +++ b/homeassistant/components/bsblan/translations/no.json @@ -11,7 +11,8 @@ "user": { "data": { "host": "Vert", - "passkey": "Tilgangsn\u00f8kkel streng" + "passkey": "Tilgangsn\u00f8kkel streng", + "port": "" }, "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 5832e0a0dd5..a7aa3d1ab13 100644 --- a/homeassistant/components/cert_expiry/translations/no.json +++ b/homeassistant/components/cert_expiry/translations/no.json @@ -13,7 +13,8 @@ "user": { "data": { "host": "Vert", - "name": "Sertifikatets navn" + "name": "Sertifikatets navn", + "port": "" }, "title": "Definer sertifikatet som skal testes" } diff --git a/homeassistant/components/climate/translations/no.json b/homeassistant/components/climate/translations/no.json index 3117378191d..4ac58d07bbb 100644 --- a/homeassistant/components/climate/translations/no.json +++ b/homeassistant/components/climate/translations/no.json @@ -16,6 +16,7 @@ }, "state": { "_": { + "auto": "", "cool": "Kj\u00f8le", "dry": "T\u00f8rr", "fan_only": "Kun vifte", diff --git a/homeassistant/components/control4/translations/fr.json b/homeassistant/components/control4/translations/fr.json new file mode 100644 index 00000000000..1b3803499de --- /dev/null +++ b/homeassistant/components/control4/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "Adresse IP", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/control4/translations/uk.json b/homeassistant/components/control4/translations/uk.json index 6c0426eba8f..c771883714a 100644 --- a/homeassistant/components/control4/translations/uk.json +++ b/homeassistant/components/control4/translations/uk.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "password": "", "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" } } diff --git a/homeassistant/components/cover/translations/fr.json b/homeassistant/components/cover/translations/fr.json index 31cb82a6e7b..92cd9b223ea 100644 --- a/homeassistant/components/cover/translations/fr.json +++ b/homeassistant/components/cover/translations/fr.json @@ -5,7 +5,9 @@ "close_tilt": "Fermer {entity_name}", "open": "Ouvrir {entity_name}", "open_tilt": "Ouvrir {entity_name}", - "set_position": "D\u00e9finir la position de {entity_name}" + "set_position": "D\u00e9finir la position de {entity_name}", + "set_tilt_position": "D\u00e9finir la position d'inclinaison de {entity_name}", + "stop": "Arr\u00eater {entity_name}" }, "condition_type": { "is_closed": "{entity_name} est ferm\u00e9", diff --git a/homeassistant/components/deconz/translations/no.json b/homeassistant/components/deconz/translations/no.json index 6eb5624f05f..231901c4cd4 100644 --- a/homeassistant/components/deconz/translations/no.json +++ b/homeassistant/components/deconz/translations/no.json @@ -23,7 +23,8 @@ }, "manual_input": { "data": { - "host": "Vert" + "host": "Vert", + "port": "" } }, "user": { @@ -47,6 +48,12 @@ "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/demo/translations/no.json b/homeassistant/components/demo/translations/no.json index 3e7dfafac08..5003b9da568 100644 --- a/homeassistant/components/demo/translations/no.json +++ b/homeassistant/components/demo/translations/no.json @@ -16,5 +16,6 @@ } } } - } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/no.json b/homeassistant/components/devolo_home_control/translations/no.json index 08b70ba7a4e..19c8d2653c1 100644 --- a/homeassistant/components/devolo_home_control/translations/no.json +++ b/homeassistant/components/devolo_home_control/translations/no.json @@ -13,8 +13,10 @@ "mydevolo_url": "mydevolo URL", "password": "Passord", "username": "E-postadresse / devolo-ID" - } + }, + "title": "" } } - } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/no.json b/homeassistant/components/dexcom/translations/no.json index de99cfe0fbc..61ad015b5a4 100644 --- a/homeassistant/components/dexcom/translations/no.json +++ b/homeassistant/components/dexcom/translations/no.json @@ -12,6 +12,7 @@ "user": { "data": { "password": "Passord", + "server": "", "username": "Brukernavn" }, "description": "Angi Dexcom Share-legitimasjon", diff --git a/homeassistant/components/directv/translations/no.json b/homeassistant/components/directv/translations/no.json index 88f36decaea..c6db33d32d0 100644 --- a/homeassistant/components/directv/translations/no.json +++ b/homeassistant/components/directv/translations/no.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "Tilkobling feilet" }, + "flow_title": "", "step": { "ssdp_confirm": { "description": "Vil du sette opp {name} ?" diff --git a/homeassistant/components/dsmr/translations/ca.json b/homeassistant/components/dsmr/translations/ca.json new file mode 100644 index 00000000000..14e637f5f98 --- /dev/null +++ b/homeassistant/components/dsmr/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/dsmr/translations/fr.json b/homeassistant/components/dsmr/translations/fr.json new file mode 100644 index 00000000000..c4bc0d48b1a --- /dev/null +++ b/homeassistant/components/dsmr/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/dunehd/translations/no.json b/homeassistant/components/dunehd/translations/no.json index 6230845a09e..e395c28b7a9 100644 --- a/homeassistant/components/dunehd/translations/no.json +++ b/homeassistant/components/dunehd/translations/no.json @@ -13,7 +13,8 @@ "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." + "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": "" } } } diff --git a/homeassistant/components/eafm/translations/fr.json b/homeassistant/components/eafm/translations/fr.json index ca9d788e75d..2d963bbe5df 100644 --- a/homeassistant/components/eafm/translations/fr.json +++ b/homeassistant/components/eafm/translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "no_stations": "Aucune station de surveillance des inondations n'a \u00e9t\u00e9 trouv\u00e9e." }, "step": { diff --git a/homeassistant/components/elgato/translations/no.json b/homeassistant/components/elgato/translations/no.json index 9c78b3411e3..bb7e56211de 100644 --- a/homeassistant/components/elgato/translations/no.json +++ b/homeassistant/components/elgato/translations/no.json @@ -11,7 +11,8 @@ "step": { "user": { "data": { - "host": "Vert" + "host": "Vert", + "port": "" }, "description": "Sett opp Elgato Key Light for \u00e5 integrere med Home Assistant." }, diff --git a/homeassistant/components/emulated_roku/translations/et.json b/homeassistant/components/emulated_roku/translations/et.json index d6a9fded4b6..b94548b44af 100644 --- a/homeassistant/components/emulated_roku/translations/et.json +++ b/homeassistant/components/emulated_roku/translations/et.json @@ -3,9 +3,11 @@ "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 b87607d2f35..a3fc35edcc8 100644 --- a/homeassistant/components/enocean/translations/no.json +++ b/homeassistant/components/enocean/translations/no.json @@ -22,5 +22,6 @@ "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 9a23a04b540..3c2dafff34d 100644 --- a/homeassistant/components/esphome/translations/no.json +++ b/homeassistant/components/esphome/translations/no.json @@ -23,7 +23,8 @@ }, "user": { "data": { - "host": "Vert" + "host": "Vert", + "port": "" }, "description": "Vennligst fyll inn tilkoblingsinnstillinger for din [ESPHome](https://esphomelib.com/) node." } diff --git a/homeassistant/components/flo/translations/fr.json b/homeassistant/components/flo/translations/fr.json new file mode 100644 index 00000000000..4013a390696 --- /dev/null +++ b/homeassistant/components/flo/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + }, + "title": "flo" +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/no.json b/homeassistant/components/forked_daapd/translations/no.json index d86d47c4137..ac58ddf9639 100644 --- a/homeassistant/components/forked_daapd/translations/no.json +++ b/homeassistant/components/forked_daapd/translations/no.json @@ -17,7 +17,8 @@ "data": { "host": "Vert", "name": "Vennlig navn", - "password": "API-passord (la st\u00e5 tomt hvis ingen passord)" + "password": "API-passord (la st\u00e5 tomt hvis ingen passord)", + "port": "" }, "title": "Konfigurere forked-daapd-enhet" } diff --git a/homeassistant/components/freebox/translations/no.json b/homeassistant/components/freebox/translations/no.json index f93450837a2..0ec9bf70ecd 100644 --- a/homeassistant/components/freebox/translations/no.json +++ b/homeassistant/components/freebox/translations/no.json @@ -15,8 +15,10 @@ }, "user": { "data": { - "host": "Vert" - } + "host": "Vert", + "port": "" + }, + "title": "" } } } diff --git a/homeassistant/components/garmin_connect/translations/no.json b/homeassistant/components/garmin_connect/translations/no.json index 1c814306b3b..9058d46d02a 100644 --- a/homeassistant/components/garmin_connect/translations/no.json +++ b/homeassistant/components/garmin_connect/translations/no.json @@ -15,7 +15,8 @@ "password": "Passord", "username": "Brukernavn" }, - "description": "Fyll inn legitimasjonen din." + "description": "Fyll inn legitimasjonen din.", + "title": "" } } } diff --git a/homeassistant/components/gdacs/translations/no.json b/homeassistant/components/gdacs/translations/no.json index 3ca22c398e0..372a24c0b38 100644 --- a/homeassistant/components/gdacs/translations/no.json +++ b/homeassistant/components/gdacs/translations/no.json @@ -5,6 +5,9 @@ }, "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 3ca22c398e0..fc3b339d807 100644 --- a/homeassistant/components/geonetnz_quakes/translations/no.json +++ b/homeassistant/components/geonetnz_quakes/translations/no.json @@ -5,6 +5,10 @@ }, "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 50ffa06071e..646afcc1d16 100644 --- a/homeassistant/components/geonetnz_volcano/translations/no.json +++ b/homeassistant/components/geonetnz_volcano/translations/no.json @@ -5,6 +5,9 @@ }, "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 7df7ba57b3b..784b75c9ee5 100644 --- a/homeassistant/components/gios/translations/no.json +++ b/homeassistant/components/gios/translations/no.json @@ -14,7 +14,8 @@ "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" + "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": "" } } } diff --git a/homeassistant/components/glances/translations/no.json b/homeassistant/components/glances/translations/no.json index 666aaa6bf00..dd593c4add6 100644 --- a/homeassistant/components/glances/translations/no.json +++ b/homeassistant/components/glances/translations/no.json @@ -13,6 +13,7 @@ "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/gogogate2/translations/ca.json b/homeassistant/components/gogogate2/translations/ca.json index bb5797d6517..a68c0e6384c 100644 --- a/homeassistant/components/gogogate2/translations/ca.json +++ b/homeassistant/components/gogogate2/translations/ca.json @@ -15,7 +15,7 @@ "username": "Nom d'usuari" }, "description": "Proporciona, a continuaci\u00f3, la informaci\u00f3 necess\u00e0ria.", - "title": "Configuraci\u00f3 de GogoGate2" + "title": "Configuraci\u00f3 de GogoGate2 o iSmartGate" } } } diff --git a/homeassistant/components/gogogate2/translations/no.json b/homeassistant/components/gogogate2/translations/no.json index 8adc85e0c26..b050a2eadd1 100644 --- a/homeassistant/components/gogogate2/translations/no.json +++ b/homeassistant/components/gogogate2/translations/no.json @@ -15,7 +15,7 @@ "username": "Brukernavn" }, "description": "Gi n\u00f8dvendig informasjon nedenfor.", - "title": "Konfigurer GogoGate2" + "title": "Sett opp GogoGate2 eller iSmartGate" } } } diff --git a/homeassistant/components/group/translations/nb.json b/homeassistant/components/group/translations/nb.json index 7d2edd69113..14ac7fac24f 100644 --- a/homeassistant/components/group/translations/nb.json +++ b/homeassistant/components/group/translations/nb.json @@ -6,6 +6,7 @@ "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 698af4fe68c..763021190c1 100644 --- a/homeassistant/components/group/translations/no.json +++ b/homeassistant/components/group/translations/no.json @@ -6,8 +6,10 @@ "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 850424df514..fbe5f881124 100644 --- a/homeassistant/components/guardian/translations/no.json +++ b/homeassistant/components/guardian/translations/no.json @@ -8,7 +8,8 @@ "step": { "user": { "data": { - "ip_address": "IP adresse" + "ip_address": "IP adresse", + "port": "" }, "description": "Konfigurer en lokal Elexa Guardian-enhet." }, diff --git a/homeassistant/components/hassio/translations/nb.json b/homeassistant/components/hassio/translations/nb.json new file mode 100644 index 00000000000..d8a4c453015 --- /dev/null +++ b/homeassistant/components/hassio/translations/nb.json @@ -0,0 +1,3 @@ +{ + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/no.json b/homeassistant/components/hassio/translations/no.json new file mode 100644 index 00000000000..d8a4c453015 --- /dev/null +++ b/homeassistant/components/hassio/translations/no.json @@ -0,0 +1,3 @@ +{ + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/fr.json b/homeassistant/components/hlk_sw16/translations/fr.json new file mode 100644 index 00000000000..45620fe7795 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/nb.json b/homeassistant/components/homeassistant/translations/nb.json new file mode 100644 index 00000000000..d8a4c453015 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/nb.json @@ -0,0 +1,3 @@ +{ + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/no.json b/homeassistant/components/homeassistant/translations/no.json new file mode 100644 index 00000000000..d8a4c453015 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/no.json @@ -0,0 +1,3 @@ +{ + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/pt.json b/homeassistant/components/homeassistant/translations/pt.json new file mode 100644 index 00000000000..d8a4c453015 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/pt.json @@ -0,0 +1,3 @@ +{ + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/pl.json b/homeassistant/components/homekit_controller/translations/pl.json index 94e3422338b..bed897ea242 100644 --- a/homeassistant/components/homekit_controller/translations/pl.json +++ b/homeassistant/components/homekit_controller/translations/pl.json @@ -27,6 +27,9 @@ "description": "Wprowad\u017a kod parowania HomeKit, aby u\u017cy\u0107 tego akcesorium", "title": "Sparuj z akcesorium HomeKit" }, + "protocol_error": { + "description": "Urz\u0105dzenie mo\u017ce nie by\u0107 w trybie parowania i wymaga\u0107 " + }, "user": { "data": { "device": "Urz\u0105dzenie" diff --git a/homeassistant/components/homematicip_cloud/translations/ca.json b/homeassistant/components/homematicip_cloud/translations/ca.json index fe64ea49e83..871af68e360 100644 --- a/homeassistant/components/homematicip_cloud/translations/ca.json +++ b/homeassistant/components/homematicip_cloud/translations/ca.json @@ -7,7 +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.", + "invalid_sgtin_or_pin": "Codi PIN o SGTIN 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/pt.json b/homeassistant/components/homematicip_cloud/translations/pt.json index 645ba242561..f8a69ab709d 100644 --- a/homeassistant/components/homematicip_cloud/translations/pt.json +++ b/homeassistant/components/homematicip_cloud/translations/pt.json @@ -6,6 +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.", diff --git a/homeassistant/components/huawei_lte/translations/fr.json b/homeassistant/components/huawei_lte/translations/fr.json index 5270168d9ca..2c7437d9a14 100644 --- a/homeassistant/components/huawei_lte/translations/fr.json +++ b/homeassistant/components/huawei_lte/translations/fr.json @@ -21,6 +21,7 @@ "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/no.json b/homeassistant/components/huawei_lte/translations/no.json index 1cc80845ea6..710a318d966 100644 --- a/homeassistant/components/huawei_lte/translations/no.json +++ b/homeassistant/components/huawei_lte/translations/no.json @@ -16,6 +16,7 @@ "response_error": "Ukjent feil fra enheten", "unknown_connection_error": "Ukjent feil under tilkobling til enhet" }, + "flow_title": "", "step": { "user": { "data": { diff --git a/homeassistant/components/huawei_lte/translations/pt.json b/homeassistant/components/huawei_lte/translations/pt.json index f43cf3acc4f..b92752d7d13 100644 --- a/homeassistant/components/huawei_lte/translations/pt.json +++ b/homeassistant/components/huawei_lte/translations/pt.json @@ -15,6 +15,7 @@ "user": { "data": { "password": "Palavra-passe", + "url": "", "username": "Nome do utilizador" }, "title": "Configurar o Huawei LTE" diff --git a/homeassistant/components/hue/translations/et.json b/homeassistant/components/hue/translations/et.json index e7ff3c415fb..92553c84cfe 100644 --- a/homeassistant/components/hue/translations/et.json +++ b/homeassistant/components/hue/translations/et.json @@ -2,6 +2,13 @@ "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 dbef1a20d48..2d51ee26452 100644 --- a/homeassistant/components/hue/translations/no.json +++ b/homeassistant/components/hue/translations/no.json @@ -22,7 +22,8 @@ "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)" + "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": "" }, "manual": { "data": { diff --git a/homeassistant/components/humidifier/translations/fr.json b/homeassistant/components/humidifier/translations/fr.json index cd4b723a986..4b680bdba7f 100644 --- a/homeassistant/components/humidifier/translations/fr.json +++ b/homeassistant/components/humidifier/translations/fr.json @@ -6,6 +6,14 @@ "toggle": "Inverser {nom_entit\u00e9}", "turn_off": "\u00c9teindre {entity_name}", "turn_on": "Allumer {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est d\u00e9sactiv\u00e9", + "is_on": "{entity_name} est activ\u00e9" + }, + "trigger_type": { + "turned_off": "{entity_name} s'est \u00e9teint", + "turned_on": "{entity_name} s'est allum\u00e9" } }, "state": { diff --git a/homeassistant/components/insteon/translations/ca.json b/homeassistant/components/insteon/translations/ca.json index e1d470b784c..ea5300200c2 100644 --- a/homeassistant/components/insteon/translations/ca.json +++ b/homeassistant/components/insteon/translations/ca.json @@ -2,11 +2,11 @@ "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": "Ha fallat la connexi\u00f3", "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.", + "cannot_connect": "Ha fallat la connexi\u00f3", "select_single": "Selecciona una opci\u00f3." }, "step": { @@ -57,7 +57,7 @@ }, "plm": { "data": { - "device": "Dispositiu PLM (ex: /dev/ttyUSB0 o COM3)" + "device": "Ruta del port USB del dispositiu" }, "description": "Configura el m\u00f2dem Insteon PowerLink (PLM).", "title": "Insteon PLM" @@ -77,7 +77,7 @@ "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.", + "cannot_connect": "Ha fallat la connexi\u00f3", "input_error": "Entrades inv\u00e0lides, revisa els valors.", "select_single": "Selecciona una opci\u00f3." }, @@ -103,10 +103,10 @@ }, "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" + "host": "Adre\u00e7a IP", + "password": "Contrasenya", + "port": "Port", + "username": "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" diff --git a/homeassistant/components/insteon/translations/fr.json b/homeassistant/components/insteon/translations/fr.json index e0f56f093af..45b85201a3c 100644 --- a/homeassistant/components/insteon/translations/fr.json +++ b/homeassistant/components/insteon/translations/fr.json @@ -83,7 +83,9 @@ }, "add_x10": { "data": { - "platform": "Plate-forme" + "platform": "Plate-forme", + "steps": "Pas de gradateur (pour les appareils d'\u00e9clairage uniquement, par d\u00e9faut 22)", + "unitcode": "Code de l'unit\u00e9 (1-16)" }, "description": "Modifiez le mot de passe Insteon Hub.", "title": "Insteon" @@ -99,8 +101,10 @@ }, "init": { "data": { + "add_override": "Ajoutez un remplacement de p\u00e9riph\u00e9rique.", "add_x10": "Ajouter un appareil X10.", "change_hub_config": "Modifier la configuration du Hub.", + "remove_override": "Supprimer un remplacement d'appareil", "remove_x10": "Retirez un p\u00e9riph\u00e9rique X10." }, "description": "S\u00e9lectionnez une option \u00e0 configurer.", @@ -110,6 +114,7 @@ "data": { "address": "S\u00e9lectionner une adresse de p\u00e9riph\u00e9rique \u00e0 retirer" }, + "description": "Supprimer un remplacement d'appareil", "title": "Insteon" }, "remove_x10": { diff --git a/homeassistant/components/insteon/translations/pt.json b/homeassistant/components/insteon/translations/pt.json index 4e44844483d..e25fe7db5bd 100644 --- a/homeassistant/components/insteon/translations/pt.json +++ b/homeassistant/components/insteon/translations/pt.json @@ -42,9 +42,15 @@ }, "options": { "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "select_single": "Selecione uma op\u00e7\u00e3o." }, "step": { + "add_x10": { + "data": { + "platform": "Plataforma" + } + }, "change_hub_config": { "data": { "host": "Endere\u00e7o IP", @@ -52,6 +58,12 @@ "port": "Porta", "username": "Nome de Utilizador" } + }, + "init": { + "data": { + "remove_x10": "Remova um dispositivo X10." + }, + "description": "Selecione uma op\u00e7\u00e3o para configurar." } } } diff --git a/homeassistant/components/ipp/translations/no.json b/homeassistant/components/ipp/translations/no.json index 749b5e5bab8..4e94efe71c1 100644 --- a/homeassistant/components/ipp/translations/no.json +++ b/homeassistant/components/ipp/translations/no.json @@ -19,6 +19,7 @@ "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/iqvia/translations/fr.json b/homeassistant/components/iqvia/translations/fr.json index c10a1da59f2..22f45ac2f0e 100644 --- a/homeassistant/components/iqvia/translations/fr.json +++ b/homeassistant/components/iqvia/translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ce code postal a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9." + }, "error": { "invalid_zip_code": "Code postal invalide" }, diff --git a/homeassistant/components/iqvia/translations/no.json b/homeassistant/components/iqvia/translations/no.json index 9359014dcf1..98526dfed24 100644 --- a/homeassistant/components/iqvia/translations/no.json +++ b/homeassistant/components/iqvia/translations/no.json @@ -11,7 +11,8 @@ "data": { "zip_code": "Postnummer" }, - "description": "Fyll ut ditt amerikanske eller kanadiske postnummer." + "description": "Fyll ut ditt amerikanske eller kanadiske postnummer.", + "title": "" } } } diff --git a/homeassistant/components/kodi/translations/pt.json b/homeassistant/components/kodi/translations/pt.json index 12aef9a997d..f28cee08d1b 100644 --- a/homeassistant/components/kodi/translations/pt.json +++ b/homeassistant/components/kodi/translations/pt.json @@ -18,16 +18,21 @@ "username": "Nome de Utilizador" } }, + "discovery_confirm": { + "title": "Kodi descoberto" + }, "host": { "data": { "host": "Servidor", - "port": "Porta" + "port": "Porta", + "ssl": "Conecte-se por SSL" } }, "user": { "data": { "host": "Servidor", - "port": "Porta" + "port": "Porta", + "ssl": "Conecte-se por SSL" }, "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." }, diff --git a/homeassistant/components/konnected/translations/no.json b/homeassistant/components/konnected/translations/no.json index ab7bae93b13..af39b9750a4 100644 --- a/homeassistant/components/konnected/translations/no.json +++ b/homeassistant/components/konnected/translations/no.json @@ -20,7 +20,8 @@ }, "user": { "data": { - "host": "IP adresse" + "host": "IP adresse", + "port": "" }, "description": "Vennligst skriv inn verten informasjon for din Konnected Panel." } diff --git a/homeassistant/components/lovelace/translations/nb.json b/homeassistant/components/lovelace/translations/nb.json new file mode 100644 index 00000000000..d8a4c453015 --- /dev/null +++ b/homeassistant/components/lovelace/translations/nb.json @@ -0,0 +1,3 @@ +{ + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/no.json b/homeassistant/components/lovelace/translations/no.json new file mode 100644 index 00000000000..d8a4c453015 --- /dev/null +++ b/homeassistant/components/lovelace/translations/no.json @@ -0,0 +1,3 @@ +{ + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/translations/no.json b/homeassistant/components/luftdaten/translations/no.json index 841ba4ad3da..8c1b69bed07 100644 --- a/homeassistant/components/luftdaten/translations/no.json +++ b/homeassistant/components/luftdaten/translations/no.json @@ -10,7 +10,8 @@ "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 39c8336e074..90489288b62 100644 --- a/homeassistant/components/met/translations/no.json +++ b/homeassistant/components/met/translations/no.json @@ -11,6 +11,7 @@ "longitude": "Lengdegrad", "name": "Navn" }, + "description": "", "title": "Lokasjon" } } diff --git a/homeassistant/components/met/translations/pt.json b/homeassistant/components/met/translations/pt.json index d134c28020f..1afabe51e79 100644 --- a/homeassistant/components/met/translations/pt.json +++ b/homeassistant/components/met/translations/pt.json @@ -11,6 +11,7 @@ "longitude": "Longitude", "name": "Nome" }, + "description": "", "title": "Localiza\u00e7\u00e3o" } } diff --git a/homeassistant/components/meteo_france/translations/no.json b/homeassistant/components/meteo_france/translations/no.json index 10c5915fdbc..91eea1fcec7 100644 --- a/homeassistant/components/meteo_france/translations/no.json +++ b/homeassistant/components/meteo_france/translations/no.json @@ -19,7 +19,8 @@ "data": { "city": "By" }, - "description": "Fyll inn postnummeret (bare for Frankrike, anbefalt) eller bynavn" + "description": "Fyll inn postnummeret (bare for Frankrike, anbefalt) eller bynavn", + "title": "" } } }, diff --git a/homeassistant/components/meteo_france/translations/pt.json b/homeassistant/components/meteo_france/translations/pt.json index 3137ef26505..025d58f5197 100644 --- a/homeassistant/components/meteo_france/translations/pt.json +++ b/homeassistant/components/meteo_france/translations/pt.json @@ -4,7 +4,8 @@ "user": { "data": { "city": "Cidade" - } + }, + "title": "" } } } diff --git a/homeassistant/components/metoffice/translations/fr.json b/homeassistant/components/metoffice/translations/fr.json index 44d4762d547..a046a71fe95 100644 --- a/homeassistant/components/metoffice/translations/fr.json +++ b/homeassistant/components/metoffice/translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, "error": { "cannot_connect": "Echec de connexion", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/mikrotik/translations/no.json b/homeassistant/components/mikrotik/translations/no.json index 0ae25436433..1e528fa4986 100644 --- a/homeassistant/components/mikrotik/translations/no.json +++ b/homeassistant/components/mikrotik/translations/no.json @@ -14,6 +14,7 @@ "host": "Vert", "name": "Navn", "password": "Passord", + "port": "", "username": "Brukernavn", "verify_ssl": "Bruk ssl" }, diff --git a/homeassistant/components/mill/translations/fr.json b/homeassistant/components/mill/translations/fr.json new file mode 100644 index 00000000000..9fa7c48bb68 --- /dev/null +++ b/homeassistant/components/mill/translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9" + }, + "error": { + "connection_error": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/no.json b/homeassistant/components/monoprice/translations/no.json index 45954d9840b..acd4bde8774 100644 --- a/homeassistant/components/monoprice/translations/no.json +++ b/homeassistant/components/monoprice/translations/no.json @@ -10,6 +10,7 @@ "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/no.json b/homeassistant/components/mqtt/translations/no.json index c5ee982b63d..b1863b90d1c 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -12,6 +12,7 @@ "broker": "Megler", "discovery": "Aktiver oppdagelse", "password": "Passord", + "port": "", "username": "Brukernavn" }, "description": "Vennligst fyll ut tilkoblingsinformasjonen for din MQTT megler." @@ -58,6 +59,7 @@ "data": { "broker": "Megler", "password": "Passord", + "port": "", "username": "Brukernavn" }, "description": "Vennligst oppgi tilkoblingsinformasjonen for din MQTT megler." diff --git a/homeassistant/components/nightscout/translations/fr.json b/homeassistant/components/nightscout/translations/fr.json index 5b31b06ea3a..9ed1ea1bfd1 100644 --- a/homeassistant/components/nightscout/translations/fr.json +++ b/homeassistant/components/nightscout/translations/fr.json @@ -3,6 +3,10 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/nut/translations/no.json b/homeassistant/components/nut/translations/no.json index 9bf7f981dca..6fd749442c3 100644 --- a/homeassistant/components/nut/translations/no.json +++ b/homeassistant/components/nut/translations/no.json @@ -16,6 +16,7 @@ }, "ups": { "data": { + "alias": "", "resources": "Ressurser" }, "title": "Velg UPS som skal overv\u00e5kes" @@ -24,6 +25,7 @@ "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 dcb3a102829..4f605a518d7 100644 --- a/homeassistant/components/onvif/translations/no.json +++ b/homeassistant/components/onvif/translations/no.json @@ -34,7 +34,8 @@ "manual_input": { "data": { "host": "Vert", - "name": "Navn" + "name": "Navn", + "port": "" }, "title": "Konfigurere ONVIF-enhet" }, diff --git a/homeassistant/components/opentherm_gw/translations/no.json b/homeassistant/components/opentherm_gw/translations/no.json index ed4dbd4abfb..f0ecf0277b2 100644 --- a/homeassistant/components/opentherm_gw/translations/no.json +++ b/homeassistant/components/opentherm_gw/translations/no.json @@ -10,8 +10,10 @@ "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 0342dd3ebcb..960e3a9cf5c 100644 --- a/homeassistant/components/opentherm_gw/translations/pt.json +++ b/homeassistant/components/opentherm_gw/translations/pt.json @@ -3,6 +3,7 @@ "step": { "init": { "data": { + "id": "", "name": "Nome" } } diff --git a/homeassistant/components/openweathermap/translations/ca.json b/homeassistant/components/openweathermap/translations/ca.json new file mode 100644 index 00000000000..240e378c0fa --- /dev/null +++ b/homeassistant/components/openweathermap/translations/ca.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "La integraci\u00f3 OpenWeatherMap per a aquestes coordenades ja est\u00e0 configurada." + }, + "error": { + "auth": "La clau API no \u00e9s correcta.", + "connection": "No s'ha pogut connectar amb l'API d'OWM" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API d'OpenWeatherMap", + "language": "Idioma", + "latitude": "Latitud", + "longitude": "Longitud", + "mode": "Mode", + "name": "Nom de la integraci\u00f3" + }, + "description": "Configura la integraci\u00f3 OpenWeatherMap. Per generar la clau API, ves a https://openweathermap.org/appid", + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Idioma", + "mode": "Mode" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/fr.json b/homeassistant/components/openweathermap/translations/fr.json index b55997e1f8d..ab53d663f48 100644 --- a/homeassistant/components/openweathermap/translations/fr.json +++ b/homeassistant/components/openweathermap/translations/fr.json @@ -1,17 +1,23 @@ { "config": { + "abort": { + "already_configured": "L'int\u00e9gration OpenWeatherMap pour ces coordonn\u00e9es est d\u00e9j\u00e0 configur\u00e9e." + }, "error": { - "auth": "La cl\u00e9 API n'est pas correcte." + "auth": "La cl\u00e9 API n'est pas correcte.", + "connection": "Impossible de se connecter \u00e0 l'API OWM" }, "step": { "user": { "data": { + "api_key": "Cl\u00e9 d'API OpenWeatherMap", "language": "Langue", "latitude": "Latitude", "longitude": "Longitude", "mode": "Mode", "name": "Nom de l'int\u00e9gration" }, + "description": "Configurez l'int\u00e9gration OpenWeatherMap. Pour g\u00e9n\u00e9rer la cl\u00e9 API, acc\u00e9dez \u00e0 https://openweathermap.org/appid", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/openweathermap/translations/no.json b/homeassistant/components/openweathermap/translations/no.json new file mode 100644 index 00000000000..cda3666ff18 --- /dev/null +++ b/homeassistant/components/openweathermap/translations/no.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "OpenWeatherMap-integrasjon for disse koordinatene er allerede konfigurert." + }, + "error": { + "auth": "API-n\u00f8kkelen er ikke korrekt.", + "connection": "Kan ikke koble til OWM API" + }, + "step": { + "user": { + "data": { + "api_key": "OpenWeatherMap API-n\u00f8kkel", + "language": "Spr\u00e5k", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "mode": "Modus", + "name": "Navn p\u00e5 integrasjon" + }, + "description": "Sett opp OpenWeatherMap-integrasjon. For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://openweathermap.org/appid", + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Spr\u00e5k", + "mode": "Modus" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/pt.json b/homeassistant/components/openweathermap/translations/pt.json new file mode 100644 index 00000000000..fa7fdc63989 --- /dev/null +++ b/homeassistant/components/openweathermap/translations/pt.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "auth": "A chave da API n\u00e3o est\u00e1 correta.", + "connection": "N\u00e3o \u00e9 poss\u00edvel conectar \u00e0 API OWM" + }, + "step": { + "user": { + "data": { + "language": "Idioma", + "latitude": "Latitude", + "longitude": "Longitude", + "mode": "Modo", + "name": "Nome da integra\u00e7\u00e3o" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Idioma", + "mode": "Modo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/fr.json b/homeassistant/components/ovo_energy/translations/fr.json index 2a1ce2d8d73..f8bdce3d3a2 100644 --- a/homeassistant/components/ovo_energy/translations/fr.json +++ b/homeassistant/components/ovo_energy/translations/fr.json @@ -1,10 +1,16 @@ { "config": { "error": { + "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9", + "authorization_error": "Erreur d'autorisation. V\u00e9rifiez vos identifiants.", "connection_error": "\u00c9chec de connexion" }, "step": { "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, "title": "Ajouter un compte OVO Energy" } } diff --git a/homeassistant/components/panasonic_viera/translations/no.json b/homeassistant/components/panasonic_viera/translations/no.json index 91a01793c1c..039adbd2ad3 100644 --- a/homeassistant/components/panasonic_viera/translations/no.json +++ b/homeassistant/components/panasonic_viera/translations/no.json @@ -11,6 +11,9 @@ }, "step": { "pairing": { + "data": { + "pin": "" + }, "description": "Angi PIN-koden som vises p\u00e5 TV-en", "title": "Sammenkobling" }, @@ -23,5 +26,6 @@ "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 98c0b9241fb..6d380619114 100644 --- a/homeassistant/components/person/translations/nb.json +++ b/homeassistant/components/person/translations/nb.json @@ -4,5 +4,6 @@ "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 98c0b9241fb..6d380619114 100644 --- a/homeassistant/components/person/translations/no.json +++ b/homeassistant/components/person/translations/no.json @@ -4,5 +4,6 @@ "home": "Hjemme", "not_home": "Borte" } - } + }, + "title": "" } \ 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 0e56dfaa0d9..1ccc5ac7d76 100644 --- a/homeassistant/components/pi_hole/translations/fr.json +++ b/homeassistant/components/pi_hole/translations/fr.json @@ -11,6 +11,7 @@ "data": { "api_key": "Cl\u00e9 d'API", "host": "H\u00f4te", + "location": "Emplacement", "name": "Nom", "port": "Port", "ssl": "Utiliser SSL", diff --git a/homeassistant/components/pi_hole/translations/no.json b/homeassistant/components/pi_hole/translations/no.json index 4655d254070..387b6c0d1eb 100644 --- a/homeassistant/components/pi_hole/translations/no.json +++ b/homeassistant/components/pi_hole/translations/no.json @@ -13,6 +13,7 @@ "host": "Vert", "location": "Beliggenhet", "name": "Navn", + "port": "", "ssl": "Bruk SSL", "verify_ssl": "Verifisere SSL-sertifikat" } diff --git a/homeassistant/components/plant/translations/nb.json b/homeassistant/components/plant/translations/nb.json index 0d144184263..c8f9e3e1d44 100644 --- a/homeassistant/components/plant/translations/nb.json +++ b/homeassistant/components/plant/translations/nb.json @@ -1,6 +1,7 @@ { "state": { "_": { + "ok": "", "problem": "Problem" } }, diff --git a/homeassistant/components/plant/translations/no.json b/homeassistant/components/plant/translations/no.json index 0a08a5eaed4..e82299e36e9 100644 --- a/homeassistant/components/plant/translations/no.json +++ b/homeassistant/components/plant/translations/no.json @@ -1,3 +1,9 @@ { + "state": { + "_": { + "ok": "", + "problem": "" + } + }, "title": "Plantemonitor" } \ No newline at end of file diff --git a/homeassistant/components/plex/translations/no.json b/homeassistant/components/plex/translations/no.json index 027260ea34d..c7374e27b60 100644 --- a/homeassistant/components/plex/translations/no.json +++ b/homeassistant/components/plex/translations/no.json @@ -19,6 +19,7 @@ "manual_setup": { "data": { "host": "Vert", + "port": "", "ssl": "Bruk SSL", "token": "Token (valgfritt)", "verify_ssl": "Verifisere SSL-sertifikat" @@ -26,16 +27,21 @@ "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." + "description": "Fortsett til [plex.tv] (https://plex.tv) for \u00e5 koble en Plex-server.", + "title": "" }, "user_advanced": { "data": { "setup_method": "Oppsettmetode" - } + }, + "title": "" } } }, diff --git a/homeassistant/components/plugwise/translations/ca.json b/homeassistant/components/plugwise/translations/ca.json index 40a7a0da317..c61c28e6668 100644 --- a/homeassistant/components/plugwise/translations/ca.json +++ b/homeassistant/components/plugwise/translations/ca.json @@ -19,5 +19,15 @@ "title": "Connecta't amb el Smile" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Interval d'escaneig (segons)" + }, + "description": "Ajusta les opcions de Plugwise" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/fr.json b/homeassistant/components/plugwise/translations/fr.json index 392d990fbbf..7ef9296193b 100644 --- a/homeassistant/components/plugwise/translations/fr.json +++ b/homeassistant/components/plugwise/translations/fr.json @@ -19,5 +19,15 @@ "title": "Se connecter \u00e0 Smile" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalle de mise \u00e0 jour (secondes)" + }, + "description": "Ajuster les options Plugwise" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/no.json b/homeassistant/components/plugwise/translations/no.json index 694e6348cae..2ee14f0e152 100644 --- a/homeassistant/components/plugwise/translations/no.json +++ b/homeassistant/components/plugwise/translations/no.json @@ -8,14 +8,26 @@ "invalid_auth": "Ugyldig godkjenning, sjekk din 8-tegns Smile ID", "unknown": "Uventet feil" }, + "flow_title": "", "step": { "user": { "data": { - "host": "Smile IP-adresse" + "host": "Smile IP-adresse", + "password": "" }, "description": "Detaljer", "title": "Koble til Smile" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Skanneintervall (sekunder)" + }, + "description": "Juster Plugwise-alternativer" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/pt.json b/homeassistant/components/plugwise/translations/pt.json index 0c5c7760566..808e3f3f7ea 100644 --- a/homeassistant/components/plugwise/translations/pt.json +++ b/homeassistant/components/plugwise/translations/pt.json @@ -3,5 +3,14 @@ "error": { "unknown": "Erro inesperado" } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o (segundos)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json index 650ae69a972..1c4f8ded80c 100644 --- a/homeassistant/components/plugwise/translations/ru.json +++ b/homeassistant/components/plugwise/translations/ru.json @@ -19,5 +19,15 @@ "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Plugwise" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/zh-Hant.json b/homeassistant/components/plugwise/translations/zh-Hant.json index 8cc068681c3..ee6966c1bdb 100644 --- a/homeassistant/components/plugwise/translations/zh-Hant.json +++ b/homeassistant/components/plugwise/translations/zh-Hant.json @@ -19,5 +19,15 @@ "title": "\u9023\u7dda\u81f3 Smile" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u6383\u63cf\u9593\u8ddd\uff08\u79d2\uff09" + }, + "description": "\u8abf\u6574 Plugwise \u9078\u9805" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/no.json b/homeassistant/components/poolsense/translations/no.json index a199f7384a8..38adc04c1db 100644 --- a/homeassistant/components/poolsense/translations/no.json +++ b/homeassistant/components/poolsense/translations/no.json @@ -12,7 +12,8 @@ "email": "E-post", "password": "Passord" }, - "description": "[%key:common::config_flow::description%]" + "description": "[%key:common::config_flow::description%]", + "title": "" } } } diff --git a/homeassistant/components/ps4/translations/no.json b/homeassistant/components/ps4/translations/no.json index 4bf3b02b0b5..814f09095a2 100644 --- a/homeassistant/components/ps4/translations/no.json +++ b/homeassistant/components/ps4/translations/no.json @@ -15,21 +15,26 @@ }, "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." + "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": "" }, "link": { "data": { + "code": "", "ip_address": "IP adresse", - "name": "Navn" + "name": "Navn", + "region": "" }, - "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." + "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": "" }, "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." + "description": "Velg modus for konfigurasjon. Feltet IP-adresse kan st\u00e5 tomt dersom du velger Automatisk Oppdagelse, da enheter vil bli oppdaget automatisk.", + "title": "" } } } diff --git a/homeassistant/components/rainmachine/translations/no.json b/homeassistant/components/rainmachine/translations/no.json index 294c2726396..bc80cdedb31 100644 --- a/homeassistant/components/rainmachine/translations/no.json +++ b/homeassistant/components/rainmachine/translations/no.json @@ -11,7 +11,8 @@ "user": { "data": { "ip_address": "Vertsnavn eller IP-adresse", - "password": "Passord" + "password": "Passord", + "port": "" }, "title": "Fyll ut informasjonen din" } diff --git a/homeassistant/components/remote/translations/ca.json b/homeassistant/components/remote/translations/ca.json index 94ff71f6d92..084711de1cb 100644 --- a/homeassistant/components/remote/translations/ca.json +++ b/homeassistant/components/remote/translations/ca.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "Commuta {entity_name}", + "turn_off": "Apaga {entity_name}", + "turn_on": "Engega {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est\u00e0 apagat/da", + "is_on": "{entity_name} est\u00e0 engegat/da" + }, + "trigger_type": { + "turned_off": "{entity_name} s'ha apagat", + "turned_on": "{entity_name} s'ha engegat" + } + }, "state": { "_": { "off": "OFF", diff --git a/homeassistant/components/remote/translations/en.json b/homeassistant/components/remote/translations/en.json index 731a21d4547..dc11ed5917d 100644 --- a/homeassistant/components/remote/translations/en.json +++ b/homeassistant/components/remote/translations/en.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "Toggle {entity_name}", + "turn_off": "Turn off {entity_name}", + "turn_on": "Turn on {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} is off", + "is_on": "{entity_name} is on" + }, + "trigger_type": { + "turned_off": "{entity_name} turned off", + "turned_on": "{entity_name} turned on" + } + }, "state": { "_": { "off": "Off", diff --git a/homeassistant/components/remote/translations/fr.json b/homeassistant/components/remote/translations/fr.json index ad2461e1f0a..37d97469645 100644 --- a/homeassistant/components/remote/translations/fr.json +++ b/homeassistant/components/remote/translations/fr.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "Basculer {entity_name}", + "turn_off": "\u00c9teindre {entity_name}", + "turn_on": "Allumer {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est d\u00e9sactiv\u00e9", + "is_on": "{entity_name} est activ\u00e9" + }, + "trigger_type": { + "turned_off": "{entity_name} s'est \u00e9teint", + "turned_on": "{entity_name} s'est allum\u00e9" + } + }, "state": { "_": { "off": "Inactif", diff --git a/homeassistant/components/remote/translations/pt.json b/homeassistant/components/remote/translations/pt.json index fb303b36aa6..929fd7863bd 100644 --- a/homeassistant/components/remote/translations/pt.json +++ b/homeassistant/components/remote/translations/pt.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "Alternar {entity_name}", + "turn_off": "Desligar {entity_name}", + "turn_on": "Ligar {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est\u00e1 desligada", + "is_on": "{entity_name} est\u00e1 ligada" + }, + "trigger_type": { + "turned_off": "{entity_name} desligou-se", + "turned_on": "{entity_name} ligou-se" + } + }, "state": { "_": { "off": "Desativado", diff --git a/homeassistant/components/remote/translations/ru.json b/homeassistant/components/remote/translations/ru.json index 14dd4a6ec2d..0a725bcca91 100644 --- a/homeassistant/components/remote/translations/ru.json +++ b/homeassistant/components/remote/translations/ru.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "\u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}", + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" + }, + "trigger_type": { + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" + } + }, "state": { "_": { "off": "\u0412\u044b\u043a\u043b", diff --git a/homeassistant/components/rfxtrx/translations/fr.json b/homeassistant/components/rfxtrx/translations/fr.json new file mode 100644 index 00000000000..c4bc0d48b1a --- /dev/null +++ b/homeassistant/components/rfxtrx/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/risco/translations/pt.json b/homeassistant/components/risco/translations/pt.json index eb4bf7ba6a7..7b98c6234c5 100644 --- a/homeassistant/components/risco/translations/pt.json +++ b/homeassistant/components/risco/translations/pt.json @@ -12,6 +12,7 @@ "user": { "data": { "password": "Palavra-passe", + "pin": "C\u00f3digo PIN", "username": "Nome de Utilizador" } } diff --git a/homeassistant/components/roku/translations/no.json b/homeassistant/components/roku/translations/no.json index 10fb51557da..43e0ea1f1c8 100644 --- a/homeassistant/components/roku/translations/no.json +++ b/homeassistant/components/roku/translations/no.json @@ -10,7 +10,8 @@ "flow_title": "Roku: {name}", "step": { "ssdp_confirm": { - "description": "Vil du sette opp {name} ?" + "description": "Vil du sette opp {name} ?", + "title": "" }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/no.json b/homeassistant/components/samsungtv/translations/no.json index 022dddcd00b..e0420ba74af 100644 --- a/homeassistant/components/samsungtv/translations/no.json +++ b/homeassistant/components/samsungtv/translations/no.json @@ -10,7 +10,8 @@ "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." + "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": "" }, "user": { "data": { diff --git a/homeassistant/components/scene/translations/no.json b/homeassistant/components/scene/translations/no.json new file mode 100644 index 00000000000..d8a4c453015 --- /dev/null +++ b/homeassistant/components/scene/translations/no.json @@ -0,0 +1,3 @@ +{ + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/script/translations/no.json b/homeassistant/components/script/translations/no.json index 6cace1e1570..28122450085 100644 --- a/homeassistant/components/script/translations/no.json +++ b/homeassistant/components/script/translations/no.json @@ -4,5 +4,6 @@ "off": "Av", "on": "P\u00e5" } - } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/es-419.json b/homeassistant/components/sensor/translations/es-419.json index acf91a79104..e724fe3a106 100644 --- a/homeassistant/components/sensor/translations/es-419.json +++ b/homeassistant/components/sensor/translations/es-419.json @@ -10,5 +10,11 @@ "value": "{entity_name} cambios de valor" } }, + "state": { + "_": { + "off": "", + "on": "" + } + }, "title": "Sensor" } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/nb.json b/homeassistant/components/sensor/translations/nb.json index 6cace1e1570..28122450085 100644 --- a/homeassistant/components/sensor/translations/nb.json +++ b/homeassistant/components/sensor/translations/nb.json @@ -4,5 +4,6 @@ "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 d8d05b81042..e957aef5b87 100644 --- a/homeassistant/components/sensor/translations/no.json +++ b/homeassistant/components/sensor/translations/no.json @@ -36,5 +36,6 @@ "off": "Av", "on": "P\u00e5" } - } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/no.json b/homeassistant/components/sentry/translations/no.json index 48504931104..6048bde74e1 100644 --- a/homeassistant/components/sentry/translations/no.json +++ b/homeassistant/components/sentry/translations/no.json @@ -13,7 +13,8 @@ "data": { "dsn": "DSN" }, - "description": "Fyll inn din Sentry DNS" + "description": "Fyll inn din Sentry DNS", + "title": "" } } }, diff --git a/homeassistant/components/simplisafe/translations/fr.json b/homeassistant/components/simplisafe/translations/fr.json index 730b9b810d1..9f62eb20823 100644 --- a/homeassistant/components/simplisafe/translations/fr.json +++ b/homeassistant/components/simplisafe/translations/fr.json @@ -8,6 +8,11 @@ "invalid_credentials": "Informations d'identification invalides" }, "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + } + }, "user": { "data": { "code": "Code (utilis\u00e9 dans l'interface Home Assistant)", diff --git a/homeassistant/components/smappee/translations/fr.json b/homeassistant/components/smappee/translations/fr.json index 18985ce9f34..d1dca6c5895 100644 --- a/homeassistant/components/smappee/translations/fr.json +++ b/homeassistant/components/smappee/translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "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." }, @@ -10,6 +11,11 @@ "environment": "Environnement" } }, + "local": { + "data": { + "host": "H\u00f4te" + } + }, "pick_implementation": { "title": "Choisissez la m\u00e9thode d'authentification" } diff --git a/homeassistant/components/soma/translations/no.json b/homeassistant/components/soma/translations/no.json index 5a084433eed..5c2c01ca7a6 100644 --- a/homeassistant/components/soma/translations/no.json +++ b/homeassistant/components/soma/translations/no.json @@ -13,9 +13,11 @@ "step": { "user": { "data": { - "host": "Vert" + "host": "Vert", + "port": "" }, - "description": "Vennligst fyll inn tilkoblingsinnstillingene for din SOMA Connect." + "description": "Vennligst fyll inn tilkoblingsinnstillingene for din SOMA Connect.", + "title": "" } } } diff --git a/homeassistant/components/sonarr/translations/no.json b/homeassistant/components/sonarr/translations/no.json index 2a7ede2964a..694565b72d6 100644 --- a/homeassistant/components/sonarr/translations/no.json +++ b/homeassistant/components/sonarr/translations/no.json @@ -8,12 +8,14 @@ "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" }, @@ -30,5 +32,6 @@ } } } - } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/no.json b/homeassistant/components/songpal/translations/no.json index 1096e9a0e89..eb07eeb2daa 100644 --- a/homeassistant/components/songpal/translations/no.json +++ b/homeassistant/components/songpal/translations/no.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes." }, + "flow_title": "", "step": { "init": { "description": "Vil du sette opp {name} ({host})?" diff --git a/homeassistant/components/spider/translations/fr.json b/homeassistant/components/spider/translations/fr.json index 807ba246694..959a28add76 100644 --- a/homeassistant/components/spider/translations/fr.json +++ b/homeassistant/components/spider/translations/fr.json @@ -2,6 +2,18 @@ "config": { "abort": { "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "error": { + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/fr.json b/homeassistant/components/squeezebox/translations/fr.json index 8107b902b4b..9e7445a4a32 100644 --- a/homeassistant/components/squeezebox/translations/fr.json +++ b/homeassistant/components/squeezebox/translations/fr.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", "no_server_found": "Impossible de d\u00e9couvrir automatiquement le serveur.", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/squeezebox/translations/no.json b/homeassistant/components/squeezebox/translations/no.json index dd9192cfb50..ddda0b61be2 100644 --- a/homeassistant/components/squeezebox/translations/no.json +++ b/homeassistant/components/squeezebox/translations/no.json @@ -10,11 +10,13 @@ "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" @@ -26,5 +28,6 @@ "title": "Konfigurer Logitech Media Server" } } - } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/starline/translations/no.json b/homeassistant/components/starline/translations/no.json index 89dc882cf82..36545f3efd7 100644 --- a/homeassistant/components/starline/translations/no.json +++ b/homeassistant/components/starline/translations/no.json @@ -17,7 +17,9 @@ "auth_captcha": { "data": { "captcha_code": "Kode fra bilde" - } + }, + "description": "", + "title": "" }, "auth_mfa": { "data": { diff --git a/homeassistant/components/switch/translations/es-419.json b/homeassistant/components/switch/translations/es-419.json index a7087a1bbf1..7fb04127b15 100644 --- a/homeassistant/components/switch/translations/es-419.json +++ b/homeassistant/components/switch/translations/es-419.json @@ -14,5 +14,11 @@ "turned_on": "{entity_name} encendido" } }, + "state": { + "_": { + "off": "", + "on": "" + } + }, "title": "Interruptor" } \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/no.json b/homeassistant/components/synology_dsm/translations/no.json index 2bfc824cb76..f8d7add4dc2 100644 --- a/homeassistant/components/synology_dsm/translations/no.json +++ b/homeassistant/components/synology_dsm/translations/no.json @@ -21,18 +21,22 @@ "link": { "data": { "password": "Passord", + "port": "", "ssl": "Bruk SSL/TLS til \u00e5 koble til NAS-en", "username": "Brukernavn" }, - "description": "Vil du konfigurere {name} ({host})?" + "description": "Vil du konfigurere {name} ({host})?", + "title": "" }, "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/tibber/translations/fr.json b/homeassistant/components/tibber/translations/fr.json index 223c1d44780..24fef7886ca 100644 --- a/homeassistant/components/tibber/translations/fr.json +++ b/homeassistant/components/tibber/translations/fr.json @@ -5,6 +5,7 @@ }, "error": { "connection_error": "Erreur de connexion \u00e0 Tibber", + "invalid_access_token": "Jeton d'acc\u00e8s non valide", "timeout": "D\u00e9lai de connexion \u00e0 Tibber" }, "step": { diff --git a/homeassistant/components/tibber/translations/no.json b/homeassistant/components/tibber/translations/no.json index 4480fb106de..34e078f5467 100644 --- a/homeassistant/components/tibber/translations/no.json +++ b/homeassistant/components/tibber/translations/no.json @@ -13,8 +13,10 @@ "data": { "access_token": "Tilgangstoken" }, - "description": "Fyll inn din tilgangstoken fra [https://developer.tibber.com/settings/accesstoken](https://developer.tibber.com/settings/accesstoken)" + "description": "Fyll inn din tilgangstoken fra [https://developer.tibber.com/settings/accesstoken](https://developer.tibber.com/settings/accesstoken)", + "title": "" } } - } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/pt.json b/homeassistant/components/tibber/translations/pt.json new file mode 100644 index 00000000000..243987422dd --- /dev/null +++ b/homeassistant/components/tibber/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "access_token": "" + }, + "title": "" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/no.json b/homeassistant/components/totalconnect/translations/no.json index a8d6ac5dc23..c312f98f3d2 100644 --- a/homeassistant/components/totalconnect/translations/no.json +++ b/homeassistant/components/totalconnect/translations/no.json @@ -11,7 +11,8 @@ "data": { "password": "Passord", "username": "Brukernavn" - } + }, + "title": "" } } } diff --git a/homeassistant/components/tradfri/translations/et.json b/homeassistant/components/tradfri/translations/et.json new file mode 100644 index 00000000000..5d0a728407a --- /dev/null +++ b/homeassistant/components/tradfri/translations/et.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "auth": { + "data": { + "host": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/no.json b/homeassistant/components/transmission/translations/no.json index d71e2c2c590..48b86516917 100644 --- a/homeassistant/components/transmission/translations/no.json +++ b/homeassistant/components/transmission/translations/no.json @@ -14,6 +14,7 @@ "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 ca6de86f30b..5681f95d984 100644 --- a/homeassistant/components/tuya/translations/no.json +++ b/homeassistant/components/tuya/translations/no.json @@ -17,7 +17,8 @@ "platform": "Appen der kontoen din registreres", "username": "Brukernavn" }, - "description": "Skriv inn din Tuya-legitimasjon." + "description": "Skriv inn din Tuya-legitimasjon.", + "title": "" } } } diff --git a/homeassistant/components/twentemilieu/translations/no.json b/homeassistant/components/twentemilieu/translations/no.json index 0ed6471e4fd..a9d3c184495 100644 --- a/homeassistant/components/twentemilieu/translations/no.json +++ b/homeassistant/components/twentemilieu/translations/no.json @@ -14,7 +14,8 @@ "house_number": "Husnummer", "post_code": "Postnummer" }, - "description": "Sett opp Twente Milieu som gir informasjon om innsamling av avfall p\u00e5 adressen din." + "description": "Sett opp Twente Milieu som gir informasjon om innsamling av avfall p\u00e5 adressen din.", + "title": "" } } } diff --git a/homeassistant/components/unifi/translations/fr.json b/homeassistant/components/unifi/translations/fr.json index e92c33c19de..21d422ace4d 100644 --- a/homeassistant/components/unifi/translations/fr.json +++ b/homeassistant/components/unifi/translations/fr.json @@ -49,6 +49,7 @@ }, "simple_options": { "data": { + "block_client": "Clients contr\u00f4l\u00e9s par acc\u00e8s r\u00e9seau", "track_clients": "Suivi de clients r\u00e9seaux", "track_devices": "Suivi d'\u00e9quipement r\u00e9seau (Equipements Ubiquiti)" }, diff --git a/homeassistant/components/unifi/translations/no.json b/homeassistant/components/unifi/translations/no.json index 2742ae6a0c5..a861790ba8d 100644 --- a/homeassistant/components/unifi/translations/no.json +++ b/homeassistant/components/unifi/translations/no.json @@ -13,6 +13,7 @@ "data": { "host": "Vert", "password": "Passord", + "port": "", "site": "Nettsted-ID", "username": "Brukernavn", "verify_ssl": "Kontroller bruker riktig sertifikat" diff --git a/homeassistant/components/volumio/translations/fr.json b/homeassistant/components/volumio/translations/fr.json new file mode 100644 index 00000000000..03844ccf99b --- /dev/null +++ b/homeassistant/components/volumio/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/fr.json b/homeassistant/components/wolflink/translations/fr.json new file mode 100644 index 00000000000..aa84ec33d8c --- /dev/null +++ b/homeassistant/components/wolflink/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ 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 534a6b3654e..22d3bbaccd3 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ca.json +++ b/homeassistant/components/xiaomi_aqara/translations/ca.json @@ -7,7 +7,7 @@ }, "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_host": "Adre\u00e7a IP no v\u00e0lida, consulta https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "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", diff --git a/homeassistant/components/xiaomi_aqara/translations/no.json b/homeassistant/components/xiaomi_aqara/translations/no.json index 0119813e3e4..39f472299d7 100644 --- a/homeassistant/components/xiaomi_aqara/translations/no.json +++ b/homeassistant/components/xiaomi_aqara/translations/no.json @@ -13,8 +13,12 @@ "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" }, @@ -32,7 +36,8 @@ "interface": "Nettverksgrensesnittet som skal brukes", "mac": "Mac-adresse (valgfritt)" }, - "description": "Koble til Xiaomi Aqara Gateway, hvis IP- og mac-adressene er tomme, brukes automatisk oppdagelse" + "description": "Koble til Xiaomi Aqara Gateway, hvis IP- og mac-adressene er tomme, brukes automatisk oppdagelse", + "title": "" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/pt.json b/homeassistant/components/xiaomi_aqara/translations/pt.json index 1983f8b28a5..a1340f587c9 100644 --- a/homeassistant/components/xiaomi_aqara/translations/pt.json +++ b/homeassistant/components/xiaomi_aqara/translations/pt.json @@ -4,6 +4,11 @@ "invalid_host": "Endere\u00e7o IP Inv\u00e1lido" }, "step": { + "select": { + "data": { + "select_ip": "" + } + }, "settings": { "data": { "name": "Nome da Gateway" diff --git a/homeassistant/components/xiaomi_miio/translations/no.json b/homeassistant/components/xiaomi_miio/translations/no.json index ff0ba48d98a..cf978d4a015 100644 --- a/homeassistant/components/xiaomi_miio/translations/no.json +++ b/homeassistant/components/xiaomi_miio/translations/no.json @@ -23,7 +23,8 @@ "data": { "gateway": "Koble til en Xiaomi Gateway" }, - "description": "Velg hvilken enhet du vil koble til." + "description": "Velg hvilken enhet du vil koble til.", + "title": "" } } } diff --git a/homeassistant/components/zha/translations/fr.json b/homeassistant/components/zha/translations/fr.json index 1bfe4e5a3ac..1abfb3a9502 100644 --- a/homeassistant/components/zha/translations/fr.json +++ b/homeassistant/components/zha/translations/fr.json @@ -70,6 +70,13 @@ "device_shaken": "Appareil secou\u00e9", "device_slid": "Appareil gliss\u00e9 \"{subtype}\"", "device_tilted": "Dispositif inclin\u00e9", + "remote_button_alt_double_press": "Double-clic sur le bouton \" {subtype} \" (mode alternatif)", + "remote_button_alt_long_press": "Bouton \" {subtype} \" enfonc\u00e9 en continu (mode alternatif)", + "remote_button_alt_long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long (mode alternatif)", + "remote_button_alt_quadruple_press": "Bouton \" {subtype} \" quadruple cliqu\u00e9 (mode alternatif)", + "remote_button_alt_quintuple_press": "Bouton \" {subtype} \" quintuple cliqu\u00e9 (mode alternatif)", + "remote_button_alt_short_press": "Bouton \" {subtype} \" appuy\u00e9 (mode alternatif)", + "remote_button_alt_short_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 (mode alternatif)", "remote_button_alt_triple_press": "\"{subtype}\" bouton triple-cliqu\u00e9 (mode alternatif)", "remote_button_double_press": "Double clic sur le bouton \" {subtype} \"", "remote_button_long_press": "Bouton \"{subtype}\" appuy\u00e9 continuellement", diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json index c5112235239..e97a4e27ccd 100644 --- a/homeassistant/components/zha/translations/no.json +++ b/homeassistant/components/zha/translations/no.json @@ -27,7 +27,8 @@ "data": { "path": "Seriell enhetsbane" }, - "description": "Velg seriell port for Zigbee radio" + "description": "Velg seriell port for Zigbee radio", + "title": "" } } }, diff --git a/homeassistant/components/zone/translations/no.json b/homeassistant/components/zone/translations/no.json index 415c0a6afaa..9bf6e189369 100644 --- a/homeassistant/components/zone/translations/no.json +++ b/homeassistant/components/zone/translations/no.json @@ -10,7 +10,8 @@ "latitude": "Breddegrad", "longitude": "Lengdegrad", "name": "Navn", - "passive": "Passiv" + "passive": "Passiv", + "radius": "" }, "title": "Definer sone parametere" } From bb9ea7ce6eaac2032d4514118fa17a68405088b5 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 8 Sep 2020 08:37:44 +0200 Subject: [PATCH 002/514] 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 56a52b4f4e5..6b1ef2f152a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -657,11 +657,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 a495946eebe3747b07b8037d84cb9212f5653b1f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Sep 2020 02:41:17 -0500 Subject: [PATCH 003/514] 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 0c8630a2f04bc832c455db2b832dce13f458e117 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 8 Sep 2020 10:35:01 +0200 Subject: [PATCH 004/514] 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 6b1ef2f152a..4462b99ffcc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -755,7 +755,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 c68056d3ad76bd601b2b6e344738c5d52b89596f Mon Sep 17 00:00:00 2001 From: Andreas Rehn Date: Tue, 8 Sep 2020 10:40:05 +0200 Subject: [PATCH 005/514] Add missing EDL21 OBIS codes (#39714) --- homeassistant/components/edl21/sensor.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index c33c9765730..0bc4c21fc2f 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -77,6 +77,18 @@ class EDL21: # D=7: Instantaneous value # E=0: Total "1-0:16.7.0*255": "Sum active instantaneous power", + # C=36: Active power L1 + # D=7: Instantaneous value + # E=0: Total + "1-0:36.7.0*255": "L1 active instantaneous power", + # C=56: Active power L1 + # D=7: Instantaneous value + # E=0: Total + "1-0:56.7.0*255": "L2 active instantaneous power", + # C=76: Active power L1 + # D=7: Instantaneous value + # E=0: Total + "1-0:76.7.0*255": "L3 active instantaneous power", } _OBIS_BLACKLIST = { # A=129: Manufacturer specific From 401002dcd4a9b935e0a8a422ac11ed2b52d57d32 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 8 Sep 2020 11:59:39 +0200 Subject: [PATCH 006/514] 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 fa1a53cdeb037dbb923a8f66f50ba3a3b1b885aa Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Sep 2020 13:50:53 +0200 Subject: [PATCH 007/514] 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 973524a4e68efee4c1e484e5c726d10f656a6db1 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 8 Sep 2020 14:13:48 +0200 Subject: [PATCH 008/514] 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 47610f242eb..369bcd40a1b 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -1565,6 +1565,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 1e2741ff1bacd1244f9c67ed838c3a5a740601e4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 15:12:20 +0200 Subject: [PATCH 009/514] 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 4462b99ffcc..1280df1e654 100644 --- a/.coveragerc +++ b/.coveragerc @@ -757,7 +757,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 5bcffba53ece85e66ea5af4e01d65ffc20b4941b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 15:23:29 +0200 Subject: [PATCH 010/514] 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 63aa46369b6413187eaf87c84f50ce198a67a511 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 15:23:38 +0200 Subject: [PATCH 011/514] 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 c6cba5ebc8d2f0bf17b913fe76d878545756f86e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 15:42:50 +0200 Subject: [PATCH 012/514] 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 8e4710a2a921a4c3c0d1ac6e7985230ab203a9bc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 8 Sep 2020 15:52:04 +0200 Subject: [PATCH 013/514] 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 a695dc20fd13e7ed1852722b79a6961b56a46ae9 Mon Sep 17 00:00:00 2001 From: Emilv2 Date: Tue, 8 Sep 2020 16:18:34 +0200 Subject: [PATCH 014/514] 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 f075823529eab589c81f71cd6b753c4d885fc647 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 8 Sep 2020 16:39:48 +0200 Subject: [PATCH 015/514] Bump version to 0.116.0dev0 (#39768) --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ea5c203ee02..4411de047d5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 115 +MINOR_VERSION = 116 PATCH_VERSION = "0.dev0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" From 586d7eaba6a21f36ee02646189bafd87a9aac0f0 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 8 Sep 2020 16:42:01 +0200 Subject: [PATCH 016/514] 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 369bcd40a1b..51287f9f288 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 0507ec9d8b6c03833167b982141c95baa4a7d4fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Sep 2020 10:08:31 -0500 Subject: [PATCH 017/514] 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 4bbd69955b289ade1dfaebb839610b2f21e1f152 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Sep 2020 10:12:23 -0500 Subject: [PATCH 018/514] 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 8adfcf23f0f1317214113b03ff571842a642244e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Sep 2020 10:31:08 -0500 Subject: [PATCH 019/514] 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 066a4185182a9500eb691ed26fed8137652f8b0c 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 020/514] 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 9aba1985c79beaa6dd5551ac42351ce2a95357f0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Sep 2020 23:11:42 +0200 Subject: [PATCH 021/514] 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 a45f5c7831ad24c00b90692a24688974894807b8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 8 Sep 2020 23:17:30 +0200 Subject: [PATCH 022/514] 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 748e2696dc20ea4cf618b5e42f3fb39911f93413 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 8 Sep 2020 23:22:44 +0200 Subject: [PATCH 023/514] 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 95a33ba89496bf954ce72e2e2676ea2932335be7 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 8 Sep 2020 23:38:41 +0200 Subject: [PATCH 024/514] Bump Synology DSM to 0.9.0 (#39819) --- CODEOWNERS | 2 +- homeassistant/components/synology_dsm/__init__.py | 6 ++++-- homeassistant/components/synology_dsm/config_flow.py | 11 ++++++----- homeassistant/components/synology_dsm/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 70d89c5e45e..d186ff45cbb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -420,7 +420,7 @@ homeassistant/components/switchbot/* @danielhiversen homeassistant/components/switcher_kis/* @tomerfi homeassistant/components/switchmate/* @danielhiversen homeassistant/components/syncthru/* @nielstron -homeassistant/components/synology_dsm/* @ProtoThis @Quentame +homeassistant/components/synology_dsm/* @hacf-fr @Quentame homeassistant/components/synology_srm/* @aerialls homeassistant/components/syslog/* @fabaff homeassistant/components/tado/* @michaelarnauts @bdraco diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 2e4b550337b..7e229ef4a5b 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -311,7 +311,9 @@ class SynoApi: def _fetch_device_configuration(self): """Fetch initial device config.""" self.information = self.dsm.information + self.information.update() self.network = self.dsm.network + self.network.update() if self._with_security: self.security = self.dsm.security @@ -444,7 +446,7 @@ class SynologyDSMDeviceEntity(SynologyDSMEntity): self._device_type = None if "volume" in entity_type: - volume = self._api.storage._get_volume(self._device_id) + volume = self._api.storage.get_volume(self._device_id) # Volume does not have a name self._device_name = volume["id"].replace("_", " ").capitalize() self._device_manufacturer = "Synology" @@ -457,7 +459,7 @@ class SynologyDSMDeviceEntity(SynologyDSMEntity): .replace("shr", "SHR") ) elif "disk" in entity_type: - disk = self._api.storage._get_disk(self._device_id) + disk = self._api.storage.get_disk(self._device_id) self._device_name = disk["name"] self._device_manufacturer = disk["vendor"] self._device_model = disk["model"].strip() diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index a4d7d75e073..a9df6f362cc 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -260,14 +260,15 @@ def _login_and_fetch_syno_info(api, otp_code): """Login to the NAS and fetch basic data.""" # These do i/o api.login(otp_code) - utilisation = api.utilisation - storage = api.storage + api.utilisation.update() + api.storage.update() + api.network.update() if ( not api.information.serial - or utilisation.cpu_user_load is None - or not storage.disks_ids - or not storage.volumes_ids + or api.utilisation.cpu_user_load is None + or not api.storage.disks_ids + or not api.storage.volumes_ids or not api.network.macs ): raise InvalidData diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 6ad926cfb9e..aee8c464464 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,8 +2,8 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["python-synology==0.8.2"], - "codeowners": ["@ProtoThis", "@Quentame"], + "requirements": ["python-synology==0.9.0"], + "codeowners": ["@hacf-fr", "@Quentame"], "config_flow": true, "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 22688907370..a24f9c66737 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1761,7 +1761,7 @@ python-sochain-api==0.0.2 python-songpal==0.12 # homeassistant.components.synology_dsm -python-synology==0.8.2 +python-synology==0.9.0 # homeassistant.components.tado python-tado==0.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3b94a5c1da..21e2b2f82c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -830,7 +830,7 @@ python-openzwave-mqtt==1.0.5 python-songpal==0.12 # homeassistant.components.synology_dsm -python-synology==0.8.2 +python-synology==0.9.0 # homeassistant.components.tado python-tado==0.8.1 From 1c4951a8d751e636f4757431b43b79da7d0cf7a4 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 8 Sep 2020 23:42:45 +0200 Subject: [PATCH 025/514] 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 90892d275c259088ed302bdaa8838303a6ef4094 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 9 Sep 2020 00:03:44 +0000 Subject: [PATCH 026/514] [ci skip] Translation update --- .../binary_sensor/translations/ca.json | 8 ++--- .../components/cover/translations/ca.json | 6 ++-- .../components/dsmr/translations/it.json | 7 ++++ .../components/gogogate2/translations/it.json | 2 +- .../components/group/translations/ca.json | 2 +- .../homematicip_cloud/translations/it.json | 3 +- .../media_player/translations/ca.json | 2 +- .../openweathermap/translations/it.json | 35 +++++++++++++++++++ .../components/plugwise/translations/cs.json | 11 ++++++ .../components/plugwise/translations/it.json | 10 ++++++ .../components/remote/translations/ca.json | 4 +-- .../components/remote/translations/cs.json | 15 ++++++++ .../components/remote/translations/it.json | 15 ++++++++ .../components/vacuum/translations/ca.json | 2 +- .../xiaomi_aqara/translations/it.json | 2 +- .../components/yeelight/translations/it.json | 3 +- 16 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/dsmr/translations/it.json create mode 100644 homeassistant/components/openweathermap/translations/it.json create mode 100644 homeassistant/components/plugwise/translations/cs.json diff --git a/homeassistant/components/binary_sensor/translations/ca.json b/homeassistant/components/binary_sensor/translations/ca.json index bf16523251e..ee06ef9d87f 100644 --- a/homeassistant/components/binary_sensor/translations/ca.json +++ b/homeassistant/components/binary_sensor/translations/ca.json @@ -107,11 +107,11 @@ "on": "Connectat" }, "door": { - "off": "Tancat/da", + "off": "Tancat/ada", "on": "Obert/a" }, "garage_door": { - "off": "Tancat/da", + "off": "Tancat/ada", "on": "Obert/a" }, "gas": { @@ -139,7 +139,7 @@ "on": "Detectat" }, "opening": { - "off": "Tancat/da", + "off": "Tancat/ada", "on": "Obert/a" }, "presence": { @@ -167,7 +167,7 @@ "on": "Detectat" }, "window": { - "off": "Tancat/da", + "off": "Tancat/ada", "on": "Obert/a" } }, diff --git a/homeassistant/components/cover/translations/ca.json b/homeassistant/components/cover/translations/ca.json index d033606abdd..3c4cb0c8b1b 100644 --- a/homeassistant/components/cover/translations/ca.json +++ b/homeassistant/components/cover/translations/ca.json @@ -10,7 +10,7 @@ "stop": "Atura {entity_name}" }, "condition_type": { - "is_closed": "{entity_name} est\u00e0 tancat/da", + "is_closed": "{entity_name} est\u00e0 tancat/ada", "is_closing": "{entity_name} est\u00e0 tancant-se", "is_open": "{entity_name} est\u00e0 obert/a", "is_opening": "{entity_name} s'est\u00e0 obrint", @@ -18,7 +18,7 @@ "is_tilt_position": "La inclinaci\u00f3 actual de {entity_name} \u00e9s" }, "trigger_type": { - "closed": "{entity_name} tancat/da", + "closed": "{entity_name} tancat/ada", "closing": "{entity_name} tancant-se", "opened": "{entity_name} s'ha obert", "opening": "{entity_name} obrint-se", @@ -28,7 +28,7 @@ }, "state": { "_": { - "closed": "Tancat/da", + "closed": "Tancat/ada", "closing": "Tancant", "open": "Obert/a", "opening": "Obrint", diff --git a/homeassistant/components/dsmr/translations/it.json b/homeassistant/components/dsmr/translations/it.json new file mode 100644 index 00000000000..43906c0d6c8 --- /dev/null +++ b/homeassistant/components/dsmr/translations/it.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/it.json b/homeassistant/components/gogogate2/translations/it.json index 378d55630a4..7b1dbe4e3e4 100644 --- a/homeassistant/components/gogogate2/translations/it.json +++ b/homeassistant/components/gogogate2/translations/it.json @@ -15,7 +15,7 @@ "username": "Nome utente" }, "description": "Fornire le informazioni richieste di seguito.", - "title": "Configurazione GogoGate2" + "title": "Configurazione di GogoGate2 o iSmartGate" } } } diff --git a/homeassistant/components/group/translations/ca.json b/homeassistant/components/group/translations/ca.json index 21b6361589c..552a2c9677e 100644 --- a/homeassistant/components/group/translations/ca.json +++ b/homeassistant/components/group/translations/ca.json @@ -1,7 +1,7 @@ { "state": { "_": { - "closed": "Tancat/da", + "closed": "Tancat/ada", "home": "A casa", "locked": "Bloquejat", "not_home": "Fora", diff --git a/homeassistant/components/homematicip_cloud/translations/it.json b/homeassistant/components/homematicip_cloud/translations/it.json index 9be01273fc1..4e7bfd1108c 100644 --- a/homeassistant/components/homematicip_cloud/translations/it.json +++ b/homeassistant/components/homematicip_cloud/translations/it.json @@ -6,7 +6,8 @@ "unknown": "Si \u00e8 verificato un errore sconosciuto." }, "error": { - "invalid_sgtin_or_pin": "PIN non valido, riprova.", + "invalid_pin": "PIN non valido, riprova.", + "invalid_sgtin_or_pin": "SGTIN o PIN non valido, riprovare.", "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/media_player/translations/ca.json b/homeassistant/components/media_player/translations/ca.json index 1a1c161e5ec..67f7aad655b 100644 --- a/homeassistant/components/media_player/translations/ca.json +++ b/homeassistant/components/media_player/translations/ca.json @@ -13,7 +13,7 @@ "idle": "Inactiu", "off": "OFF", "on": "ON", - "paused": "Pausat/da", + "paused": "Pausat/ada", "playing": "Reproduint", "standby": "En espera" } diff --git a/homeassistant/components/openweathermap/translations/it.json b/homeassistant/components/openweathermap/translations/it.json new file mode 100644 index 00000000000..c53e88d9558 --- /dev/null +++ b/homeassistant/components/openweathermap/translations/it.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "L'integrazione di OpenWeatherMap per queste coordinate \u00e8 gi\u00e0 configurata." + }, + "error": { + "auth": "La chiave API non \u00e8 corretta.", + "connection": "Impossibile connettersi all'API OWM" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API OpenWeatherMap", + "language": "Lingua", + "latitude": "Latitudine", + "longitude": "Logitudine", + "mode": "Modalit\u00e0", + "name": "Nome dell'integrazione" + }, + "description": "Configura l'integrazione di OpenWeatherMap. Per generare la chiave API, vai su https://openweathermap.org/appid", + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Lingua", + "mode": "Modalit\u00e0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/cs.json b/homeassistant/components/plugwise/translations/cs.json new file mode 100644 index 00000000000..ca6cd85a20f --- /dev/null +++ b/homeassistant/components/plugwise/translations/cs.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Interval sledov\u00e1n\u00ed (v sekund\u00e1ch)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/it.json b/homeassistant/components/plugwise/translations/it.json index b6c03cf8899..b1b3365125a 100644 --- a/homeassistant/components/plugwise/translations/it.json +++ b/homeassistant/components/plugwise/translations/it.json @@ -19,5 +19,15 @@ "title": "Connettersi al dispositivo" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervallo di scansione (secondi)" + }, + "description": "Regolare le opzioni Plugwise" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/remote/translations/ca.json b/homeassistant/components/remote/translations/ca.json index 084711de1cb..7e001059f14 100644 --- a/homeassistant/components/remote/translations/ca.json +++ b/homeassistant/components/remote/translations/ca.json @@ -6,8 +6,8 @@ "turn_on": "Engega {entity_name}" }, "condition_type": { - "is_off": "{entity_name} est\u00e0 apagat/da", - "is_on": "{entity_name} est\u00e0 engegat/da" + "is_off": "{entity_name} est\u00e0 apagat/ada", + "is_on": "{entity_name} est\u00e0 engegat/ada" }, "trigger_type": { "turned_off": "{entity_name} s'ha apagat", diff --git a/homeassistant/components/remote/translations/cs.json b/homeassistant/components/remote/translations/cs.json index 098b1191b8f..91102dd4461 100644 --- a/homeassistant/components/remote/translations/cs.json +++ b/homeassistant/components/remote/translations/cs.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "P\u0159epnout {entity_name}", + "turn_off": "Vypnout {entity_name}", + "turn_on": "Zapnout {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} je vypnuto", + "is_on": "{entity_name} je zapnuto" + }, + "trigger_type": { + "turned_off": "{entity_name} vypnuto", + "turned_on": "{entity_name} zapnuto" + } + }, "state": { "_": { "off": "Neaktivn\u00ed", diff --git a/homeassistant/components/remote/translations/it.json b/homeassistant/components/remote/translations/it.json index e3ebc2a3dda..e770712be19 100644 --- a/homeassistant/components/remote/translations/it.json +++ b/homeassistant/components/remote/translations/it.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "Commuta {entity_name}", + "turn_off": "Disattivare {entity_name}", + "turn_on": "Attivare {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u00e8 spento", + "is_on": "{entity_name} \u00e8 acceso" + }, + "trigger_type": { + "turned_off": "{entity_name} disattivato", + "turned_on": "{entity_name} attivato" + } + }, "state": { "_": { "off": "Spento", diff --git a/homeassistant/components/vacuum/translations/ca.json b/homeassistant/components/vacuum/translations/ca.json index 5f8f234d808..d98a51a5363 100644 --- a/homeassistant/components/vacuum/translations/ca.json +++ b/homeassistant/components/vacuum/translations/ca.json @@ -21,7 +21,7 @@ "idle": "Inactiu", "off": "OFF", "on": "ON", - "paused": "Pausat/da", + "paused": "Pausat/ada", "returning": "Retornant a base" } }, diff --git a/homeassistant/components/xiaomi_aqara/translations/it.json b/homeassistant/components/xiaomi_aqara/translations/it.json index 6b7f6d907ae..dfffec72272 100644 --- a/homeassistant/components/xiaomi_aqara/translations/it.json +++ b/homeassistant/components/xiaomi_aqara/translations/it.json @@ -7,7 +7,7 @@ }, "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_host": "Indirizzo IP non valido, vedere https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "Interfaccia di rete non valida", "invalid_key": "Chiave gateway non valida", "invalid_mac": "Indirizzo Mac non valido", diff --git a/homeassistant/components/yeelight/translations/it.json b/homeassistant/components/yeelight/translations/it.json index e3d965661b5..1c887037b8e 100644 --- a/homeassistant/components/yeelight/translations/it.json +++ b/homeassistant/components/yeelight/translations/it.json @@ -15,9 +15,10 @@ }, "user": { "data": { + "host": "Host", "ip_address": "Indirizzo IP" }, - "description": "Se lasci vuoto l'indirizzo IP, verr\u00e0 utilizzato il rilevamento per trovare i dispositivi." + "description": "Se lasci l'host vuoto, il rilevamento verr\u00e0 utilizzato per trovare i dispositivi." } } }, From 8185ddf9a122228bd339df46ffe69aa449dc7f55 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Wed, 9 Sep 2020 12:57:24 +0200 Subject: [PATCH 027/514] Add systemmonitor check for mandatory "arg" of sensors (#39687) * Added check for mandatory "arg" of sensors * Make pylint happy * Moved to vol validator function for "arg" checks * Make pylint happy once again * Adjustments from code review --- .../components/systemmonitor/sensor.py | 82 +++++++++++++------ 1 file changed, 59 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index e8ff20b2b5a..e7ad77e1842 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -35,43 +35,78 @@ if sys.maxsize > 2 ** 32: else: CPU_ICON = "mdi:cpu-32-bit" +# Schema: [name, unit of measurement, icon, device class, flag if mandatory arg] 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)", 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"], - "load_15m": ["Load (15m)", " ", "mdi:memory", None], - "load_1m": ["Load (1m)", " ", "mdi:memory", None], - "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)", 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], - "packets_out": ["Packets out", " ", "mdi:server-network", None], + "disk_free": ["Disk free", DATA_GIBIBYTES, "mdi:harddisk", None, False], + "disk_use": ["Disk use", DATA_GIBIBYTES, "mdi:harddisk", None, False], + "disk_use_percent": [ + "Disk use (percent)", + PERCENTAGE, + "mdi:harddisk", + None, + False, + ], + "ipv4_address": ["IPv4 address", "", "mdi:server-network", None, True], + "ipv6_address": ["IPv6 address", "", "mdi:server-network", None, True], + "last_boot": ["Last boot", "", "mdi:clock", "timestamp", False], + "load_15m": ["Load (15m)", " ", CPU_ICON, None, False], + "load_1m": ["Load (1m)", " ", CPU_ICON, None, False], + "load_5m": ["Load (5m)", " ", CPU_ICON, None, False], + "memory_free": ["Memory free", DATA_MEBIBYTES, "mdi:memory", None, False], + "memory_use": ["Memory use", DATA_MEBIBYTES, "mdi:memory", None, False], + "memory_use_percent": [ + "Memory use (percent)", + PERCENTAGE, + "mdi:memory", + None, + False, + ], + "network_in": ["Network in", DATA_MEBIBYTES, "mdi:server-network", None, True], + "network_out": ["Network out", DATA_MEBIBYTES, "mdi:server-network", None, True], + "packets_in": ["Packets in", " ", "mdi:server-network", None, True], + "packets_out": ["Packets out", " ", "mdi:server-network", None, True], "throughput_network_in": [ "Network throughput in", DATA_RATE_MEGABYTES_PER_SECOND, "mdi:server-network", None, + True, ], "throughput_network_out": [ "Network throughput out", DATA_RATE_MEGABYTES_PER_SECOND, "mdi:server-network", - None, + True, ], - "process": ["Process", " ", 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)", PERCENTAGE, "mdi:harddisk", None], + "process": ["Process", " ", CPU_ICON, None, True], + "processor_use": ["Processor use", PERCENTAGE, CPU_ICON, None, False], + "processor_temperature": [ + "Processor temperature", + TEMP_CELSIUS, + CPU_ICON, + None, + False, + ], + "swap_free": ["Swap free", DATA_MEBIBYTES, "mdi:harddisk", None, True], + "swap_use": ["Swap use", DATA_MEBIBYTES, "mdi:harddisk", None, False], + "swap_use_percent": ["Swap use (percent)", PERCENTAGE, "mdi:harddisk", None, False], } + +def check_required_arg(value): + """Validate that the required "arg" for the sensor types that need it are set.""" + for sensor in value: + sensor_type = sensor[CONF_TYPE] + sensor_arg = sensor.get(CONF_ARG) + + if sensor_arg is None and SENSOR_TYPES[sensor_type][4]: + raise vol.RequiredFieldInvalid( + f"Mandatory 'arg' is missing for sensor type '{sensor_type}'." + ) + + return value + + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_RESOURCES, default={CONF_TYPE: "disk_use"}): vol.All( @@ -84,6 +119,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) ], + check_required_arg, ) } ) From 4f342eae2717e164fb11038661f63b71473a82f9 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 9 Sep 2020 14:12:11 +0200 Subject: [PATCH 028/514] 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 be04f14a789dda255ba62be864e491909a48f9b2 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 9 Sep 2020 14:48:28 +0200 Subject: [PATCH 029/514] 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 74219663d617492ec49cd25f284c56777916233c Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 9 Sep 2020 07:56:40 -0500 Subject: [PATCH 030/514] 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 1280df1e654..162e0c65f06 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 1f000e2c3e9780e352d17d32d1965be41a08dce9 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 9 Sep 2020 08:08:43 -0700 Subject: [PATCH 031/514] Allow setting of hvac_mode when setting temperature in ozw (#39853) --- homeassistant/components/ozw/climate.py | 6 ++++ tests/components/ozw/test_climate.py | 40 ++++++++++++++++++------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/ozw/climate.py b/homeassistant/components/ozw/climate.py index 1486d98de2c..a74fd869f0f 100644 --- a/homeassistant/components/ozw/climate.py +++ b/homeassistant/components/ozw/climate.py @@ -5,6 +5,7 @@ from typing import Optional, Tuple from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity from homeassistant.components.climate.const import ( + ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, @@ -252,6 +253,11 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): Must know if single or double setpoint. """ + hvac_mode = kwargs.get(ATTR_HVAC_MODE) + + if hvac_mode is not None: + await self.async_set_hvac_mode(hvac_mode) + if len(self._current_mode_setpoint_values) == 1: setpoint = self._current_mode_setpoint_values[0] target_temp = kwargs.get(ATTR_TEMPERATURE) diff --git a/tests/components/ozw/test_climate.py b/tests/components/ozw/test_climate.py index 70fba99f7f2..26295e79b07 100644 --- a/tests/components/ozw/test_climate.py +++ b/tests/components/ozw/test_climate.py @@ -57,6 +57,24 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): assert round(msg["payload"]["Value"], 2) == 78.98 assert msg["payload"]["ValueIDKey"] == 281475099443218 + # Test hvac_mode with set_temperature + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.ct32_thermostat_mode", + "temperature": 24.1, + "hvac_mode": "cool", + }, + blocking=True, + ) + assert len(sent_messages) == 3 # 2 messages + msg = sent_messages[-1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + # Celsius is converted to Fahrenheit here! + assert round(msg["payload"]["Value"], 2) == 75.38 + assert msg["payload"]["ValueIDKey"] == 281475099443218 + # Test set mode await hass.services.async_call( "climate", @@ -64,7 +82,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): {"entity_id": "climate.ct32_thermostat_mode", "hvac_mode": HVAC_MODE_HEAT_COOL}, blocking=True, ) - assert len(sent_messages) == 2 + assert len(sent_messages) == 4 msg = sent_messages[-1] assert msg["topic"] == "OpenZWave/1/command/setvalue/" assert msg["payload"] == {"Value": 3, "ValueIDKey": 122683412} @@ -76,7 +94,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): {"entity_id": "climate.ct32_thermostat_mode", "hvac_mode": "fan_only"}, blocking=True, ) - assert len(sent_messages) == 2 + assert len(sent_messages) == 4 assert "Received an invalid hvac mode: fan_only" in caplog.text # Test set fan mode @@ -86,7 +104,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): {"entity_id": "climate.ct32_thermostat_mode", "fan_mode": "On Low"}, blocking=True, ) - assert len(sent_messages) == 3 + assert len(sent_messages) == 5 msg = sent_messages[-1] assert msg["topic"] == "OpenZWave/1/command/setvalue/" assert msg["payload"] == {"Value": 1, "ValueIDKey": 122748948} @@ -98,7 +116,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): {"entity_id": "climate.ct32_thermostat_mode", "fan_mode": "invalid fan mode"}, blocking=True, ) - assert len(sent_messages) == 3 + assert len(sent_messages) == 5 assert "Received an invalid fan mode: invalid fan mode" in caplog.text # Test incoming mode change to auto, @@ -123,7 +141,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): }, blocking=True, ) - assert len(sent_messages) == 5 # 2 messages ! + assert len(sent_messages) == 7 # 2 messages ! msg = sent_messages[-2] # low setpoint assert msg["topic"] == "OpenZWave/1/command/setvalue/" assert round(msg["payload"]["Value"], 2) == 68.0 @@ -162,7 +180,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): }, blocking=True, ) - assert len(sent_messages) == 6 + assert len(sent_messages) == 8 msg = sent_messages[-1] assert msg["topic"] == "OpenZWave/1/command/setvalue/" assert msg["payload"] == { @@ -180,7 +198,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): }, blocking=True, ) - assert len(sent_messages) == 7 + assert len(sent_messages) == 9 msg = sent_messages[-1] assert msg["topic"] == "OpenZWave/1/command/setvalue/" assert msg["payload"] == { @@ -199,7 +217,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): }, blocking=True, ) - assert len(sent_messages) == 8 + assert len(sent_messages) == 10 msg = sent_messages[-1] assert msg["topic"] == "OpenZWave/1/command/setvalue/" assert msg["payload"] == { @@ -217,7 +235,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): }, blocking=True, ) - assert len(sent_messages) == 8 + assert len(sent_messages) == 10 assert "Received an invalid preset mode: invalid preset mode" in caplog.text # test thermostat device without a mode commandclass @@ -244,7 +262,7 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): }, blocking=True, ) - assert len(sent_messages) == 9 + assert len(sent_messages) == 11 msg = sent_messages[-1] assert msg["topic"] == "OpenZWave/1/command/setvalue/" assert msg["payload"] == { @@ -261,5 +279,5 @@ async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): }, blocking=True, ) - assert len(sent_messages) == 9 + assert len(sent_messages) == 11 assert "does not support setting a mode" in caplog.text From 5fd059a3b63cf00943fe7434d72d0c4da9f66bce Mon Sep 17 00:00:00 2001 From: Colin Frei Date: Wed, 9 Sep 2020 20:19:37 +0200 Subject: [PATCH 032/514] 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 b0b0b15d9dfe034cd1738d4868567dead28f0de2 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 9 Sep 2020 15:08:55 -0400 Subject: [PATCH 033/514] 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 a24f9c66737..7287f613fcc 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 21e2b2f82c4..28906de633d 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 b7ad83c65571b2a5f18f185f3030e924a83fe97d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Sep 2020 15:19:14 -0500 Subject: [PATCH 034/514] 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 7287f613fcc..c66acc032d4 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 28906de633d..5e18738f556 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 9358b5089e8e3a8fd664487cf449557b9b6c9757 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Wed, 9 Sep 2020 16:19:30 -0400 Subject: [PATCH 035/514] 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 e6bc48ab68a08ff742708a76390c2a68592e9a97 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 9 Sep 2020 22:19:52 +0200 Subject: [PATCH 036/514] 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 9c40b511f250fc40497da3ec9cefda177431b962 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 9 Sep 2020 22:30:40 +0200 Subject: [PATCH 037/514] 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 a918981ff3b0798717e7f1ea8c261e674fd4b5b2 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 9 Sep 2020 16:22:26 -0500 Subject: [PATCH 038/514] 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 94b4824c279d6ae9b411dacbbfa9f3e2b642c3fc 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 039/514] 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 c66acc032d4..315ed0b7c12 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 5e18738f556..6cfb65411e6 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 c06b18b47b0c272b64965773cbbc679eb8ab423b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Sep 2020 00:36:58 +0200 Subject: [PATCH 040/514] 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 0e11c10468894dd128698ede47abc7a2ae50aa20 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 9 Sep 2020 16:41:07 -0600 Subject: [PATCH 041/514] Prompt user to reauthenticate AirVisual when API key expires (#38341) * Prompt user to reauthenticate AirVisual when API key expires * Don't version bump * Cleanup * Linting --- .../components/airvisual/__init__.py | 15 ++- .../components/airvisual/config_flow.py | 58 +++++++-- .../components/airvisual/test_config_flow.py | 116 +++++++++++------- 3 files changed, 138 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 563e24bf8fd..f06e4fe70b7 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -4,7 +4,12 @@ from datetime import timedelta from math import ceil from pyairvisual import Client -from pyairvisual.errors import AirVisualError, NodeProError +from pyairvisual.errors import ( + AirVisualError, + InvalidKeyError, + KeyExpiredError, + NodeProError, +) import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -229,6 +234,14 @@ async def async_setup_entry(hass, config_entry): try: return await api_coro + except (InvalidKeyError, KeyExpiredError): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "reauth"}, + data=config_entry.data, + ) + ) except AirVisualError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index abbc2df9061..bb1c262eba7 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -34,12 +34,19 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 2 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + def __init__(self): + """Initialize the config flow.""" + self._geo_id = None + self._latitude = None + self._longitude = None + + self.api_key_data_schema = vol.Schema({vol.Required(CONF_API_KEY): str}) + @property def geography_schema(self): """Return the data schema for the cloud API.""" - return vol.Schema( + return self.api_key_data_schema.extend( { - vol.Required(CONF_API_KEY): str, vol.Required( CONF_LATITUDE, default=self.hass.config.latitude ): cv.latitude, @@ -85,8 +92,8 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="geography", data_schema=self.geography_schema ) - geo_id = async_get_geography_id(user_input) - await self._async_set_unique_id(geo_id) + self._geo_id = async_get_geography_id(user_input) + await self._async_set_unique_id(self._geo_id) self._abort_if_unique_id_configured() # Find older config entries without unique ID: @@ -95,7 +102,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): continue if any( - geo_id == async_get_geography_id(geography) + self._geo_id == async_get_geography_id(geography) for geography in entry.data[CONF_GEOGRAPHIES] ): return self.async_abort(reason="already_configured") @@ -123,10 +130,19 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): checked_keys.add(user_input[CONF_API_KEY]) - return self.async_create_entry( - title=f"Cloud API ({geo_id})", - data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY}, - ) + return await self.async_step_geography_finish(user_input) + + async def async_step_geography_finish(self, user_input=None): + """Handle the finalization of a Cloud API config entry.""" + existing_entry = await self.async_set_unique_id(self._geo_id) + if existing_entry: + self.hass.config_entries.async_update_entry(existing_entry, data=user_input) + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry( + title=f"Cloud API ({self._geo_id})", + data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY}, + ) async def async_step_import(self, import_config): """Import a config entry from configuration.yaml.""" @@ -164,6 +180,30 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO}, ) + async def async_step_reauth(self, data): + """Handle configuration by re-auth.""" + self._latitude = data[CONF_LATITUDE] + self._longitude = data[CONF_LONGITUDE] + + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Handle re-auth completion.""" + if not user_input: + return self.async_show_form( + step_id="reauth_confirm", data_schema=self.api_key_data_schema + ) + + conf = { + CONF_API_KEY: user_input[CONF_API_KEY], + CONF_LATITUDE: self._latitude, + CONF_LONGITUDE: self._longitude, + } + + self._geo_id = async_get_geography_id(conf) + + return await self.async_step_geography_finish(conf) + async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" if not user_input: diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index 8912b0287d7..d365720ad26 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -31,7 +31,6 @@ async def test_duplicate_error(hass): CONF_LATITUDE: 51.528308, CONF_LONGITUDE: -0.3817765, } - node_pro_conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "12345"} MockConfigEntry( domain=DOMAIN, unique_id="51.528308, -0.3817765", data=geography_conf @@ -44,6 +43,8 @@ async def test_duplicate_error(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + node_pro_conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "12345"} + MockConfigEntry( domain=DOMAIN, unique_id="192.168.1.100", data=node_pro_conf ).add_to_hass(hass) @@ -78,24 +79,6 @@ async def test_invalid_identifier(hass): assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} -async def test_node_pro_error(hass): - """Test that an invalid Node/Pro ID shows an error.""" - node_pro_conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "my_password"} - - with patch( - "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"} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=node_pro_conf - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_IP_ADDRESS: "unable_to_connect"} - - async def test_migration(hass): """Test migrating from version 1 to the current version.""" conf = { @@ -142,6 +125,24 @@ async def test_migration(hass): } +async def test_node_pro_error(hass): + """Test that an invalid Node/Pro ID shows an error.""" + node_pro_conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "my_password"} + + with patch( + "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"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=node_pro_conf + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_IP_ADDRESS: "unable_to_connect"} + + async def test_options_flow(hass): """Test config flow options.""" geography_conf = { @@ -198,28 +199,6 @@ async def test_step_geography(hass): } -async def test_step_node_pro(hass): - """Test the Node/Pro step.""" - conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "my_password"} - - with patch( - "homeassistant.components.airvisual.async_setup_entry", return_value=True - ), patch("pyairvisual.node.Node.from_samba"): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={"type": "AirVisual Node/Pro"} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=conf - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "Node/Pro (192.168.1.100)" - assert result["data"] == { - CONF_IP_ADDRESS: "192.168.1.100", - CONF_PASSWORD: "my_password", - CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO, - } - - async def test_step_import(hass): """Test the import step for both types of configuration.""" geography_conf = { @@ -245,6 +224,61 @@ async def test_step_import(hass): } +async def test_step_node_pro(hass): + """Test the Node/Pro step.""" + conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "my_password"} + + with patch( + "homeassistant.components.airvisual.async_setup_entry", return_value=True + ), patch("pyairvisual.node.Node.from_samba"): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={"type": "AirVisual Node/Pro"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=conf + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Node/Pro (192.168.1.100)" + assert result["data"] == { + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PASSWORD: "my_password", + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO, + } + + +async def test_step_reauth(hass): + """Test that the reauth step works.""" + geography_conf = { + CONF_API_KEY: "abcde12345", + CONF_LATITUDE: 51.528308, + CONF_LONGITUDE: -0.3817765, + } + + MockConfigEntry( + domain=DOMAIN, unique_id="51.528308, -0.3817765", data=geography_conf + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=geography_conf + ) + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ), patch("pyairvisual.api.API.nearest_city"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_API_KEY: "defgh67890"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + + assert len(hass.config_entries.async_entries()) == 1 + + async def test_step_user(hass): """Test the user ("pick the integration type") step.""" result = await hass.config_entries.flow.async_init( From 1a126111170e3837fcba4b4f8fee14778cba3262 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 10 Sep 2020 00:49:54 +0200 Subject: [PATCH 042/514] Upgrade sentry-sdk to 0.17.4 (#39868) --- 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 471a349a4df..1a9bb74a8ee 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.3"], + "requirements": ["sentry-sdk==0.17.4"], "codeowners": ["@dcramer", "@frenck"] } diff --git a/requirements_all.txt b/requirements_all.txt index 315ed0b7c12..8f0732156e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1964,7 +1964,7 @@ sense-hat==2.2.0 sense_energy==0.8.0 # homeassistant.components.sentry -sentry-sdk==0.17.3 +sentry-sdk==0.17.4 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6cfb65411e6..0e73f611cda 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -912,7 +912,7 @@ samsungtvws[websocket]==1.4.0 sense_energy==0.8.0 # homeassistant.components.sentry -sentry-sdk==0.17.3 +sentry-sdk==0.17.4 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From 1f75f61bb0a89934086778f965e8e8666194123f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Oldag?= Date: Thu, 10 Sep 2020 00:52:27 +0200 Subject: [PATCH 043/514] Bump pyTibber to 0.15.2 (#39870) --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 36f4002949b..b3decdd250a 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -2,7 +2,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.14.0"], + "requirements": ["pyTibber==0.15.2"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index 8f0732156e0..8b895a7f930 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1202,7 +1202,7 @@ pyRFXtrx==0.25 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.14.0 +pyTibber==0.15.2 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0e73f611cda..239b6207d92 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -583,7 +583,7 @@ pyMetno==0.8.1 pyRFXtrx==0.25 # homeassistant.components.tibber -pyTibber==0.14.0 +pyTibber==0.15.2 # homeassistant.components.nextbus py_nextbusnext==0.1.4 From 6e79d49c805084c4173eb16b8b1579dafacd5762 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 10 Sep 2020 00:04:20 +0000 Subject: [PATCH 044/514] [ci skip] Translation update --- .../cover/translations/zh-Hant.json | 4 +-- .../components/dsmr/translations/nb.json | 7 +++++ .../homematicip_cloud/translations/nb.json | 7 +++++ .../humidifier/translations/zh-Hant.json | 4 +-- .../light/translations/zh-Hant.json | 4 +-- .../openweathermap/translations/nb.json | 27 +++++++++++++++++++ .../components/remote/translations/nb.json | 15 +++++++++++ .../components/remote/translations/no.json | 15 +++++++++++ .../remote/translations/zh-Hant.json | 15 +++++++++++ .../components/sharkiq/translations/nb.json | 16 +++++++++++ .../components/shelly/translations/nb.json | 12 +++++++++ .../switch/translations/zh-Hant.json | 4 +-- .../components/yeelight/translations/nb.json | 11 ++++++++ 13 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/dsmr/translations/nb.json create mode 100644 homeassistant/components/homematicip_cloud/translations/nb.json create mode 100644 homeassistant/components/openweathermap/translations/nb.json create mode 100644 homeassistant/components/sharkiq/translations/nb.json create mode 100644 homeassistant/components/shelly/translations/nb.json create mode 100644 homeassistant/components/yeelight/translations/nb.json diff --git a/homeassistant/components/cover/translations/zh-Hant.json b/homeassistant/components/cover/translations/zh-Hant.json index a8752b13f00..d23d4778809 100644 --- a/homeassistant/components/cover/translations/zh-Hant.json +++ b/homeassistant/components/cover/translations/zh-Hant.json @@ -10,9 +10,9 @@ "stop": "\u505c\u6b62 {entity_name}" }, "condition_type": { - "is_closed": "{entity_name}\u5df2\u95dc\u9589", + "is_closed": "{entity_name}\u70ba\u95dc\u9589", "is_closing": "{entity_name}\u6b63\u5728\u95dc\u9589", - "is_open": "{entity_name}\u5df2\u958b\u555f", + "is_open": "{entity_name}\u70ba\u958b\u555f", "is_opening": "{entity_name}\u6b63\u5728\u958b\u555f", "is_position": "\u76ee\u524d{entity_name}\u4f4d\u7f6e\u70ba", "is_tilt_position": "\u76ee\u524d{entity_name}\u6a19\u984c\u4f4d\u7f6e\u70ba" diff --git a/homeassistant/components/dsmr/translations/nb.json b/homeassistant/components/dsmr/translations/nb.json new file mode 100644 index 00000000000..6ba5a1f3978 --- /dev/null +++ b/homeassistant/components/dsmr/translations/nb.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/nb.json b/homeassistant/components/homematicip_cloud/translations/nb.json new file mode 100644 index 00000000000..b9c78635b91 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/translations/nb.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_pin": "Ugyldig PIN-kode, pr\u00f8v p\u00e5 nytt." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/zh-Hant.json b/homeassistant/components/humidifier/translations/zh-Hant.json index 3e37ba38f64..0534d45d705 100644 --- a/homeassistant/components/humidifier/translations/zh-Hant.json +++ b/homeassistant/components/humidifier/translations/zh-Hant.json @@ -9,8 +9,8 @@ }, "condition_type": { "is_mode": "{entity_name}\u8a2d\u5b9a\u70ba\u6307\u5b9a\u6a21\u5f0f", - "is_off": "{entity_name}\u5df2\u95dc\u9589", - "is_on": "{entity_name}\u5df2\u958b\u555f" + "is_off": "{entity_name}\u70ba\u95dc\u9589", + "is_on": "{entity_name}j\u70ba\u958b\u555f" }, "trigger_type": { "target_humidity_changed": "{entity_name}\u8a2d\u5b9a\u6fd5\u5ea6\u5df2\u8b8a\u66f4", diff --git a/homeassistant/components/light/translations/zh-Hant.json b/homeassistant/components/light/translations/zh-Hant.json index 0872a742f6f..65c57839464 100644 --- a/homeassistant/components/light/translations/zh-Hant.json +++ b/homeassistant/components/light/translations/zh-Hant.json @@ -9,8 +9,8 @@ "turn_on": "\u958b\u555f{entity_name}" }, "condition_type": { - "is_off": "{entity_name}\u5df2\u95dc\u9589", - "is_on": "{entity_name}\u5df2\u958b\u555f" + "is_off": "{entity_name}\u70ba\u95dc\u9589", + "is_on": "{entity_name}j\u70ba\u958b\u555f" }, "trigger_type": { "turned_off": "{entity_name}\u5df2\u95dc\u9589", diff --git a/homeassistant/components/openweathermap/translations/nb.json b/homeassistant/components/openweathermap/translations/nb.json new file mode 100644 index 00000000000..f62cc08ac81 --- /dev/null +++ b/homeassistant/components/openweathermap/translations/nb.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "OpenWeatherMap-integrering for disse koordinatene er allerede konfigurert." + }, + "error": { + "auth": "API-n\u00f8kkelen er ikke riktig." + }, + "step": { + "user": { + "data": { + "mode": "Modus" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Spr\u00e5k", + "mode": "Modus" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/remote/translations/nb.json b/homeassistant/components/remote/translations/nb.json index 2e65d515e59..f8bff2524cd 100644 --- a/homeassistant/components/remote/translations/nb.json +++ b/homeassistant/components/remote/translations/nb.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "Aktiv\u00e9r/deaktiv\u00e9r {entity_name}", + "turn_off": "Sl\u00e5 av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} er av", + "is_on": "{entity_name} er p\u00e5" + }, + "trigger_type": { + "turned_off": "{entity_name} sl\u00e5tt av", + "turned_on": "{entity_name} sl\u00e5tt p\u00e5" + } + }, "state": { "_": { "off": "Av", diff --git a/homeassistant/components/remote/translations/no.json b/homeassistant/components/remote/translations/no.json index 2e65d515e59..5120acfcb61 100644 --- a/homeassistant/components/remote/translations/no.json +++ b/homeassistant/components/remote/translations/no.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "Veksle {entity_name}", + "turn_off": "Sl\u00e5 av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} er av", + "is_on": "{entity_name} er p\u00e5" + }, + "trigger_type": { + "turned_off": "{entity_name} sl\u00e5tt av", + "turned_on": "{entity_name} sl\u00e5tt p\u00e5" + } + }, "state": { "_": { "off": "Av", diff --git a/homeassistant/components/remote/translations/zh-Hant.json b/homeassistant/components/remote/translations/zh-Hant.json index b387a57723d..2c6c3240dbd 100644 --- a/homeassistant/components/remote/translations/zh-Hant.json +++ b/homeassistant/components/remote/translations/zh-Hant.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "\u5207\u63db{entity_name}", + "turn_off": "\u95dc\u9589{entity_name}", + "turn_on": "\u958b\u555f{entity_name}" + }, + "condition_type": { + "is_off": "{entity_name}\u70ba\u95dc\u9589", + "is_on": "{entity_name}\u70ba\u958b\u555f" + }, + "trigger_type": { + "turned_off": "{entity_name}\u5df2\u95dc\u9589", + "turned_on": "{entity_name}\u5df2\u958b\u555f" + } + }, "state": { "_": { "off": "\u95dc\u9589", diff --git a/homeassistant/components/sharkiq/translations/nb.json b/homeassistant/components/sharkiq/translations/nb.json new file mode 100644 index 00000000000..c7b6400d476 --- /dev/null +++ b/homeassistant/components/sharkiq/translations/nb.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uforventet feil" + }, + "step": { + "reauth": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/nb.json b/homeassistant/components/shelly/translations/nb.json new file mode 100644 index 00000000000..ef07be6f70d --- /dev/null +++ b/homeassistant/components/shelly/translations/nb.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "credentials": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/translations/zh-Hant.json b/homeassistant/components/switch/translations/zh-Hant.json index 6ce2ab3d29a..e286f5f138a 100644 --- a/homeassistant/components/switch/translations/zh-Hant.json +++ b/homeassistant/components/switch/translations/zh-Hant.json @@ -6,8 +6,8 @@ "turn_on": "\u958b\u555f{entity_name}" }, "condition_type": { - "is_off": "{entity_name}\u5df2\u95dc\u9589", - "is_on": "{entity_name}\u5df2\u958b\u555f" + "is_off": "{entity_name}\u70ba\u95dc\u9589", + "is_on": "{entity_name}j\u70ba\u958b\u555f" }, "trigger_type": { "turned_off": "{entity_name}\u5df2\u95dc\u9589", diff --git a/homeassistant/components/yeelight/translations/nb.json b/homeassistant/components/yeelight/translations/nb.json new file mode 100644 index 00000000000..dd84563c39c --- /dev/null +++ b/homeassistant/components/yeelight/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Vert" + } + } + } + } +} \ No newline at end of file From b70df4ab18f2f1279925cb62b247baecfe961ce3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 10 Sep 2020 07:58:40 +0200 Subject: [PATCH 045/514] 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 b57f97b954c25d4611c04646ba2f0ad01b879fe6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 10 Sep 2020 08:34:15 +0200 Subject: [PATCH 046/514] Upgrade isort to 5.5.2 (#39879) --- .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 91add974b8d..e24b6095b4a 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.1 + rev: 5.5.2 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 2daad6e33f0..06b8430a740 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.1 +isort==5.5.2 pydocstyle==5.1.1 pyupgrade==2.7.2 yamllint==1.24.2 From a6a011ec196d7213521bd2d4a839b200d749a225 Mon Sep 17 00:00:00 2001 From: Marius <33354141+Mariusthvdb@users.noreply.github.com> Date: Thu, 10 Sep 2020 09:21:51 +0200 Subject: [PATCH 047/514] Use entity_class 'safety' in synology_dsm storage sensors (#39757) * Use entity_class: 'safety' in storage sensors and more meaningful icons then 'mdi:test-tube', not sure if these are even necessary, given the device_class that defines the icons too? * Set device_class, not icon Still, the temperature sensors have both set, should I take these out in this go too? While we're at it.... * added device_class temperature to temp sensors and removed explicit icons do we need to set a D_c also on the Status sensor? line 187 * revert device_class Safety which is now set in const.py following up on https://github.com/home-assistant/core/pull/39757#pullrequestreview-483705147 * Use DEVICE_CLASS_SAFETY from const + revert temp to avoid conflict * const from binary * reverted non binary status sensors to use: "mdi:checkbox-marked-circle-outline" and set None to device_class Co-authored-by: Quentame --- .../components/synology_dsm/binary_sensor.py | 12 +----------- homeassistant/components/synology_dsm/const.py | 13 +++++++------ 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index c95b4298f5d..a75f57db678 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -1,10 +1,7 @@ """Support for Synology DSM binary sensors.""" from typing import Dict -from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_SAFETY, - BinarySensorEntity, -) +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DISKS from homeassistant.helpers.typing import HomeAssistantType @@ -17,8 +14,6 @@ from .const import ( SYNO_API, ) -DEFAULT_DEVICE_CLASS = DEVICE_CLASS_SAFETY - async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities @@ -76,8 +71,3 @@ 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 diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 163561d13a0..2816ae681a1 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -4,6 +4,7 @@ from synology_dsm.api.core.security import SynoCoreSecurity from synology_dsm.api.core.utilization import SynoCoreUtilization from synology_dsm.api.storage.storage import SynoStorage +from homeassistant.components.binary_sensor import DEVICE_CLASS_SAFETY from homeassistant.const import ( DATA_MEGABYTES, DATA_RATE_KILOBYTES_PER_SECOND, @@ -41,15 +42,15 @@ STORAGE_DISK_BINARY_SENSORS = { f"{SynoStorage.API_KEY}:disk_exceed_bad_sector_thr": { ENTITY_NAME: "Exceeded Max Bad Sectors", ENTITY_UNIT: None, - ENTITY_ICON: "mdi:test-tube", - ENTITY_CLASS: None, + ENTITY_ICON: None, + ENTITY_CLASS: DEVICE_CLASS_SAFETY, ENTITY_ENABLE: True, }, f"{SynoStorage.API_KEY}:disk_below_remain_life_thr": { ENTITY_NAME: "Below Min Remaining Life", ENTITY_UNIT: None, - ENTITY_ICON: "mdi:test-tube", - ENTITY_CLASS: None, + ENTITY_ICON: None, + ENTITY_CLASS: DEVICE_CLASS_SAFETY, ENTITY_ENABLE: True, }, } @@ -58,8 +59,8 @@ SECURITY_BINARY_SENSORS = { f"{SynoCoreSecurity.API_KEY}:status": { ENTITY_NAME: "Security status", ENTITY_UNIT: None, - ENTITY_ICON: "mdi:checkbox-marked-circle-outline", - ENTITY_CLASS: "safety", + ENTITY_ICON: None, + ENTITY_CLASS: DEVICE_CLASS_SAFETY, ENTITY_ENABLE: True, }, } From 8648d8d0123e7f2d99c32089da2ee1f257cbff7b 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 048/514] 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 8b895a7f930..a9560de44e5 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 239b6207d92..501b42e658d 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 c9f87afd8b6699337d3ff5d0f8e86f8f974029f0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Sep 2020 10:51:13 +0200 Subject: [PATCH 049/514] Optimize requirements check with stdlib (#39871) * Check requirements don't conflict stdlib * Use regex --- script/hassfest/__main__.py | 2 +- script/hassfest/requirements.py | 129 +++++++++++++++++++++++--------- 2 files changed, 94 insertions(+), 37 deletions(-) diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 26af118d11e..e3e4fbf38c6 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -114,7 +114,7 @@ def main(): try: start = monotonic() print(f"Validating {plugin.__name__.split('.')[-1]}...", end="", flush=True) - if plugin is requirements: + if plugin is requirements and not config.specific_integrations: print() plugin.validate(integrations, config) print(" done in {:.2f}s".format(monotonic() - start)) diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index ab43cd62bd5..c2173cc1d13 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -1,4 +1,6 @@ """Validate requirements.""" +from collections import deque +import json import operator import re import subprocess @@ -27,6 +29,7 @@ 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} +PIPDEPTREE_CACHE = None def normalize_package_name(requirement: str) -> str: @@ -43,8 +46,15 @@ def normalize_package_name(requirement: str) -> str: def validate(integrations: Dict[str, Integration], config: Config): """Handle requirements for integrations.""" + ensure_cache() + # check for incompatible requirements - for integration in tqdm(integrations.values()): + items = integrations.values() + + if not config.specific_integrations: + tqdm(items) + + for integration in items: if not integration.manifest: continue @@ -92,39 +102,68 @@ def validate_requirements(integration: Integration): ) +def ensure_cache(): + """Ensure we have a cache of pipdeptree. + + { + "flake8-docstring": { + "key": "flake8-docstrings", + "package_name": "flake8-docstrings", + "installed_version": "1.5.0" + "dependencies": {"flake8"} + } + } + """ + global PIPDEPTREE_CACHE + + if PIPDEPTREE_CACHE is not None: + return + + cache = {} + + for item in json.loads( + subprocess.run( + ["pipdeptree", "-w", "silence", "--json"], + check=True, + capture_output=True, + text=True, + ).stdout + ): + cache[item["package"]["key"]] = { + **item["package"], + "dependencies": {dep["key"] for dep in item["dependencies"]}, + } + + PIPDEPTREE_CACHE = cache + + def get_requirements(integration: Integration, packages: Set[str]) -> Set[str]: """Return all (recursively) requirements for an integration.""" + ensure_cache() + 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}" - ) + to_check = deque(packages) + + while to_check: + package = to_check.popleft() + + if package in all_requirements: 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) + all_requirements.add(package) - 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) + item = PIPDEPTREE_CACHE.get(package) + + if item is None: + # Only warn if direct dependencies could not be resolved + if package in packages: + integration.add_error( + "requirements", f"Failed to resolve requirements for {package}" + ) + continue + + to_check.extend(item["dependencies"]) return all_requirements @@ -134,15 +173,11 @@ def install_requirements(integration: Integration, requirements: Set[str]) -> bo Return True if successful. """ + global PIPDEPTREE_CACHE + + ensure_cache() + 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: @@ -155,17 +190,39 @@ def install_requirements(integration: Integration, requirements: Set[str]) -> bo install_args = match.group(1) requirement_arg = match.group(2) + is_installed = False + + normalized = normalize_package_name(requirement_arg) + + if normalized and "==" in requirement_arg: + ver = requirement_arg.split("==")[-1] + item = PIPDEPTREE_CACHE.get(normalized) + is_installed = item and item["installed_version"] == ver + + if not is_installed: + try: + is_installed = pkg_util.is_installed(req) + except ValueError: + is_installed = False + + if is_installed: + continue + args = [sys.executable, "-m", "pip", "install", "--quiet"] if install_args: args.append(install_args) args.append(requirement_arg) try: - subprocess.run(args, check=True) + result = subprocess.run(args, check=True, capture_output=True, text=True) except subprocess.SubprocessError: integration.add_error( "requirements", f"Requirement {req} failed to install", ) + else: + # Clear the pipdeptree cache if something got installed + if "Successfully installed" in result.stdout: + PIPDEPTREE_CACHE = None if integration.errors: return False From 7b3369b71dec6274d7e53ba49a9fdb7ae26778b0 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 10 Sep 2020 11:18:43 +0200 Subject: [PATCH 050/514] 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 a9560de44e5..dd2c485bf4d 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 501b42e658d..b1a5fe7ed77 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 bb661ad083eecf9a46f12d4a7b9248a2ead5c617 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Sep 2020 11:25:56 +0200 Subject: [PATCH 051/514] 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 b49e6243d13a783b4a68fbc4fc09cc5cf2cdf56d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 10 Sep 2020 12:06:18 +0200 Subject: [PATCH 052/514] 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 35a9106a4b80c3d4e348f44a589a3ad238c709a8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Sep 2020 12:08:17 +0200 Subject: [PATCH 053/514] 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 14f7f5ba4574fbf81cf82e9a32fd37e926485ddc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 10 Sep 2020 13:16:33 +0200 Subject: [PATCH 054/514] Remove stale debug from WLED tests (#39882) --- tests/components/wled/test_sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index f4efea1b57d..39cde51dbfd 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -183,7 +183,6 @@ async def test_disabled_by_default_sensors( """Test the disabled by default WLED sensors.""" await init_integration(hass, aioclient_mock) registry = await hass.helpers.entity_registry.async_get_registry() - print(registry.entities) state = hass.states.get(entity_id) assert state is None From 162c39258e68ae42fe4e1560ae91ed54f5662409 Mon Sep 17 00:00:00 2001 From: RogerSelwyn Date: Thu, 10 Sep 2020 12:25:14 +0100 Subject: [PATCH 055/514] Add Carbon Monoxide binary sensor to Homekit Controller (#39889) --- .../homekit_controller/binary_sensor.py | 20 +++++++++++++++ .../components/homekit_controller/const.py | 1 + .../homekit_controller/test_binary_sensor.py | 25 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 939c6055e10..29fddf99189 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -4,6 +4,7 @@ import logging from aiohomekit.model.characteristics import CharacteristicsTypes from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, DEVICE_CLASS_OCCUPANCY, @@ -72,6 +73,24 @@ class HomeKitSmokeSensor(HomeKitEntity, BinarySensorEntity): return self.service.value(CharacteristicsTypes.SMOKE_DETECTED) == 1 +class HomeKitCarbonMonoxideSensor(HomeKitEntity, BinarySensorEntity): + """Representation of a Homekit BO sensor.""" + + @property + def device_class(self) -> str: + """Return the class of this sensor.""" + return DEVICE_CLASS_GAS + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + return [CharacteristicsTypes.CARBON_MONOXIDE_DETECTED] + + @property + def is_on(self): + """Return true if CO is currently detected.""" + return self.service.value(CharacteristicsTypes.CARBON_MONOXIDE_DETECTED) == 1 + + class HomeKitOccupancySensor(HomeKitEntity, BinarySensorEntity): """Representation of a Homekit occupancy sensor.""" @@ -112,6 +131,7 @@ ENTITY_TYPES = { "motion": HomeKitMotionSensor, "contact": HomeKitContactSensor, "smoke": HomeKitSmokeSensor, + "carbon-monoxide": HomeKitCarbonMonoxideSensor, "occupancy": HomeKitOccupancySensor, "leak": HomeKitLeakSensor, } diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 394750c0688..2a8a106a296 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -28,6 +28,7 @@ HOMEKIT_ACCESSORY_DISPATCH = { "temperature": "sensor", "battery": "sensor", "smoke": "binary_sensor", + "carbon-monoxide": "binary_sensor", "leak": "binary_sensor", "fan": "fan", "fanv2": "fan", diff --git a/tests/components/homekit_controller/test_binary_sensor.py b/tests/components/homekit_controller/test_binary_sensor.py index 460d14d0d48..e9ba4420176 100644 --- a/tests/components/homekit_controller/test_binary_sensor.py +++ b/tests/components/homekit_controller/test_binary_sensor.py @@ -3,6 +3,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, DEVICE_CLASS_OCCUPANCY, @@ -15,6 +16,7 @@ from tests.components.homekit_controller.common import setup_test_component MOTION_DETECTED = ("motion", "motion-detected") CONTACT_STATE = ("contact", "contact-state") SMOKE_DETECTED = ("smoke", "smoke-detected") +CARBON_MONOXIDE_DETECTED = ("carbon-monoxide", "carbon-monoxide.detected") OCCUPANCY_DETECTED = ("occupancy", "occupancy-detected") LEAK_DETECTED = ("leak", "leak-detected") @@ -88,6 +90,29 @@ async def test_smoke_sensor_read_state(hass, utcnow): assert state.attributes["device_class"] == DEVICE_CLASS_SMOKE +def create_carbon_monoxide_sensor_service(accessory): + """Define carbon monoxide sensor characteristics.""" + service = accessory.add_service(ServicesTypes.CARBON_MONOXIDE_SENSOR) + + cur_state = service.add_char(CharacteristicsTypes.CARBON_MONOXIDE_DETECTED) + cur_state.value = 0 + + +async def test_carbon_monoxide_sensor_read_state(hass, utcnow): + """Test that we can read the state of a HomeKit contact accessory.""" + helper = await setup_test_component(hass, create_carbon_monoxide_sensor_service) + + helper.characteristics[CARBON_MONOXIDE_DETECTED].value = 0 + state = await helper.poll_and_get_state() + assert state.state == "off" + + helper.characteristics[CARBON_MONOXIDE_DETECTED].value = 1 + state = await helper.poll_and_get_state() + assert state.state == "on" + + assert state.attributes["device_class"] == DEVICE_CLASS_GAS + + def create_occupancy_sensor_service(accessory): """Define occupancy characteristics.""" service = accessory.add_service(ServicesTypes.OCCUPANCY_SENSOR) From 0cce35b23edffd7beffe6b817fa7a357265bf0ca 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 056/514] 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 68e2824a285da9067df67a21da7d48e7f358fc33 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 10 Sep 2020 15:56:05 +0200 Subject: [PATCH 057/514] 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 40bcd38caa24547c06b5f823fc409ad556ab0c43 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 10 Sep 2020 16:58:15 +0200 Subject: [PATCH 058/514] Update azure-pipelines-wheels.yml --- 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 ac5f4fd824f..77d90ac94f1 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -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 b5005430be8449f5a6d4107f9a127c90e3dd93c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Sep 2020 11:10:43 -0500 Subject: [PATCH 059/514] 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 aa9dff572e315a35a3dbdc68543c7a532bfca22a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Sep 2020 20:41:42 +0200 Subject: [PATCH 060/514] 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 4411de047d5..81f2243bca3 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 fb31b04c08b3302b0a3cff92a18ba707309b40eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Sep 2020 13:43:45 -0500 Subject: [PATCH 061/514] Increase template test coverage. (#39908) --- tests/helpers/test_template.py | 39 ++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index c1f018b47d6..5d84eb0f4e6 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2321,3 +2321,42 @@ async def test_protected_blocked(hass): tmp = template.Template('{{ states.sensor.any.__getattr__("any") }}', hass) with pytest.raises(TemplateError): tmp.async_render() + + +async def test_demo_template(hass): + """Test the demo template works as expected.""" + hass.states.async_set("sun.sun", "above", {"elevation": 50, "next_rising": "later"}) + for i in range(2): + hass.states.async_set(f"sensor.sensor{i}", "on") + + demo_template_str = """ +{## Imitate available variables: ##} +{% set my_test_json = { + "temperature": 25, + "unit": "°C" +} %} + +The temperature is {{ my_test_json.temperature }} {{ my_test_json.unit }}. + +{% if is_state("sun.sun", "above_horizon") -%} + The sun rose {{ relative_time(states.sun.sun.last_changed) }} ago. +{%- else -%} + The sun will rise at {{ as_timestamp(strptime(state_attr("sun.sun", "next_rising"), "")) | timestamp_local }}. +{%- endif %} + +For loop example getting 3 entity values: + +{% for states in states | slice(3) -%} + {% set state = states | first %} + {%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%} + {{ state.name | lower }} is {{state.state_with_unit}} +{%- endfor %}. +""" + tmp = template.Template(demo_template_str, hass) + + result = tmp.async_render() + assert "The temperature is 25" in result + assert "is on" in result + assert "sensor0" in result + assert "sensor1" in result + assert "sun" in result From 047dc19351df6ae0e4edf0b2bf00fa14e5300fdf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Sep 2020 13:44:11 -0500 Subject: [PATCH 062/514] Add zeroconf discovery to homekit (#39907) Ensures HomeKit Bridge is offered for onboarding if homekit is detected on the network. --- homeassistant/components/homekit/manifest.json | 1 + homeassistant/generated/zeroconf.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 87ef0dc5ec8..486e9f1643c 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -20,5 +20,6 @@ "codeowners": [ "@bdraco" ], + "zeroconf": ["_homekit._tcp.local."], "config_flow": true } diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index ea61ccfbaeb..55ec2110c2e 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -37,6 +37,9 @@ ZEROCONF = { "_hap._tcp.local.": [ "homekit_controller" ], + "_homekit._tcp.local.": [ + "homekit" + ], "_http._tcp.local.": [ "shelly" ], From bedc1e56728afd79ccc9c66469efab0356100afe Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 10 Sep 2020 20:47:15 +0200 Subject: [PATCH 063/514] Upgrade numpy to 1.19.2 (#39912) --- 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 | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 1f862bb1bbf..5ab331df44e 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.1", "pyiqvia==0.2.1"], + "requirements": ["numpy==1.19.2", "pyiqvia==0.2.1"], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 1fb7096d5fa..24b84e305e7 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.1", "opencv-python-headless==4.3.0.36"], + "requirements": ["numpy==1.19.2", "opencv-python-headless==4.3.0.36"], "codeowners": [] } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 2f1c391094c..a8739a86d70 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.3.0", "tf-models-official==2.3.0", "pycocotools==2.0.1", - "numpy==1.19.1", + "numpy==1.19.2", "pillow==7.2.0" ], "codeowners": [] diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index a43c2bb0cce..88e32ce4a46 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.1"], + "requirements": ["numpy==1.19.2"], "codeowners": [], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index dd2c485bf4d..3284107841a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -995,7 +995,7 @@ numato-gpio==0.8.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.19.1 +numpy==1.19.2 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1a5fe7ed77..e4ca732ee69 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -472,7 +472,7 @@ numato-gpio==0.8.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.19.1 +numpy==1.19.2 # homeassistant.components.google oauth2client==4.0.0 From fd8a4182d980d5cdcbc23d39049d76fd6a7b4b6e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Sep 2020 13:50:11 -0500 Subject: [PATCH 064/514] 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 4d6e694d1488634439420411867a6d717d41587b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Sep 2020 20:52:23 +0200 Subject: [PATCH 065/514] 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 b66aaeea996bf5c5043037306ab1c77a3ca83129 Mon Sep 17 00:00:00 2001 From: Xiaonan Shen Date: Fri, 11 Sep 2020 03:08:37 +0800 Subject: [PATCH 066/514] Add camera support to synology_dsm (#39838) * Add camera support to synology_dsm * Code improvements * More code improvements --- .coveragerc | 1 + .../components/synology_dsm/__init__.py | 20 ++++ .../components/synology_dsm/camera.py | 97 +++++++++++++++++++ .../components/synology_dsm/const.py | 2 +- 4 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/synology_dsm/camera.py diff --git a/.coveragerc b/.coveragerc index 162e0c65f06..3785240a387 100644 --- a/.coveragerc +++ b/.coveragerc @@ -829,6 +829,7 @@ omit = homeassistant/components/synology/camera.py homeassistant/components/synology_chat/notify.py homeassistant/components/synology_dsm/__init__.py + homeassistant/components/synology_dsm/camera.py homeassistant/components/synology_dsm/binary_sensor.py homeassistant/components/synology_dsm/sensor.py homeassistant/components/synology_srm/device_tracker.py diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 7e229ef4a5b..223235a4121 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -10,6 +10,7 @@ from synology_dsm.api.core.utilization import SynoCoreUtilization from synology_dsm.api.dsm.information import SynoDSMInformation from synology_dsm.api.dsm.network import SynoDSMNetwork from synology_dsm.api.storage.storage import SynoStorage +from synology_dsm.api.surveillance_station import SynoSurveillanceStation import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -225,12 +226,14 @@ class SynoApi: self.security: SynoCoreSecurity = None self.storage: SynoStorage = None self.utilisation: SynoCoreUtilization = None + self.surveillance_station: SynoSurveillanceStation = None # Should we fetch them self._fetching_entities = {} self._with_security = True self._with_storage = True self._with_utilisation = True + self._with_surveillance_station = True self._unsub_dispatcher = None @@ -250,6 +253,11 @@ class SynoApi: device_token=self._entry.data.get("device_token"), ) + await self._hass.async_add_executor_job(self.dsm.discover_apis) + self._with_surveillance_station = bool( + self.dsm.apis.get(SynoSurveillanceStation.CAMERA_API_KEY) + ) + self._async_setup_api_requests() await self._hass.async_add_executor_job(self._fetch_device_configuration) @@ -294,6 +302,9 @@ class SynoApi: self._with_utilisation = bool( self._fetching_entities.get(SynoCoreUtilization.API_KEY) ) + self._with_surveillance_station = bool( + self._fetching_entities.get(SynoSurveillanceStation.CAMERA_API_KEY) + ) # Reset not used API if not self._with_security: @@ -308,6 +319,10 @@ class SynoApi: self.dsm.reset(self.utilisation) self.utilisation = None + if not self._with_surveillance_station: + self.dsm.reset(self.surveillance_station) + self.surveillance_station = None + def _fetch_device_configuration(self): """Fetch initial device config.""" self.information = self.dsm.information @@ -324,6 +339,9 @@ class SynoApi: if self._with_utilisation: self.utilisation = self.dsm.utilisation + if self._with_surveillance_station: + self.surveillance_station = self.dsm.surveillance_station + async def async_unload(self): """Stop interacting with the NAS and prepare for removal from hass.""" self._unsub_dispatcher() @@ -345,6 +363,8 @@ class SynologyDSMEntity(Entity): entity_info: Dict[str, str], ): """Initialize the Synology DSM entity.""" + super().__init__() + self._api = api self._api_key = entity_type.split(":")[0] self.entity_type = entity_type.split(":")[-1] diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py new file mode 100644 index 00000000000..66137850a7c --- /dev/null +++ b/homeassistant/components/synology_dsm/camera.py @@ -0,0 +1,97 @@ +"""Support for Synology DSM cameras.""" +from typing import Dict + +from synology_dsm.api.surveillance_station import SynoSurveillanceStation + +from homeassistant.components.camera import SUPPORT_STREAM, Camera +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from . import SynologyDSMEntity +from .const import ( + DOMAIN, + ENTITY_CLASS, + ENTITY_ENABLE, + ENTITY_ICON, + ENTITY_NAME, + ENTITY_UNIT, + SYNO_API, +) + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up the Synology NAS binary sensor.""" + + api = hass.data[DOMAIN][entry.unique_id][SYNO_API] + + if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis: + return True + + surveillance_station = api.surveillance_station + await hass.async_add_executor_job(surveillance_station.update) + cameras = surveillance_station.get_all_cameras() + entities = [SynoDSMCamera(api, camera) for camera in cameras] + + async_add_entities(entities) + + +class SynoDSMCamera(SynologyDSMEntity, Camera): + """Representation a Synology camera.""" + + def __init__(self, api, camera): + """Initialize a Synology camera.""" + super().__init__( + api, + f"{SynoSurveillanceStation.CAMERA_API_KEY}:{camera.id}", + { + ENTITY_NAME: camera.name, + ENTITY_CLASS: None, + ENTITY_ICON: None, + ENTITY_ENABLE: True, + ENTITY_UNIT: None, + }, + ) + self._camera = camera + + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, self._api.information.serial, self._camera.id)}, + "name": self.name, + "model": self._camera.model, + "via_device": (DOMAIN, self._api.information.serial), + } + + @property + def supported_features(self) -> int: + """Return supported features of this camera.""" + return SUPPORT_STREAM + + @property + def is_recording(self): + """Return true if the device is recording.""" + return self._camera.is_recording + + @property + def motion_detection_enabled(self): + """Return the camera motion detection status.""" + return self._camera.is_motion_detection_enabled + + def camera_image(self) -> bytes: + """Return bytes of camera image.""" + return self._api.surveillance_station.get_camera_image(self._camera.id) + + async def stream_source(self) -> str: + """Return the source of the stream.""" + return self._camera.live_view.rtsp + + def enable_motion_detection(self): + """Enable motion detection in the camera.""" + self._api.surveillance_station.enable_motion_detection(self._camera.id) + + def disable_motion_detection(self): + """Disable motion detection in camera.""" + self._api.surveillance_station.disable_motion_detection(self._camera.id) diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 2816ae681a1..693d8b2cd50 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -13,7 +13,7 @@ from homeassistant.const import ( ) DOMAIN = "synology_dsm" -PLATFORMS = ["binary_sensor", "sensor"] +PLATFORMS = ["binary_sensor", "camera", "sensor"] # Entry keys SYNO_API = "syno_api" From 4ee5a29bc057e2bb38ff52f86f43bbc90434b366 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 067/514] 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 cfc020daa2da50d857c3997ae83ae6dee1800f89 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Sep 2020 23:07:23 +0200 Subject: [PATCH 068/514] 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 357f2c10fda..a71799db168 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": ["_http._tcp.local."], "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 3284107841a..a0d933d7f06 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 e4ca732ee69..7f2ab990bd7 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 bf1ad0a757f863d5ab3f85571250f38ab39ff0f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Sep 2020 16:19:11 -0500 Subject: [PATCH 069/514] 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 39d441a29b0e641ad8f64f81bf61b17338872c11 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 11 Sep 2020 00:09:17 +0000 Subject: [PATCH 070/514] [ci skip] Translation update --- homeassistant/components/almond/translations/ca.json | 3 ++- homeassistant/components/almond/translations/en.json | 3 ++- homeassistant/components/almond/translations/it.json | 3 ++- homeassistant/components/almond/translations/ru.json | 3 ++- homeassistant/components/dsmr/translations/it.json | 8 ++++++++ .../components/home_connect/translations/ca.json | 3 ++- .../components/home_connect/translations/en.json | 3 ++- .../components/home_connect/translations/it.json | 3 ++- .../components/home_connect/translations/ru.json | 3 ++- homeassistant/components/netatmo/translations/ca.json | 1 + homeassistant/components/netatmo/translations/en.json | 1 + homeassistant/components/netatmo/translations/it.json | 1 + homeassistant/components/netatmo/translations/ru.json | 1 + homeassistant/components/smappee/translations/ca.json | 3 ++- homeassistant/components/smappee/translations/en.json | 3 ++- homeassistant/components/smappee/translations/it.json | 3 ++- homeassistant/components/smappee/translations/ru.json | 3 ++- homeassistant/components/somfy/translations/ca.json | 3 ++- homeassistant/components/somfy/translations/en.json | 3 ++- homeassistant/components/somfy/translations/it.json | 3 ++- homeassistant/components/somfy/translations/ru.json | 3 ++- homeassistant/components/spotify/translations/ca.json | 1 + homeassistant/components/spotify/translations/en.json | 1 + homeassistant/components/spotify/translations/it.json | 1 + homeassistant/components/spotify/translations/ru.json | 1 + homeassistant/components/toon/translations/ca.json | 3 ++- homeassistant/components/toon/translations/en.json | 3 ++- homeassistant/components/toon/translations/it.json | 3 ++- homeassistant/components/toon/translations/ru.json | 3 ++- homeassistant/components/withings/translations/ca.json | 3 ++- homeassistant/components/withings/translations/en.json | 3 ++- homeassistant/components/withings/translations/it.json | 3 ++- homeassistant/components/withings/translations/ru.json | 3 ++- 33 files changed, 64 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/almond/translations/ca.json b/homeassistant/components/almond/translations/ca.json index 8747f1ed7df..81f114be1eb 100644 --- a/homeassistant/components/almond/translations/ca.json +++ b/homeassistant/components/almond/translations/ca.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte amb Almond.", "cannot_connect": "No es pot connectar amb el servidor d'Almond.", - "missing_configuration": "Consulta la documentaci\u00f3 sobre com configurar Almond." + "missing_configuration": "Consulta la documentaci\u00f3 sobre com configurar Almond.", + "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/almond/translations/en.json b/homeassistant/components/almond/translations/en.json index 2a587d46403..01608d56faf 100644 --- a/homeassistant/components/almond/translations/en.json +++ b/homeassistant/components/almond/translations/en.json @@ -3,7 +3,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": "No URL available. For information about this error, [check the help section]({docs_url})" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/almond/translations/it.json b/homeassistant/components/almond/translations/it.json index 3e68336bf3e..aa371144630 100644 --- a/homeassistant/components/almond/translations/it.json +++ b/homeassistant/components/almond/translations/it.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "\u00c8 possibile configurare un solo account Almond.", "cannot_connect": "Impossibile connettersi al server Almond.", - "missing_configuration": "Si prega di controllare la documentazione su come impostare Almond." + "missing_configuration": "Si prega di controllare la documentazione su come impostare Almond.", + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/almond/translations/ru.json b/homeassistant/components/almond/translations/ru.json index a4fa0f5d46c..3039ecc2d41 100644 --- a/homeassistant/components/almond/translations/ru.json +++ b/homeassistant/components/almond/translations/ru.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Almond.", - "missing_configuration": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 Almond." + "missing_configuration": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 Almond.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435." }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/dsmr/translations/it.json b/homeassistant/components/dsmr/translations/it.json index 43906c0d6c8..b295fb60747 100644 --- a/homeassistant/components/dsmr/translations/it.json +++ b/homeassistant/components/dsmr/translations/it.json @@ -2,6 +2,14 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "one": "uno", + "other": "altri" + }, + "step": { + "one": "uno", + "other": "altri" } } } \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/ca.json b/homeassistant/components/home_connect/translations/ca.json index be6054f9bda..6553ce7e24d 100644 --- a/homeassistant/components/home_connect/translations/ca.json +++ b/homeassistant/components/home_connect/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "missing_configuration": "El component Home Connect no est\u00e0 configurat. Mira'n la documentaci\u00f3." + "missing_configuration": "El component Home Connect no est\u00e0 configurat. Mira'n la documentaci\u00f3.", + "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})" }, "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Home Connect." diff --git a/homeassistant/components/home_connect/translations/en.json b/homeassistant/components/home_connect/translations/en.json index 78310536205..24190814216 100644 --- a/homeassistant/components/home_connect/translations/en.json +++ b/homeassistant/components/home_connect/translations/en.json @@ -1,7 +1,8 @@ { "config": { "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": "No URL available. For information about this error, [check the help section]({docs_url})" }, "create_entry": { "default": "Successfully authenticated with Home Connect." diff --git a/homeassistant/components/home_connect/translations/it.json b/homeassistant/components/home_connect/translations/it.json index 3899d3b749f..98aa955b020 100644 --- a/homeassistant/components/home_connect/translations/it.json +++ b/homeassistant/components/home_connect/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "missing_configuration": "Il componente Home Connect non \u00e8 configurato. Si prega di seguire la documentazione." + "missing_configuration": "Il componente Home Connect non \u00e8 configurato. Si prega di seguire la documentazione.", + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})" }, "create_entry": { "default": "Autenticazione riuscita con Home Connect." diff --git a/homeassistant/components/home_connect/translations/ru.json b/homeassistant/components/home_connect/translations/ru.json index 2ef6f7d6697..b354d91c20c 100644 --- a/homeassistant/components/home_connect/translations/ru.json +++ b/homeassistant/components/home_connect/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "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.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435." }, "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/ca.json b/homeassistant/components/netatmo/translations/ca.json index 99104c168cf..f1c91932c7e 100644 --- a/homeassistant/components/netatmo/translations/ca.json +++ b/homeassistant/components/netatmo/translations/ca.json @@ -3,6 +3,7 @@ "abort": { "authorize_url_timeout": "[%key::common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "[%key::common::config_flow::abort::oauth2_missing_configuration%]", + "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "create_entry": { diff --git a/homeassistant/components/netatmo/translations/en.json b/homeassistant/components/netatmo/translations/en.json index 04ac8e69f11..e31d801b7a0 100644 --- a/homeassistant/components/netatmo/translations/en.json +++ b/homeassistant/components/netatmo/translations/en.json @@ -3,6 +3,7 @@ "abort": { "authorize_url_timeout": "Timeout generating authorize URL.", "missing_configuration": "The component is not configured. Please follow the documentation.", + "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "create_entry": { diff --git a/homeassistant/components/netatmo/translations/it.json b/homeassistant/components/netatmo/translations/it.json index a9e54b31bd7..1b98b7d01bc 100644 --- a/homeassistant/components/netatmo/translations/it.json +++ b/homeassistant/components/netatmo/translations/it.json @@ -3,6 +3,7 @@ "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.", + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "create_entry": { diff --git a/homeassistant/components/netatmo/translations/ru.json b/homeassistant/components/netatmo/translations/ru.json index 69a0430a4ba..c9be7e60825 100644 --- a/homeassistant/components/netatmo/translations/ru.json +++ b/homeassistant/components/netatmo/translations/ru.json @@ -3,6 +3,7 @@ "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.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", "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": { diff --git a/homeassistant/components/smappee/translations/ca.json b/homeassistant/components/smappee/translations/ca.json index 54a781c1f4a..df15bdf3ed4 100644 --- a/homeassistant/components/smappee/translations/ca.json +++ b/homeassistant/components/smappee/translations/ca.json @@ -6,7 +6,8 @@ "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." + "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", + "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})" }, "flow_title": "Smappee: {name}", "step": { diff --git a/homeassistant/components/smappee/translations/en.json b/homeassistant/components/smappee/translations/en.json index 57f1498d5fa..a6cd10d0806 100644 --- a/homeassistant/components/smappee/translations/en.json +++ b/homeassistant/components/smappee/translations/en.json @@ -6,7 +6,8 @@ "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." + "missing_configuration": "The component is not configured. Please follow the documentation.", + "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})" }, "flow_title": "Smappee: {name}", "step": { diff --git a/homeassistant/components/smappee/translations/it.json b/homeassistant/components/smappee/translations/it.json index 66994517c2f..ad85b94abae 100644 --- a/homeassistant/components/smappee/translations/it.json +++ b/homeassistant/components/smappee/translations/it.json @@ -6,7 +6,8 @@ "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." + "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})" }, "flow_title": "Smappee: {name}", "step": { diff --git a/homeassistant/components/smappee/translations/ru.json b/homeassistant/components/smappee/translations/ru.json index b3650a483f6..37434488e3d 100644 --- a/homeassistant/components/smappee/translations/ru.json +++ b/homeassistant/components/smappee/translations/ru.json @@ -6,7 +6,8 @@ "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." + "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_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435." }, "flow_title": "Smappee: {name}", "step": { diff --git a/homeassistant/components/somfy/translations/ca.json b/homeassistant/components/somfy/translations/ca.json index d3e2f7b16aa..489ab7a7f9f 100644 --- a/homeassistant/components/somfy/translations/ca.json +++ b/homeassistant/components/somfy/translations/ca.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte amb Somfy.", "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", - "missing_configuration": "El component Somfy no est\u00e0 configurat. Mira'n la documentaci\u00f3." + "missing_configuration": "El component Somfy no est\u00e0 configurat. Mira'n la documentaci\u00f3.", + "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})" }, "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Somfy." diff --git a/homeassistant/components/somfy/translations/en.json b/homeassistant/components/somfy/translations/en.json index da7890f959b..2a2bb689860 100644 --- a/homeassistant/components/somfy/translations/en.json +++ b/homeassistant/components/somfy/translations/en.json @@ -3,7 +3,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": "No URL available. For information about this error, [check the help section]({docs_url})" }, "create_entry": { "default": "Successfully authenticated with Somfy." diff --git a/homeassistant/components/somfy/translations/it.json b/homeassistant/components/somfy/translations/it.json index 7e1a15cbde8..001739a7a99 100644 --- a/homeassistant/components/somfy/translations/it.json +++ b/homeassistant/components/somfy/translations/it.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "\u00c8 possibile configurare un solo account Somfy.", "authorize_url_timeout": "Tempo scaduto nel generare l'url di autorizzazione", - "missing_configuration": "Il componente Somfy non \u00e8 configurato. Si prega di seguire la documentazione." + "missing_configuration": "Il componente Somfy non \u00e8 configurato. Si prega di seguire la documentazione.", + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})" }, "create_entry": { "default": "Autenticato con successo con Somfy." diff --git a/homeassistant/components/somfy/translations/ru.json b/homeassistant/components/somfy/translations/ru.json index 85292205b28..46c1e080480 100644 --- a/homeassistant/components/somfy/translations/ru.json +++ b/homeassistant/components/somfy/translations/ru.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Somfy \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438." + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Somfy \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435." }, "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/spotify/translations/ca.json b/homeassistant/components/spotify/translations/ca.json index 005e0c5c331..db47a10ed24 100644 --- a/homeassistant/components/spotify/translations/ca.json +++ b/homeassistant/components/spotify/translations/ca.json @@ -4,6 +4,7 @@ "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.", + "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", "reauth_account_mismatch": "El compte Spotify autenticat, no coincideix amb el compte necessari per a la re-autenticaci\u00f3." }, "create_entry": { diff --git a/homeassistant/components/spotify/translations/en.json b/homeassistant/components/spotify/translations/en.json index 6cdafbf061c..c2f1b5544ae 100644 --- a/homeassistant/components/spotify/translations/en.json +++ b/homeassistant/components/spotify/translations/en.json @@ -4,6 +4,7 @@ "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.", + "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", "reauth_account_mismatch": "The Spotify account authenticated with, does not match the account needed re-authentication." }, "create_entry": { diff --git a/homeassistant/components/spotify/translations/it.json b/homeassistant/components/spotify/translations/it.json index bb494f27860..648f071b8fe 100644 --- a/homeassistant/components/spotify/translations/it.json +++ b/homeassistant/components/spotify/translations/it.json @@ -4,6 +4,7 @@ "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.", + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", "reauth_account_mismatch": "L'account Spotify con cui si \u00e8 autenticati non corrisponde all'account necessario per la ri-autenticazione." }, "create_entry": { diff --git a/homeassistant/components/spotify/translations/ru.json b/homeassistant/components/spotify/translations/ru.json index c496fa389d9..82190aa57bb 100644 --- a/homeassistant/components/spotify/translations/ru.json +++ b/homeassistant/components/spotify/translations/ru.json @@ -4,6 +4,7 @@ "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.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", "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": { diff --git a/homeassistant/components/toon/translations/ca.json b/homeassistant/components/toon/translations/ca.json index cec5d4ef851..89c8c242516 100644 --- a/homeassistant/components/toon/translations/ca.json +++ b/homeassistant/components/toon/translations/ca.json @@ -5,7 +5,8 @@ "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.", - "no_agreements": "Aquest compte no t\u00e9 pantalles Toon." + "no_agreements": "Aquest compte no t\u00e9 pantalles Toon.", + "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})" }, "step": { "agreement": { diff --git a/homeassistant/components/toon/translations/en.json b/homeassistant/components/toon/translations/en.json index b15caa77aba..eda1dcb1ee3 100644 --- a/homeassistant/components/toon/translations/en.json +++ b/homeassistant/components/toon/translations/en.json @@ -5,7 +5,8 @@ "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize URL.", "missing_configuration": "The component is not configured. Please follow the documentation.", - "no_agreements": "This account has no Toon displays." + "no_agreements": "This account has no Toon displays.", + "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})" }, "step": { "agreement": { diff --git a/homeassistant/components/toon/translations/it.json b/homeassistant/components/toon/translations/it.json index ed905ff899f..c7cfb228388 100644 --- a/homeassistant/components/toon/translations/it.json +++ b/homeassistant/components/toon/translations/it.json @@ -5,7 +5,8 @@ "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", - "no_agreements": "Questo account non ha display Toon." + "no_agreements": "Questo account non ha display Toon.", + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})" }, "step": { "agreement": { diff --git a/homeassistant/components/toon/translations/ru.json b/homeassistant/components/toon/translations/ru.json index b5ae66ee54e..4a601de3c28 100644 --- a/homeassistant/components/toon/translations/ru.json +++ b/homeassistant/components/toon/translations/ru.json @@ -5,7 +5,8 @@ "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.", "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_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_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435." }, "step": { "agreement": { diff --git a/homeassistant/components/withings/translations/ca.json b/homeassistant/components/withings/translations/ca.json index 88d3ae7e6e6..1731a7bdaaf 100644 --- a/homeassistant/components/withings/translations/ca.json +++ b/homeassistant/components/withings/translations/ca.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Configuraci\u00f3 de perfil actualitzada.", "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 Withings no est\u00e0 configurada. Mira'n la documentaci\u00f3." + "missing_configuration": "La integraci\u00f3 Withings no est\u00e0 configurada. Mira'n la documentaci\u00f3.", + "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})" }, "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Withings." diff --git a/homeassistant/components/withings/translations/en.json b/homeassistant/components/withings/translations/en.json index 185bd56153c..53f8a78f3a9 100644 --- a/homeassistant/components/withings/translations/en.json +++ b/homeassistant/components/withings/translations/en.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Configuration updated for profile.", "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The Withings integration is not configured. Please follow the documentation." + "missing_configuration": "The Withings integration is not configured. Please follow the documentation.", + "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})" }, "create_entry": { "default": "Successfully authenticated with Withings." diff --git a/homeassistant/components/withings/translations/it.json b/homeassistant/components/withings/translations/it.json index 7302797c37c..90824fd0445 100644 --- a/homeassistant/components/withings/translations/it.json +++ b/homeassistant/components/withings/translations/it.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Configurazione aggiornata per il profilo.", "authorize_url_timeout": "Timeout durante la generazione dell'URL di autorizzazione.", - "missing_configuration": "Il componente Withings non \u00e8 configurato. Si prega di seguire la documentazione." + "missing_configuration": "Il componente Withings non \u00e8 configurato. Si prega di seguire la documentazione.", + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})" }, "create_entry": { "default": "Autenticazione riuscita con Withings." diff --git a/homeassistant/components/withings/translations/ru.json b/homeassistant/components/withings/translations/ru.json index b26efaedb18..93fcce53d45 100644 --- a/homeassistant/components/withings/translations/ru.json +++ b/homeassistant/components/withings/translations/ru.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f.", "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 Withings \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 Withings \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.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435." }, "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." From 487a74ba5d27e0042f61598d615467503e4202c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Sep 2020 00:15:13 -0500 Subject: [PATCH 071/514] 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 a0d933d7f06..cc607a6cf4f 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 7f2ab990bd7..3ca516c0b3a 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 9389a7c9bec0a9fba8d630abdaaf8dfeb525a705 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Sep 2020 05:19:21 -0500 Subject: [PATCH 072/514] 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 | 117 +++++++++++++---- 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, 306 insertions(+), 56 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 a71799db168..38ccc9e0f74 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.1"], - "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 55ec2110c2e..1dfd797306f 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -7,75 +7,142 @@ 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" + } ], "_homekit._tcp.local.": [ - "homekit" + { + "domain": "homekit" + } ], "_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 5117a1684177e95697400ada52ecad7bcbc9c8bd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Sep 2020 12:24:16 +0200 Subject: [PATCH 073/514] 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 101b5b3b35bc4258e7441f314083822e1c4cd7a3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 11 Sep 2020 13:00:00 +0200 Subject: [PATCH 074/514] 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 e208aac834155091f0cbfce2b3a9c14a23c19324 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Sep 2020 06:03:31 -0500 Subject: [PATCH 075/514] Add async_track_state_removed_domain to allow tracking when a state is removed from a domain (#39859) when a state is removed from a domain --- homeassistant/helpers/event.py | 102 +++++++++++++++++++------- tests/helpers/test_event.py | 127 +++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 24 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index d9f1b8d9681..4f30d255aec 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -55,6 +55,9 @@ 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_STATE_REMOVED_DOMAIN_CALLBACKS = "track_state_removed_domain_callbacks" +TRACK_STATE_REMOVED_DOMAIN_LISTENER = "track_state_removed_domain_listener" + TRACK_ENTITY_REGISTRY_UPDATED_CALLBACKS = "track_entity_registry_updated_callbacks" TRACK_ENTITY_REGISTRY_UPDATED_LISTENER = "track_entity_registry_updated_listener" @@ -235,10 +238,7 @@ def async_track_state_change_event( EVENT_STATE_CHANGED, _async_state_change_dispatcher ) - if isinstance(entity_ids, str): - entity_ids = [entity_ids] - - entity_ids = [entity_id.lower() for entity_id in entity_ids] + entity_ids = _async_string_to_lower_list(entity_ids) for entity_id in entity_ids: entity_callbacks.setdefault(entity_id, []).append(action) @@ -315,10 +315,7 @@ def async_track_entity_registry_updated_event( EVENT_ENTITY_REGISTRY_UPDATED, _async_entity_registry_updated_dispatcher ) - if isinstance(entity_ids, str): - entity_ids = [entity_ids] - - entity_ids = [entity_id.lower() for entity_id in entity_ids] + entity_ids = _async_string_to_lower_list(entity_ids) for entity_id in entity_ids: entity_callbacks.setdefault(entity_id, []).append(action) @@ -337,6 +334,26 @@ def async_track_entity_registry_updated_event( return remove_listener +@callback +def _async_dispatch_domain_event( + hass: HomeAssistant, event: Event, callbacks: Dict[str, List] +) -> None: + domain = split_entity_id(event.data["entity_id"])[0] + + if domain not in callbacks and MATCH_ALL not in callbacks: + return + + listeners = callbacks.get(domain, []) + callbacks.get(MATCH_ALL, []) + + for action in listeners: + try: + hass.async_run_job(action, event) + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Error while processing event %s for domain %s", event, domain + ) + + @bind_hass def async_track_state_added_domain( hass: HomeAssistant, @@ -355,27 +372,13 @@ def async_track_state_added_domain( 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 - ) + _async_dispatch_domain_event(hass, event, domain_callbacks) 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] + domains = _async_string_to_lower_list(domains) for domain in domains: domain_callbacks.setdefault(domain, []).append(action) @@ -394,6 +397,57 @@ def async_track_state_added_domain( return remove_listener +@bind_hass +def async_track_state_removed_domain( + hass: HomeAssistant, + domains: Union[str, Iterable[str]], + action: Callable[[Event], Any], +) -> Callable[[], None]: + """Track state change events when an entity is removed from domains.""" + + domain_callbacks = hass.data.setdefault(TRACK_STATE_REMOVED_DOMAIN_CALLBACKS, {}) + + if TRACK_STATE_REMOVED_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("new_state") is not None: + return + + _async_dispatch_domain_event(hass, event, domain_callbacks) + + hass.data[TRACK_STATE_REMOVED_DOMAIN_LISTENER] = hass.bus.async_listen( + EVENT_STATE_CHANGED, _async_state_change_dispatcher + ) + + domains = _async_string_to_lower_list(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_REMOVED_DOMAIN_CALLBACKS, + TRACK_STATE_REMOVED_DOMAIN_LISTENER, + domains, + action, + ) + + return remove_listener + + +@callback +def _async_string_to_lower_list(instr: Union[str, Iterable[str]]) -> List[str]: + if isinstance(instr, str): + return [instr.lower()] + + return [mstr.lower() for mstr in instr] + + @callback @bind_hass def async_track_template( diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index cc06c0fd19c..fcb8655804e 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -23,6 +23,7 @@ from homeassistant.helpers.event import ( async_track_state_added_domain, async_track_state_change, async_track_state_change_event, + async_track_state_removed_domain, async_track_sunrise, async_track_sunset, async_track_template, @@ -429,6 +430,132 @@ async def test_async_track_state_added_domain(hass): unsub_throws() +async def test_async_track_state_removed_domain(hass): + """Test async_track_state_removed_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_removed_domain(hass, "light", single_run_callback) + unsub_multi = async_track_state_removed_domain( + hass, ["light", "switch"], multiple_run_callback + ) + unsub_throws = async_track_state_removed_domain( + hass, ["light", "switch"], callback_that_throws + ) + + # Adding state to state machine + hass.states.async_set("light.Bowl", "on") + hass.states.async_remove("light.Bowl") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 1 + assert single_entity_id_tracker[-1][1] is None + assert single_entity_id_tracker[-1][0] is not None + assert len(multiple_entity_id_tracker) == 1 + assert multiple_entity_id_tracker[-1][1] is None + assert multiple_entity_id_tracker[-1][0] is not None + + # Added and than removed (light) + hass.states.async_set("light.Bowl", "on") + hass.states.async_remove("light.Bowl") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 2 + assert len(multiple_entity_id_tracker) == 2 + + # Added and than removed (light) + hass.states.async_set("light.Bowl", "off") + hass.states.async_remove("light.Bowl") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 3 + assert len(multiple_entity_id_tracker) == 3 + + # Added and than removed (light) + hass.states.async_set("light.Bowl", "off", {"some_attr": 1}) + hass.states.async_remove("light.Bowl") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 4 + assert len(multiple_entity_id_tracker) == 4 + + # Added and than removed (switch) + hass.states.async_set("switch.kitchen", "on") + hass.states.async_remove("switch.kitchen") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 4 + assert len(multiple_entity_id_tracker) == 5 + + unsub_single() + # Ensure unsubing the listener works + hass.states.async_set("light.new", "off") + hass.states.async_remove("light.new") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 4 + assert len(multiple_entity_id_tracker) == 6 + + unsub_multi() + unsub_throws() + + +async def test_async_track_state_removed_domain_match_all(hass): + """Test async_track_state_removed_domain with a match_all.""" + single_entity_id_tracker = [] + match_all_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 match_all_run_callback(event): + old_state = event.data.get("old_state") + new_state = event.data.get("new_state") + + match_all_entity_id_tracker.append((old_state, new_state)) + + unsub_single = async_track_state_removed_domain(hass, "light", single_run_callback) + unsub_match_all = async_track_state_removed_domain( + hass, MATCH_ALL, match_all_run_callback + ) + hass.states.async_set("light.new", "off") + hass.states.async_remove("light.new") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 1 + assert len(match_all_entity_id_tracker) == 1 + + hass.states.async_set("switch.new", "off") + hass.states.async_remove("switch.new") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 1 + assert len(match_all_entity_id_tracker) == 2 + + unsub_match_all() + unsub_single() + hass.states.async_set("switch.new", "off") + hass.states.async_remove("switch.new") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 1 + assert len(match_all_entity_id_tracker) == 2 + + async def test_track_template(hass): """Test tracking template.""" specific_runs = [] From e96fed20c84d4de2e08b9feba3d5f71f1ef07530 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 11 Sep 2020 13:08:13 +0200 Subject: [PATCH 076/514] 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 8ea8969d80f712ad304d8512882a0b5a3a83886d Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Fri, 11 Sep 2020 13:09:31 +0200 Subject: [PATCH 077/514] 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 f59e727f162b73a0707f3a0bbc1cdb0cb84bad5a 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 078/514] 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 c2a9a39ee000e5689e3b8d2883b901089fa6f12f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 11 Sep 2020 14:02:17 +0200 Subject: [PATCH 079/514] 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 062ac5f27d311c4935ef371ad31d07e6fc7ff249 Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 11 Sep 2020 16:50:17 +0200 Subject: [PATCH 080/514] 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 9b29d09d45a4b4759d8f8ad70d37117bf561c5d4 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 081/514] 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 741487a1fc2b5f06b5be195748696fd0527067c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Sep 2020 13:18:40 -0500 Subject: [PATCH 082/514] 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 4f30d255aec..8e126c7c14c 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -581,6 +581,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 fcb8655804e..821285cfbe1 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -809,20 +809,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() @@ -830,11 +843,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() @@ -842,6 +861,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() @@ -850,26 +874,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): @@ -893,7 +942,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() @@ -901,6 +950,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] @@ -935,11 +985,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 @@ -978,10 +1039,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() @@ -991,11 +1053,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() @@ -1051,7 +1123,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( @@ -1066,6 +1138,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 988a467afd25abcd6ecde801fa897e32a4f5be22 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 11 Sep 2020 19:34:07 +0100 Subject: [PATCH 083/514] Device automation triggers for stateless HomeKit accessories (#39090) Co-authored-by: J. Nick Koston --- .../components/homekit_controller/__init__.py | 3 +- .../homekit_controller/connection.py | 7 + .../components/homekit_controller/const.py | 1 + .../homekit_controller/device_trigger.py | 268 ++++++++++++++++ .../homekit_controller/strings.json | 20 ++ .../homekit_controller/translations/en.json | 25 +- .../specific_devices/test_aqara_switch.py | 52 +++ .../specific_devices/test_hue_bridge.py | 30 ++ .../specific_devices/test_lg_tv.py | 6 +- .../homekit_controller/test_device_trigger.py | 298 ++++++++++++++++++ .../homekit_controller/aqara_switch.json | 209 ++++++++++++ 11 files changed, 915 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/homekit_controller/device_trigger.py create mode 100644 tests/components/homekit_controller/specific_devices/test_aqara_switch.py create mode 100644 tests/components/homekit_controller/test_device_trigger.py create mode 100644 tests/fixtures/homekit_controller/aqara_switch.json diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index beddd915fc0..6a9fb126c21 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -17,7 +17,7 @@ from homeassistant.helpers.entity import Entity from .config_flow import normalize_hkid from .connection import HKDevice -from .const import CONTROLLER, DOMAIN, ENTITY_MAP, KNOWN_DEVICES +from .const import CONTROLLER, DOMAIN, ENTITY_MAP, KNOWN_DEVICES, TRIGGERS from .storage import EntityMapStorage _LOGGER = logging.getLogger(__name__) @@ -200,6 +200,7 @@ async def async_setup(hass, config): zeroconf_instance = await zeroconf.async_get_instance(hass) hass.data[CONTROLLER] = aiohomekit.Controller(zeroconf_instance=zeroconf_instance) hass.data[KNOWN_DEVICES] = {} + hass.data[TRIGGERS] = {} return True diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 6a59f98f3dc..ed2aaaa4656 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -16,6 +16,7 @@ from homeassistant.core import callback from homeassistant.helpers.event import async_track_time_interval from .const import CONTROLLER, DOMAIN, ENTITY_MAP, HOMEKIT_ACCESSORY_DISPATCH +from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry DEFAULT_SCAN_INTERVAL = datetime.timedelta(seconds=60) RETRY_INTERVAL = 60 # seconds @@ -237,6 +238,9 @@ class HKDevice: await self.async_create_devices() + # Load any triggers for this config entry + await async_setup_triggers_for_entry(self.hass, self.config_entry) + self.add_entities() if self.watchable_characteristics: @@ -377,6 +381,9 @@ class HKDevice: """Process events from accessory into HA state.""" self.available = True + # Process any stateless events (via device_triggers) + async_fire_triggers(self, new_values_dict) + for (aid, cid), value in new_values_dict.items(): accessory = self.current_state.setdefault(aid, {}) accessory[cid] = value diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 2a8a106a296..b1e32417137 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -4,6 +4,7 @@ DOMAIN = "homekit_controller" KNOWN_DEVICES = f"{DOMAIN}-devices" CONTROLLER = f"{DOMAIN}-controller" ENTITY_MAP = f"{DOMAIN}-entity-map" +TRIGGERS = f"{DOMAIN}-triggers" HOMEKIT_DIR = ".homekit" PAIRING_FILE = "pairing.json" diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py new file mode 100644 index 00000000000..76b82eec597 --- /dev/null +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -0,0 +1,268 @@ +"""Provides device automations for homekit devices.""" +from typing import List + +from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.characteristics.const import InputEventValues +from aiohomekit.model.services import ServicesTypes +from aiohomekit.utils import clamp_enum_to_char +import voluptuous as vol + +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.typing import ConfigType + +from .const import DOMAIN, KNOWN_DEVICES, TRIGGERS + +TRIGGER_TYPES = { + "button1", + "button2", + "button3", + "button4", + "button5", + "button6", + "button7", + "button8", + "button9", + "button10", +} +TRIGGER_SUBTYPES = {"single_press", "double_press", "long_press"} + +CONF_IID = "iid" +CONF_SUBTYPE = "subtype" + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + vol.Required(CONF_SUBTYPE): vol.In(TRIGGER_SUBTYPES), + } +) + +HK_TO_HA_INPUT_EVENT_VALUES = { + InputEventValues.SINGLE_PRESS: "single_press", + InputEventValues.DOUBLE_PRESS: "double_press", + InputEventValues.LONG_PRESS: "long_press", +} + + +class TriggerSource: + """Represents a stateless source of event data from HomeKit.""" + + def __init__(self, connection, aid, triggers): + """Initialize a set of triggers for a device.""" + self._hass = connection.hass + self._connection = connection + self._aid = aid + self._triggers = {} + for trigger in triggers: + self._triggers[(trigger["type"], trigger["subtype"])] = trigger + self._callbacks = {} + + def fire(self, iid, value): + """Process events that have been received from a HomeKit accessory.""" + for event_handler in self._callbacks.get(iid, []): + event_handler(value) + + def async_get_triggers(self): + """List device triggers for homekit devices.""" + yield from self._triggers + + async def async_attach_trigger( + self, + config: TRIGGER_SCHEMA, + action: AutomationActionType, + automation_info: dict, + ) -> CALLBACK_TYPE: + """Attach a trigger.""" + + def event_handler(char): + if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]: + return + self._hass.async_create_task(action({"trigger": config})) + + trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]] + iid = trigger["characteristic"] + + self._connection.add_watchable_characteristics([(self._aid, iid)]) + self._callbacks.setdefault(iid, []).append(event_handler) + + def async_remove_handler(): + if iid in self._callbacks: + self._callbacks[iid].remove(event_handler) + + return async_remove_handler + + +def enumerate_stateless_switch(service): + """Enumerate a stateless switch, like a single button.""" + + # A stateless switch that has a SERVICE_LABEL_INDEX is part of a group + # And is handled separately + if service.has(CharacteristicsTypes.SERVICE_LABEL_INDEX): + if len(service.linked) > 0: + return [] + + char = service[CharacteristicsTypes.INPUT_EVENT] + + # HomeKit itself supports single, double and long presses. But the + # manufacturer might not - clamp options to what they say. + all_values = clamp_enum_to_char(InputEventValues, char) + + results = [] + for event_type in all_values: + results.append( + { + "characteristic": char.iid, + "value": event_type, + "type": "button1", + "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type], + } + ) + return results + + +def enumerate_stateless_switch_group(service): + """Enumerate a group of stateless switches, like a remote control.""" + switches = list( + service.accessory.services.filter( + service_type=ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH, + child_service=service, + order_by=[CharacteristicsTypes.SERVICE_LABEL_INDEX], + ) + ) + + results = [] + for idx, switch in enumerate(switches): + char = switch[CharacteristicsTypes.INPUT_EVENT] + + # HomeKit itself supports single, double and long presses. But the + # manufacturer might not - clamp options to what they say. + all_values = clamp_enum_to_char(InputEventValues, char) + + for event_type in all_values: + results.append( + { + "characteristic": char.iid, + "value": event_type, + "type": f"button{idx + 1}", + "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type], + } + ) + return results + + +def enumerate_doorbell(service): + """Enumerate doorbell buttons.""" + input_event = service[CharacteristicsTypes.INPUT_EVENT] + + # HomeKit itself supports single, double and long presses. But the + # manufacturer might not - clamp options to what they say. + all_values = clamp_enum_to_char(InputEventValues, input_event) + + results = [] + for event_type in all_values: + results.append( + { + "characteristic": input_event.iid, + "value": event_type, + "type": "doorbell", + "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type], + } + ) + return results + + +TRIGGER_FINDERS = { + "service-label": enumerate_stateless_switch_group, + "stateless-programmable-switch": enumerate_stateless_switch, + "doorbell": enumerate_doorbell, +} + + +async def async_setup_triggers_for_entry(hass: HomeAssistant, config_entry): + """Triggers aren't entities as they have no state, but we still need to set them up for a config entry.""" + hkid = config_entry.data["AccessoryPairingID"] + conn = hass.data[KNOWN_DEVICES][hkid] + + @callback + def async_add_service(aid, service_dict): + service_type = service_dict["stype"] + + # If not a known service type then we can't handle any stateless events for it + if service_type not in TRIGGER_FINDERS: + return False + + # We can't have multiple trigger sources for the same device id + # Can't have a doorbell and a remote control in the same accessory + # They have to be different accessories (they can be on the same bridge) + # In practice, this is inline with what iOS actually supports AFAWCT. + device_id = conn.devices[aid] + if device_id in hass.data[TRIGGERS]: + return False + + # At the moment add_listener calls us with the raw service dict, rather than + # a service model. So turn it into a service ourselves. + accessory = conn.entity_map.aid(aid) + service = accessory.services.iid(service_dict["iid"]) + + # Just because we recognise the service type doesn't mean we can actually + # extract any triggers - so only proceed if we can + triggers = TRIGGER_FINDERS[service_type](service) + if len(triggers) == 0: + return False + + trigger = TriggerSource(conn, aid, triggers) + hass.data[TRIGGERS][device_id] = trigger + + return True + + conn.add_listener(async_add_service) + + +def async_fire_triggers(conn, events): + """Process events generated by a HomeKit accessory into automation triggers.""" + for (aid, iid), ev in events.items(): + if aid in conn.devices: + device_id = conn.devices[aid] + if device_id in conn.hass.data[TRIGGERS]: + source = conn.hass.data[TRIGGERS][device_id] + source.fire(iid, ev) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for homekit devices.""" + + if device_id not in hass.data.get(TRIGGERS, {}): + return [] + + device = hass.data[TRIGGERS][device_id] + + triggers = [] + + for trigger, subtype in device.async_get_triggers(): + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: trigger, + CONF_SUBTYPE: subtype, + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + + device_id = config[CONF_DEVICE_ID] + device = hass.data[TRIGGERS][device_id] + return await device.async_attach_trigger(config, action, automation_info) diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index bc07b71fa75..babfac05718 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -46,5 +46,25 @@ "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." } + }, + "device_automation": { + "trigger_type": { + "single_press": "\"{subtype}\" pressed", + "double_press": "\"{subtype}\" pressed twice", + "long_press": "\"{subtype}\" pressed and held" + }, + "trigger_subtype": { + "doorbell": "Doorbell", + "button1": "Button 1", + "button2": "Button 2", + "button3": "Button 3", + "button4": "Button 4", + "button5": "Button 5", + "button6": "Button 6", + "button7": "Button 7", + "button8": "Button 8", + "button9": "Button 9", + "button10": "Button 10" + } } } diff --git a/homeassistant/components/homekit_controller/translations/en.json b/homeassistant/components/homekit_controller/translations/en.json index 544c86391be..afa790dd222 100644 --- a/homeassistant/components/homekit_controller/translations/en.json +++ b/homeassistant/components/homekit_controller/translations/en.json @@ -53,5 +53,26 @@ } } }, - "title": "HomeKit Controller" -} \ No newline at end of file + "title": "HomeKit Controller", + "device_automation": { + "trigger_type": { + "single_press": "\"{subtype}\" pressed", + "double_press": "\"{subtype}\" pressed twice", + "long_press": "\"{subtype}\" pressed and held" + }, + "trigger_subtype": { + "doorbell": "Doorbell", + "button1": "Button 1", + "button2": "Button 2", + "button3": "Button 3", + "button4": "Button 4", + "button5": "Button 5", + "button6": "Button 6", + "button7": "Button 7", + "button8": "Button 8", + "button9": "Button 9", + "button10": "Button 10" + } + } +} + diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py new file mode 100644 index 00000000000..a9744fb7bfc --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py @@ -0,0 +1,52 @@ +""" +Regression tests for Aqara AR004. + +This device has a non-standard programmable stateless switch service that has a +service-label-index despite not being linked to a service-label. + +https://github.com/home-assistant/core/pull/39090 +""" + +from tests.common import assert_lists_same, async_get_device_automations +from tests.components.homekit_controller.common import ( + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_aqara_switch_setup(hass): + """Test that a Aqara Switch can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "aqara_switch.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + battery_id = "sensor.programmable_switch_battery" + battery = entity_registry.async_get(battery_id) + assert battery.unique_id == "homekit-111a1111a1a111-5" + + # The fixture file has 1 button and a battery + + expected = [ + { + "device_id": battery.device_id, + "domain": "sensor", + "entity_id": "sensor.programmable_switch_battery", + "platform": "device", + "type": "battery_level", + } + ] + + for subtype in ("single_press", "double_press", "long_press"): + expected.append( + { + "device_id": battery.device_id, + "domain": "homekit_controller", + "platform": "device", + "type": "button1", + "subtype": subtype, + } + ) + + triggers = await async_get_device_automations(hass, "trigger", battery.device_id) + assert_lists_same(triggers, expected) diff --git a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py index 0b6ebc00eba..67b7508eb94 100644 --- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py @@ -1,5 +1,6 @@ """Tests for handling accessories on a Hue bridge via HomeKit.""" +from tests.common import assert_lists_same, async_get_device_automations from tests.components.homekit_controller.common import ( Helper, setup_accessories_from_file, @@ -34,3 +35,32 @@ async def test_hue_bridge_setup(hass): assert device.name == "Hue dimmer switch" assert device.model == "RWL021" assert device.sw_version == "45.1.17846" + + # The fixture file has 1 dimmer, which is a remote with 4 buttons + # It (incorrectly) claims to support single, double and long press events + # It also has a battery + + expected = [ + { + "device_id": device.id, + "domain": "sensor", + "entity_id": "sensor.hue_dimmer_switch_battery", + "platform": "device", + "type": "battery_level", + } + ] + + for button in ("button1", "button2", "button3", "button4"): + for subtype in ("single_press", "double_press", "long_press"): + expected.append( + { + "device_id": device.id, + "domain": "homekit_controller", + "platform": "device", + "type": button, + "subtype": subtype, + } + ) + + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert_lists_same(triggers, expected) diff --git a/tests/components/homekit_controller/specific_devices/test_lg_tv.py b/tests/components/homekit_controller/specific_devices/test_lg_tv.py index acebac95006..cd3f57137bf 100644 --- a/tests/components/homekit_controller/specific_devices/test_lg_tv.py +++ b/tests/components/homekit_controller/specific_devices/test_lg_tv.py @@ -1,12 +1,12 @@ """Make sure that handling real world LG HomeKit characteristics isn't broken.""" - from homeassistant.components.media_player.const import ( SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_SELECT_SOURCE, ) +from tests.common import async_get_device_automations from tests.components.homekit_controller.common import ( Helper, setup_accessories_from_file, @@ -62,3 +62,7 @@ async def test_lg_tv(hass): assert device.model == "OLED55B9PUA" assert device.sw_version == "04.71.04" assert device.via_device_id is None + + # A TV doesn't have any triggers + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert triggers == [] diff --git a/tests/components/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py new file mode 100644 index 00000000000..c8ef2cbef38 --- /dev/null +++ b/tests/components/homekit_controller/test_device_trigger.py @@ -0,0 +1,298 @@ +"""Test homekit_controller stateless triggers.""" +from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes +import pytest + +import homeassistant.components.automation as automation +from homeassistant.components.homekit_controller.const import DOMAIN +from homeassistant.setup import async_setup_component + +from tests.common import ( + assert_lists_same, + async_get_device_automations, + async_mock_service, +) +from tests.components.homekit_controller.common import setup_test_component + + +# pylint: disable=redefined-outer-name +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +def create_remote(accessory): + """Define characteristics for a button (that is inn a group).""" + service_label = accessory.add_service(ServicesTypes.SERVICE_LABEL) + + char = service_label.add_char(CharacteristicsTypes.SERVICE_LABEL_NAMESPACE) + char.value = 1 + + for i in range(4): + button = accessory.add_service(ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH) + button.linked.append(service_label) + + char = button.add_char(CharacteristicsTypes.INPUT_EVENT) + char.value = 0 + char.perms = ["pw", "pr", "ev"] + + char = button.add_char(CharacteristicsTypes.NAME) + char.value = f"Button {i + 1}" + + char = button.add_char(CharacteristicsTypes.SERVICE_LABEL_INDEX) + char.value = i + + battery = accessory.add_service(ServicesTypes.BATTERY_SERVICE) + battery.add_char(CharacteristicsTypes.BATTERY_LEVEL) + + +def create_button(accessory): + """Define a button (that is not in a group).""" + button = accessory.add_service(ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH) + + char = button.add_char(CharacteristicsTypes.INPUT_EVENT) + char.value = 0 + char.perms = ["pw", "pr", "ev"] + + char = button.add_char(CharacteristicsTypes.NAME) + char.value = "Button 1" + + battery = accessory.add_service(ServicesTypes.BATTERY_SERVICE) + battery.add_char(CharacteristicsTypes.BATTERY_LEVEL) + + +def create_doorbell(accessory): + """Define a button (that is not in a group).""" + button = accessory.add_service(ServicesTypes.DOORBELL) + + char = button.add_char(CharacteristicsTypes.INPUT_EVENT) + char.value = 0 + char.perms = ["pw", "pr", "ev"] + + char = button.add_char(CharacteristicsTypes.NAME) + char.value = "Doorbell" + + battery = accessory.add_service(ServicesTypes.BATTERY_SERVICE) + battery.add_char(CharacteristicsTypes.BATTERY_LEVEL) + + +async def test_enumerate_remote(hass, utcnow): + """Test that remote is correctly enumerated.""" + await setup_test_component(hass, create_remote) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry = entity_registry.async_get("sensor.testdevice_battery") + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + expected = [ + { + "device_id": device.id, + "domain": "sensor", + "entity_id": "sensor.testdevice_battery", + "platform": "device", + "type": "battery_level", + } + ] + + for button in ("button1", "button2", "button3", "button4"): + for subtype in ("single_press", "double_press", "long_press"): + expected.append( + { + "device_id": device.id, + "domain": "homekit_controller", + "platform": "device", + "type": button, + "subtype": subtype, + } + ) + + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert_lists_same(triggers, expected) + + +async def test_enumerate_button(hass, utcnow): + """Test that a button is correctly enumerated.""" + await setup_test_component(hass, create_button) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry = entity_registry.async_get("sensor.testdevice_battery") + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + expected = [ + { + "device_id": device.id, + "domain": "sensor", + "entity_id": "sensor.testdevice_battery", + "platform": "device", + "type": "battery_level", + } + ] + + for subtype in ("single_press", "double_press", "long_press"): + expected.append( + { + "device_id": device.id, + "domain": "homekit_controller", + "platform": "device", + "type": "button1", + "subtype": subtype, + } + ) + + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert_lists_same(triggers, expected) + + +async def test_enumerate_doorbell(hass, utcnow): + """Test that a button is correctly enumerated.""" + await setup_test_component(hass, create_doorbell) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry = entity_registry.async_get("sensor.testdevice_battery") + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + expected = [ + { + "device_id": device.id, + "domain": "sensor", + "entity_id": "sensor.testdevice_battery", + "platform": "device", + "type": "battery_level", + } + ] + + for subtype in ("single_press", "double_press", "long_press"): + expected.append( + { + "device_id": device.id, + "domain": "homekit_controller", + "platform": "device", + "type": "doorbell", + "subtype": subtype, + } + ) + + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert_lists_same(triggers, expected) + + +async def test_handle_events(hass, utcnow, calls): + """Test that events are handled.""" + helper = await setup_test_component(hass, create_remote) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry = entity_registry.async_get("sensor.testdevice_battery") + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "alias": "single_press", + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "button1", + "subtype": "single_press", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "{{ trigger.platform}} - " + "{{ trigger.type }} - {{ trigger.subtype }}" + ) + }, + }, + }, + { + "alias": "long_press", + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "button2", + "subtype": "long_press", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "{{ trigger.platform}} - " + "{{ trigger.type }} - {{ trigger.subtype }}" + ) + }, + }, + }, + ] + }, + ) + + # Make sure first automation (only) fires for single press + helper.pairing.testing.update_named_service( + "Button 1", {CharacteristicsTypes.INPUT_EVENT: 0} + ) + + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "device - button1 - single_press" + + # Make sure automation doesn't trigger for long press + helper.pairing.testing.update_named_service( + "Button 1", {CharacteristicsTypes.INPUT_EVENT: 1} + ) + + await hass.async_block_till_done() + assert len(calls) == 1 + + # Make sure automation doesn't trigger for double press + helper.pairing.testing.update_named_service( + "Button 1", {CharacteristicsTypes.INPUT_EVENT: 2} + ) + + await hass.async_block_till_done() + assert len(calls) == 1 + + # Make sure second automation fires for long press + helper.pairing.testing.update_named_service( + "Button 2", {CharacteristicsTypes.INPUT_EVENT: 2} + ) + + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "device - button2 - long_press" + + # Turn the automations off + await hass.services.async_call( + "automation", + "turn_off", + {"entity_id": "automation.long_press"}, + blocking=True, + ) + + await hass.services.async_call( + "automation", + "turn_off", + {"entity_id": "automation.single_press"}, + blocking=True, + ) + + # Make sure event no longer fires + helper.pairing.testing.update_named_service( + "Button 2", {CharacteristicsTypes.INPUT_EVENT: 2} + ) + + await hass.async_block_till_done() + assert len(calls) == 2 diff --git a/tests/fixtures/homekit_controller/aqara_switch.json b/tests/fixtures/homekit_controller/aqara_switch.json new file mode 100644 index 00000000000..320478343f4 --- /dev/null +++ b/tests/fixtures/homekit_controller/aqara_switch.json @@ -0,0 +1,209 @@ +[ + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "bool", + "iid": 65537, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "ev": false, + "format": "string", + "iid": 65538, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Aqara" + }, + { + "ev": false, + "format": "string", + "iid": 65539, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "AR004" + }, + { + "ev": false, + "format": "string", + "iid": 65540, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Programmable Switch" + }, + { + "ev": false, + "format": "string", + "iid": 65541, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "111a1111a1a111" + }, + { + "ev": false, + "format": "string", + "iid": 65542, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "9" + }, + { + "ev": false, + "format": "string", + "iid": 65543, + "perms": [ + "pr" + ], + "type": "00000053-0000-1000-8000-0026BB765291", + "value": "1.0" + } + ], + "hidden": false, + "iid": 1, + "linked": [], + "primary": false, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "ev": false, + "format": "string", + "iid": 262146, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Programmable Switch" + }, + { + "ev": false, + "format": "uint8", + "iid": 262147, + "maxValue": 2, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "00000073-0000-1000-8000-0026BB765291", + "valid-values": [ + 0, + 1, + 2 + ], + "value": null + }, + { + "ev": false, + "format": "uint8", + "iid": 262148, + "maxValue": 255, + "minStep": 1, + "minValue": 1, + "perms": [ + "pr" + ], + "type": "000000CB-0000-1000-8000-0026BB765291", + "value": 1 + } + ], + "hidden": false, + "iid": 4, + "linked": [], + "primary": true, + "stype": "stateless-programmable-switch", + "type": "00000089-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "ev": false, + "format": "string", + "iid": 327682, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Battery Sensor" + }, + { + "ev": true, + "format": "uint8", + "iid": 327683, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "00000068-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "ev": true, + "format": "uint8", + "iid": 327685, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "00000079-0000-1000-8000-0026BB765291", + "valid-values": [ + 0, + 1 + ], + "value": 0 + }, + { + "ev": true, + "format": "uint8", + "iid": 327684, + "maxValue": 2, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "0000008F-0000-1000-8000-0026BB765291", + "valid-values": [ + 0, + 1, + 2 + ], + "value": 2 + } + ], + "hidden": false, + "iid": 5, + "linked": [], + "primary": false, + "stype": "battery", + "type": "00000096-0000-1000-8000-0026BB765291" + } + ] + } +] \ No newline at end of file From f1cb8e80b3a5ceea553036ff3fe94149cae3b6e5 Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Fri, 11 Sep 2020 19:47:48 +0100 Subject: [PATCH 084/514] 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 d186ff45cbb..d6bb7042c41 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 cc607a6cf4f..82bb470b4cd 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 50c573eb4d3b015ebe40e344b09111d83a0dceeb Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 11 Sep 2020 21:38:32 +0200 Subject: [PATCH 085/514] Activate hassfest requirements CI check (#39940) Co-authored-by: Franck Nijhof --- .github/workflows/ci.yaml | 23 +++++++++--------- .../homekit_controller/manifest.json | 16 +++++++++---- .../components/samsungtv/manifest.json | 6 +++-- requirements_all.txt | 6 ++--- requirements_test_all.txt | 4 ++-- script/gen_requirements_all.py | 2 ++ script/hassfest/requirements.py | 24 +++++++++++++++---- 7 files changed, 54 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fae621670ec..3d65df477e7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -518,25 +518,24 @@ jobs: hassfest: name: Check hassfest runs-on: ubuntu-latest - needs: prepare-base + needs: prepare-tests + strategy: + matrix: + python-version: [3.7] + container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub uses: actions/checkout@v2 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.2 - id: python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore base Python virtual environment + - name: + Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2 with: path: venv key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('requirements.txt') }}-${{ - hashFiles('requirements_test.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' @@ -546,7 +545,7 @@ jobs: - name: Run hassfest run: | . venv/bin/activate - python -m script.hassfest --action validate + python -m script.hassfest --requirements --action validate gen-requirements-all: name: Check all requirements diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 1ee2f16ffcf..06199e9f210 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.49" + ], + "zeroconf": [ + "_hap._tcp.local." + ], + "after_dependencies": [ + "zeroconf" + ], + "codeowners": [ + "@Jc2k" + ] } diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index efcb9064208..5584d2dd452 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -4,13 +4,15 @@ "documentation": "https://www.home-assistant.io/integrations/samsungtv", "requirements": [ "samsungctl[websocket]==0.7.1", - "samsungtvws[websocket]==1.4.0" + "samsungtvws==1.4.0" ], "ssdp": [ { "st": "urn:samsung.com:device:RemoteControlReceiver:1" } ], - "codeowners": ["@escoand"], + "codeowners": [ + "@escoand" + ], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 82bb470b4cd..a2256a6ea82 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2,7 +2,7 @@ -r requirements.txt # homeassistant.components.nuimo_controller ---only-binary=all nuimo==0.1.0 +# --only-binary=all nuimo==0.1.0 # homeassistant.components.dht # Adafruit-DHT==1.4.0 @@ -178,7 +178,7 @@ aioguardian==1.0.1 aioharmony==0.2.6 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.49 +aiohomekit==0.2.49 # homeassistant.components.emulated_hue # homeassistant.components.http @@ -1942,7 +1942,7 @@ saltbox==0.1.3 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws[websocket]==1.4.0 +samsungtvws==1.4.0 # homeassistant.components.satel_integra satel_integra==0.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ca516c0b3a..f4acc7ae79c 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.49 # homeassistant.components.emulated_hue # homeassistant.components.http @@ -905,7 +905,7 @@ rxv==0.6.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws[websocket]==1.4.0 +samsungtvws==1.4.0 # homeassistant.components.emulated_kasa # homeassistant.components.sense diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 27482a0c215..62b1a22cc73 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -23,11 +23,13 @@ COMMENT_REQUIREMENTS = ( "bme680", "credstash", "decora", + "decora_wifi", "env_canada", "envirophat", "evdev", "face_recognition", "i2csense", + "nuimo", "opencv-python-headless", "py_noaa", "pybluez", diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index c2173cc1d13..b51cbff7185 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -2,6 +2,7 @@ from collections import deque import json import operator +import os import re import subprocess import sys @@ -31,6 +32,19 @@ SUPPORTED_PYTHON_VERSIONS = [ STD_LIBS = {version: set(stdlib_list(version)) for version in SUPPORTED_PYTHON_VERSIONS} PIPDEPTREE_CACHE = None +IGNORE_VIOLATIONS = { + # Still has standard library requirements. + "acmeda", + "blink", + "ezviz", + "hdmi_cec", + "juicenet", + "lupusec", + "rainbird", + "slide", + "suez_water", +} + def normalize_package_name(requirement: str) -> str: """Return a normalized package name from a requirement string.""" @@ -49,12 +63,10 @@ def validate(integrations: Dict[str, Integration], config: Config): ensure_cache() # check for incompatible requirements - items = integrations.values() - if not config.specific_integrations: - tqdm(items) + disable_tqdm = config.specific_integrations or os.environ.get("CI", False) - for integration in items: + for integration in tqdm(integrations.values(), disable=disable_tqdm): if not integration.manifest: continue @@ -63,6 +75,10 @@ def validate(integrations: Dict[str, Integration], config: Config): def validate_requirements(integration: Integration): """Validate requirements.""" + # Some integrations have not been fixed yet so are allowed to have violations. + if integration.domain in IGNORE_VIOLATIONS: + return + integration_requirements = set() integration_packages = set() for req in integration.requirements: From 719aa0f317d3bc184150610f534f8a0022dd1416 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 11 Sep 2020 22:05:07 +0200 Subject: [PATCH 086/514] Use STATE_UNKNOWN constant in dlink and ecobee (#39948) --- homeassistant/components/dlink/switch.py | 5 +++-- homeassistant/components/ecobee/sensor.py | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index c173c879ad1..4fd21f200dd 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_USERNAME, + STATE_UNKNOWN, TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv @@ -145,14 +146,14 @@ class SmartPlugData: _LOGGER.warning("Waiting %s s to retry", retry_seconds) return - _state = "unknown" + _state = STATE_UNKNOWN try: self._last_tried = dt_util.now() _state = self.smartplug.state except urllib.error.HTTPError: _LOGGER.error("D-Link connection problem") - if _state == "unknown": + if _state == STATE_UNKNOWN: self._n_tried += 1 self.available = False _LOGGER.warning("Failed to connect to D-Link switch") diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index d9d6e74e3de..4351b230538 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -5,6 +5,7 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, + STATE_UNKNOWN, TEMP_FAHRENHEIT, ) from homeassistant.helpers.entity import Entity @@ -108,7 +109,11 @@ class EcobeeSensor(Entity): @property def state(self): """Return the state of the sensor.""" - if self._state in [ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN, "unknown"]: + if self._state in [ + ECOBEE_STATE_CALIBRATING, + ECOBEE_STATE_UNKNOWN, + STATE_UNKNOWN, + ]: return None if self.type == "temperature": From 9b49ca3820e0b47794afd0a8d68179cbfc8e968b Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Fri, 11 Sep 2020 22:07:31 +0200 Subject: [PATCH 087/514] Add template filter timedelta_seconds to create a timedelta from seconds (#39608) --- homeassistant/helpers/template.py | 3 +- tests/helpers/test_template.py | 59 +++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 917581fac07..a0512178fdc 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1,7 +1,7 @@ """Template helper methods for rendering strings with Home Assistant data.""" import base64 import collections.abc -from datetime import datetime +from datetime import datetime, timedelta from functools import wraps import json import logging @@ -1102,6 +1102,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["utcnow"] = dt_util.utcnow self.globals["as_timestamp"] = forgiving_as_timestamp self.globals["relative_time"] = relative_time + self.globals["timedelta"] = timedelta self.globals["strptime"] = strptime self.globals["urlencode"] = urlencode if hass is None: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 5d84eb0f4e6..81deb46f928 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -892,6 +892,65 @@ def test_relative_time(mock_is_safe, hass): ) +@patch( + "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable", + return_value=True, +) +def test_timedelta(mock_is_safe, hass): + """Test relative_time method.""" + now = datetime.strptime("2000-01-01 10:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") + with patch("homeassistant.util.dt.now", return_value=now): + assert ( + "0:02:00" + == template.Template( + "{{timedelta(seconds=120)}}", + hass, + ).async_render() + ) + assert ( + "1 day, 0:00:00" + == template.Template( + "{{timedelta(seconds=86400)}}", + hass, + ).async_render() + ) + assert ( + "1 day, 4:00:00" + == template.Template( + "{{timedelta(days=1, hours=4)}}", + hass, + ).async_render() + ) + assert ( + "1 hour" + == template.Template( + "{{relative_time(now() - timedelta(seconds=3600))}}", + hass, + ).async_render() + ) + assert ( + "1 day" + == template.Template( + "{{relative_time(now() - timedelta(seconds=86400))}}", + hass, + ).async_render() + ) + assert ( + "1 day" + == template.Template( + "{{relative_time(now() - timedelta(seconds=86401))}}", + hass, + ).async_render() + ) + assert ( + "15 days" + == template.Template( + "{{relative_time(now() - timedelta(weeks=2, days=1))}}", + hass, + ).async_render() + ) + + @patch( "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable", return_value=True, From 4e10895a1925eeb626a4f0b2949973ccceb7c9f8 Mon Sep 17 00:00:00 2001 From: Xiaonan Shen Date: Sat, 12 Sep 2020 04:14:22 +0800 Subject: [PATCH 088/514] Remove unchecked return value in synology_dsm (#39929) --- homeassistant/components/synology_dsm/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 66137850a7c..4cb34ee0431 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -27,7 +27,7 @@ async def async_setup_entry( api = hass.data[DOMAIN][entry.unique_id][SYNO_API] if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis: - return True + return surveillance_station = api.surveillance_station await hass.async_add_executor_job(surveillance_station.update) From ee5c1ea3f72d122bc169a8defc4d26e63f8e8e31 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 12 Sep 2020 00:05:00 +0000 Subject: [PATCH 089/514] [ci skip] Translation update --- .../accuweather/translations/uk.json | 1 - .../components/awair/translations/hu.json | 7 +++ .../azure_devops/translations/hu.json | 7 +++ .../components/bond/translations/hu.json | 7 +++ .../components/broadlink/translations/hu.json | 7 +++ .../components/control4/translations/hu.json | 7 +++ .../components/control4/translations/uk.json | 1 - .../components/denonavr/translations/hu.json | 7 +++ .../components/dexcom/translations/hu.json | 7 +++ .../components/dsmr/translations/hu.json | 7 +++ .../components/dunehd/translations/hu.json | 10 +++++ .../components/eafm/translations/hu.json | 7 +++ .../components/flo/translations/hu.json | 7 +++ .../components/hlk_sw16/translations/hu.json | 7 +++ .../homekit_controller/translations/ca.json | 20 +++++++++ .../homekit_controller/translations/en.json | 43 +++++++++---------- .../components/kodi/translations/hu.json | 7 +++ .../components/metoffice/translations/hu.json | 7 +++ .../nightscout/translations/hu.json | 7 +++ .../ovo_energy/translations/hu.json | 7 +++ .../plum_lightpad/translations/hu.json | 7 +++ .../components/poolsense/translations/hu.json | 7 +++ .../progettihwsw/translations/hu.json | 7 +++ .../components/rfxtrx/translations/de.json | 7 +++ .../components/rfxtrx/translations/hu.json | 7 +++ .../components/risco/translations/hu.json | 7 +++ .../components/roon/translations/hu.json | 7 +++ .../components/sharkiq/translations/hu.json | 7 +++ .../components/shelly/translations/hu.json | 7 +++ .../components/smappee/translations/hu.json | 7 +++ .../smart_meter_texas/translations/hu.json | 7 +++ .../components/sms/translations/hu.json | 7 +++ .../components/sonarr/translations/hu.json | 7 +++ .../squeezebox/translations/hu.json | 7 +++ .../components/syncthru/translations/hu.json | 7 +++ .../components/vizio/translations/hu.json | 1 + .../components/volumio/translations/hu.json | 7 +++ .../components/wilight/translations/hu.json | 7 +++ .../components/wolflink/translations/hu.json | 7 +++ .../xiaomi_aqara/translations/hu.json | 7 +++ .../components/yeelight/translations/hu.json | 7 +++ 41 files changed, 297 insertions(+), 24 deletions(-) create mode 100644 homeassistant/components/awair/translations/hu.json create mode 100644 homeassistant/components/azure_devops/translations/hu.json create mode 100644 homeassistant/components/bond/translations/hu.json create mode 100644 homeassistant/components/broadlink/translations/hu.json create mode 100644 homeassistant/components/control4/translations/hu.json create mode 100644 homeassistant/components/denonavr/translations/hu.json create mode 100644 homeassistant/components/dexcom/translations/hu.json create mode 100644 homeassistant/components/dsmr/translations/hu.json create mode 100644 homeassistant/components/dunehd/translations/hu.json create mode 100644 homeassistant/components/eafm/translations/hu.json create mode 100644 homeassistant/components/flo/translations/hu.json create mode 100644 homeassistant/components/hlk_sw16/translations/hu.json create mode 100644 homeassistant/components/kodi/translations/hu.json create mode 100644 homeassistant/components/metoffice/translations/hu.json create mode 100644 homeassistant/components/nightscout/translations/hu.json create mode 100644 homeassistant/components/ovo_energy/translations/hu.json create mode 100644 homeassistant/components/plum_lightpad/translations/hu.json create mode 100644 homeassistant/components/poolsense/translations/hu.json create mode 100644 homeassistant/components/progettihwsw/translations/hu.json create mode 100644 homeassistant/components/rfxtrx/translations/de.json create mode 100644 homeassistant/components/rfxtrx/translations/hu.json create mode 100644 homeassistant/components/risco/translations/hu.json create mode 100644 homeassistant/components/roon/translations/hu.json create mode 100644 homeassistant/components/sharkiq/translations/hu.json create mode 100644 homeassistant/components/shelly/translations/hu.json create mode 100644 homeassistant/components/smappee/translations/hu.json create mode 100644 homeassistant/components/smart_meter_texas/translations/hu.json create mode 100644 homeassistant/components/sms/translations/hu.json create mode 100644 homeassistant/components/sonarr/translations/hu.json create mode 100644 homeassistant/components/squeezebox/translations/hu.json create mode 100644 homeassistant/components/syncthru/translations/hu.json create mode 100644 homeassistant/components/volumio/translations/hu.json create mode 100644 homeassistant/components/wilight/translations/hu.json create mode 100644 homeassistant/components/wolflink/translations/hu.json create mode 100644 homeassistant/components/xiaomi_aqara/translations/hu.json create mode 100644 homeassistant/components/yeelight/translations/hu.json diff --git a/homeassistant/components/accuweather/translations/uk.json b/homeassistant/components/accuweather/translations/uk.json index a399c8f0694..8c3f282b350 100644 --- a/homeassistant/components/accuweather/translations/uk.json +++ b/homeassistant/components/accuweather/translations/uk.json @@ -6,7 +6,6 @@ "step": { "user": { "data": { - "api_key": "", "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457" diff --git a/homeassistant/components/awair/translations/hu.json b/homeassistant/components/awair/translations/hu.json new file mode 100644 index 00000000000..436e8b1fb7d --- /dev/null +++ b/homeassistant/components/awair/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_devops/translations/hu.json b/homeassistant/components/azure_devops/translations/hu.json new file mode 100644 index 00000000000..436e8b1fb7d --- /dev/null +++ b/homeassistant/components/azure_devops/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/hu.json b/homeassistant/components/bond/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/bond/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/hu.json b/homeassistant/components/broadlink/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/broadlink/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/control4/translations/hu.json b/homeassistant/components/control4/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/control4/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/control4/translations/uk.json b/homeassistant/components/control4/translations/uk.json index c771883714a..6c0426eba8f 100644 --- a/homeassistant/components/control4/translations/uk.json +++ b/homeassistant/components/control4/translations/uk.json @@ -3,7 +3,6 @@ "step": { "user": { "data": { - "password": "", "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" } } diff --git a/homeassistant/components/denonavr/translations/hu.json b/homeassistant/components/denonavr/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/denonavr/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/hu.json b/homeassistant/components/dexcom/translations/hu.json new file mode 100644 index 00000000000..9f2fd5d72f4 --- /dev/null +++ b/homeassistant/components/dexcom/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured_account": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/hu.json b/homeassistant/components/dsmr/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/dsmr/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dunehd/translations/hu.json b/homeassistant/components/dunehd/translations/hu.json new file mode 100644 index 00000000000..44b4442dc31 --- /dev/null +++ b/homeassistant/components/dunehd/translations/hu.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/hu.json b/homeassistant/components/eafm/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/eafm/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/hu.json b/homeassistant/components/flo/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/flo/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/hu.json b/homeassistant/components/hlk_sw16/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ 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 bf46d584917..e13c4e25fe6 100644 --- a/homeassistant/components/homekit_controller/translations/ca.json +++ b/homeassistant/components/homekit_controller/translations/ca.json @@ -53,5 +53,25 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button1": "Bot\u00f3 1", + "button10": "Bot\u00f3 10", + "button2": "Bot\u00f3 2", + "button3": "Bot\u00f3 3", + "button4": "Bot\u00f3 4", + "button5": "Bot\u00f3 5", + "button6": "Bot\u00f3 6", + "button7": "Bot\u00f3 7", + "button8": "Bot\u00f3 8", + "button9": "Bot\u00f3 9", + "doorbell": "Timbre" + }, + "trigger_type": { + "double_press": "\"{subtype}\" premut dues vegades", + "long_press": "\"{subtype}\" premut i mantingut", + "single_press": "\"{subtype}\" premut" + } + }, "title": "Controlador HomeKit" } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/en.json b/homeassistant/components/homekit_controller/translations/en.json index afa790dd222..eb0a5cbf6b6 100644 --- a/homeassistant/components/homekit_controller/translations/en.json +++ b/homeassistant/components/homekit_controller/translations/en.json @@ -53,26 +53,25 @@ } } }, - "title": "HomeKit Controller", "device_automation": { - "trigger_type": { - "single_press": "\"{subtype}\" pressed", - "double_press": "\"{subtype}\" pressed twice", - "long_press": "\"{subtype}\" pressed and held" - }, - "trigger_subtype": { - "doorbell": "Doorbell", - "button1": "Button 1", - "button2": "Button 2", - "button3": "Button 3", - "button4": "Button 4", - "button5": "Button 5", - "button6": "Button 6", - "button7": "Button 7", - "button8": "Button 8", - "button9": "Button 9", - "button10": "Button 10" - } - } -} - + "trigger_subtype": { + "button1": "Button 1", + "button10": "Button 10", + "button2": "Button 2", + "button3": "Button 3", + "button4": "Button 4", + "button5": "Button 5", + "button6": "Button 6", + "button7": "Button 7", + "button8": "Button 8", + "button9": "Button 9", + "doorbell": "Doorbell" + }, + "trigger_type": { + "double_press": "\"{subtype}\" pressed twice", + "long_press": "\"{subtype}\" pressed and held", + "single_press": "\"{subtype}\" pressed" + } + }, + "title": "HomeKit Controller" +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/hu.json b/homeassistant/components/kodi/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/kodi/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/metoffice/translations/hu.json b/homeassistant/components/metoffice/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/metoffice/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/hu.json b/homeassistant/components/nightscout/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/nightscout/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/hu.json b/homeassistant/components/ovo_energy/translations/hu.json new file mode 100644 index 00000000000..f5481afa94a --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/hu.json b/homeassistant/components/plum_lightpad/translations/hu.json new file mode 100644 index 00000000000..436e8b1fb7d --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/hu.json b/homeassistant/components/poolsense/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/poolsense/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/hu.json b/homeassistant/components/progettihwsw/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/de.json b/homeassistant/components/rfxtrx/translations/de.json new file mode 100644 index 00000000000..da1d200c2a2 --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/de.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/hu.json b/homeassistant/components/risco/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/risco/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/hu.json b/homeassistant/components/roon/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/roon/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/hu.json b/homeassistant/components/sharkiq/translations/hu.json new file mode 100644 index 00000000000..9f2fd5d72f4 --- /dev/null +++ b/homeassistant/components/sharkiq/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured_account": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/hu.json b/homeassistant/components/shelly/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/shelly/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/hu.json b/homeassistant/components/smappee/translations/hu.json new file mode 100644 index 00000000000..5bb10e0f851 --- /dev/null +++ b/homeassistant/components/smappee/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/hu.json b/homeassistant/components/smart_meter_texas/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sms/translations/hu.json b/homeassistant/components/sms/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/sms/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/hu.json b/homeassistant/components/sonarr/translations/hu.json new file mode 100644 index 00000000000..f5301e874ea --- /dev/null +++ b/homeassistant/components/sonarr/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/hu.json b/homeassistant/components/squeezebox/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/squeezebox/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/hu.json b/homeassistant/components/syncthru/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/syncthru/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/hu.json b/homeassistant/components/vizio/translations/hu.json index 469649275e1..9c0b31ca427 100644 --- a/homeassistant/components/vizio/translations/hu.json +++ b/homeassistant/components/vizio/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "updated_entry": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban defini\u00e1lt n\u00e9v, appok \u00e9s/vagy be\u00e1ll\u00edt\u00e1sok nem egyeznek meg a kor\u00e1bban import\u00e1lt konfigur\u00e1ci\u00f3val, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt." }, "error": { diff --git a/homeassistant/components/volumio/translations/hu.json b/homeassistant/components/volumio/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/volumio/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/hu.json b/homeassistant/components/wilight/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/wilight/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/hu.json b/homeassistant/components/wolflink/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/wolflink/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/hu.json b/homeassistant/components/xiaomi_aqara/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/hu.json b/homeassistant/components/yeelight/translations/hu.json new file mode 100644 index 00000000000..3b2d79a34a7 --- /dev/null +++ b/homeassistant/components/yeelight/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file From ac2e290d9701e764722e5b536ff96ea4de9ebeff Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 12 Sep 2020 02:37:33 +0200 Subject: [PATCH 090/514] Use sound, vibration and safety device class constants in various integrations (#39952) * Use sound, vibration and safety device class constants in various integrations * Fix wrong imports --- .../components/concord232/binary_sensor.py | 3 ++- .../components/ffmpeg_noise/binary_sensor.py | 4 ++-- .../components/meteoalarm/binary_sensor.py | 9 ++++++--- .../components/mysensors/binary_sensor.py | 11 +++++++---- homeassistant/components/nest/binary_sensor.py | 7 +++++-- .../components/smartthings/binary_sensor.py | 7 +++++-- homeassistant/components/wink/binary_sensor.py | 14 +++++++++----- 7 files changed, 36 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 3077056c397..806fb5c513d 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -7,6 +7,7 @@ import requests import voluptuous as vol from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_SAFETY, DEVICE_CLASSES, PLATFORM_SCHEMA, BinarySensorEntity, @@ -87,7 +88,7 @@ def get_opening_type(zone): if "MOTION" in zone["name"]: return "motion" if "KEY" in zone["name"]: - return "safety" + return DEVICE_CLASS_SAFETY if "SMOKE" in zone["name"]: return "smoke" if "WATER" in zone["name"]: diff --git a/homeassistant/components/ffmpeg_noise/binary_sensor.py b/homeassistant/components/ffmpeg_noise/binary_sensor.py index 6ada2bb2748..387f25afe6e 100644 --- a/homeassistant/components/ffmpeg_noise/binary_sensor.py +++ b/homeassistant/components/ffmpeg_noise/binary_sensor.py @@ -4,7 +4,7 @@ import logging import haffmpeg.sensor as ffmpeg_sensor import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import DEVICE_CLASS_SOUND, PLATFORM_SCHEMA from homeassistant.components.ffmpeg import ( CONF_EXTRA_ARGUMENTS, CONF_INITIAL_STATE, @@ -84,4 +84,4 @@ class FFmpegNoise(FFmpegBinarySensor): @property def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" - return "sound" + return DEVICE_CLASS_SOUND diff --git a/homeassistant/components/meteoalarm/binary_sensor.py b/homeassistant/components/meteoalarm/binary_sensor.py index b481b417b9e..6b13d03ebba 100644 --- a/homeassistant/components/meteoalarm/binary_sensor.py +++ b/homeassistant/components/meteoalarm/binary_sensor.py @@ -5,7 +5,11 @@ import logging from meteoalertapi import Meteoalert 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 import homeassistant.helpers.config_validation as cv @@ -17,7 +21,6 @@ CONF_COUNTRY = "country" CONF_LANGUAGE = "language" CONF_PROVINCE = "province" -DEFAULT_DEVICE_CLASS = "safety" DEFAULT_NAME = "meteoalarm" SCAN_INTERVAL = timedelta(minutes=30) @@ -78,7 +81,7 @@ class MeteoAlertBinarySensor(BinarySensorEntity): @property def device_class(self): """Return the device class of this binary sensor.""" - return DEFAULT_DEVICE_CLASS + return DEVICE_CLASS_SAFETY def update(self): """Update device state.""" diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py index 0bab6ea6eea..406f82c845e 100644 --- a/homeassistant/components/mysensors/binary_sensor.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -1,6 +1,9 @@ """Support for MySensors binary sensors.""" from homeassistant.components import mysensors from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SOUND, + DEVICE_CLASS_VIBRATION, DEVICE_CLASSES, DOMAIN, BinarySensorEntity, @@ -11,10 +14,10 @@ SENSORS = { "S_DOOR": "door", "S_MOTION": "motion", "S_SMOKE": "smoke", - "S_SPRINKLER": "safety", - "S_WATER_LEAK": "safety", - "S_SOUND": "sound", - "S_VIBRATION": "vibration", + "S_SPRINKLER": DEVICE_CLASS_SAFETY, + "S_WATER_LEAK": DEVICE_CLASS_SAFETY, + "S_SOUND": DEVICE_CLASS_SOUND, + "S_VIBRATION": DEVICE_CLASS_VIBRATION, "S_MOISTURE": "moisture", } diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py index dd52e1d665f..d4c9b6ca35d 100644 --- a/homeassistant/components/nest/binary_sensor.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -2,7 +2,10 @@ from itertools import chain import logging -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_SOUND, + BinarySensorEntity, +) from homeassistant.const import CONF_MONITORED_CONDITIONS from . import CONF_BINARY_SENSORS, DATA_NEST, DATA_NEST_CONFIG, NestSensorDevice @@ -20,7 +23,7 @@ CLIMATE_BINARY_TYPES = { CAMERA_BINARY_TYPES = { "motion_detected": "motion", - "sound_detected": "sound", + "sound_detected": DEVICE_CLASS_SOUND, "person_detected": "occupancy", } diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index 825cf149952..4e555aece22 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -3,7 +3,10 @@ from typing import Optional, Sequence from pysmartthings import Attribute, Capability -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_SOUND, + BinarySensorEntity, +) from . import SmartThingsEntity from .const import DATA_BROKERS, DOMAIN @@ -25,7 +28,7 @@ ATTRIB_TO_CLASS = { Attribute.filter_status: "problem", Attribute.motion: "motion", Attribute.presence: "presence", - Attribute.sound: "sound", + Attribute.sound: DEVICE_CLASS_SOUND, Attribute.tamper: "problem", Attribute.valve: "opening", Attribute.water: "moisture", diff --git a/homeassistant/components/wink/binary_sensor.py b/homeassistant/components/wink/binary_sensor.py index d8967dd064d..34bb4feee56 100644 --- a/homeassistant/components/wink/binary_sensor.py +++ b/homeassistant/components/wink/binary_sensor.py @@ -3,7 +3,11 @@ import logging import pywink -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_SOUND, + DEVICE_CLASS_VIBRATION, + BinarySensorEntity, +) from . import DOMAIN, WinkDevice @@ -12,17 +16,17 @@ _LOGGER = logging.getLogger(__name__) # These are the available sensors mapped to binary_sensor class SENSOR_TYPES = { "brightness": "light", - "capturing_audio": "sound", + "capturing_audio": DEVICE_CLASS_SOUND, "capturing_video": None, "co_detected": "gas", "liquid_detected": "moisture", - "loudness": "sound", + "loudness": DEVICE_CLASS_SOUND, "motion": "motion", - "noise": "sound", + "noise": DEVICE_CLASS_SOUND, "opened": "opening", "presence": "occupancy", "smoke_detected": "smoke", - "vibration": "vibration", + "vibration": DEVICE_CLASS_VIBRATION, } From 9f08955fef0d9bdfb73b517f1e02d94709ef0087 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Fri, 11 Sep 2020 21:57:07 -0300 Subject: [PATCH 091/514] Improve tests for Broadlink devices (#39898) --- tests/components/broadlink/test_device.py | 117 +++++++--------------- 1 file changed, 37 insertions(+), 80 deletions(-) diff --git a/tests/components/broadlink/test_device.py b/tests/components/broadlink/test_device.py index 5cd0457b552..a226c5e484f 100644 --- a/tests/components/broadlink/test_device.py +++ b/tests/components/broadlink/test_device.py @@ -20,16 +20,13 @@ from tests.common import mock_device_registry, mock_registry async def test_device_setup(hass): """Test a successful setup.""" device = get_device("Office") - mock_api = device.get_mock_api() - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) - with patch("broadlink.gendevice", return_value=mock_api), patch.object( + with patch.object( hass.config_entries, "async_forward_entry_setup" ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: - await hass.config_entries.async_setup(mock_entry.entry_id) + mock_api, mock_entry = await device.setup_entry(hass) assert mock_entry.state == ENTRY_STATE_LOADED assert mock_api.auth.call_count == 1 @@ -46,15 +43,13 @@ async def test_device_setup_authentication_error(hass): device = get_device("Living Room") mock_api = device.get_mock_api() mock_api.auth.side_effect = blke.AuthenticationError() - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) - with patch("broadlink.gendevice", return_value=mock_api), patch.object( + with patch.object( hass.config_entries, "async_forward_entry_setup" ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: - await hass.config_entries.async_setup(mock_entry.entry_id) + mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_entry.state == ENTRY_STATE_SETUP_ERROR assert mock_api.auth.call_count == 1 @@ -72,15 +67,13 @@ async def test_device_setup_device_offline(hass): device = get_device("Office") mock_api = device.get_mock_api() mock_api.auth.side_effect = blke.DeviceOfflineError() - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) - with patch("broadlink.gendevice", return_value=mock_api), patch.object( + with patch.object( hass.config_entries, "async_forward_entry_setup" ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: - await hass.config_entries.async_setup(mock_entry.entry_id) + mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_entry.state == ENTRY_STATE_SETUP_RETRY assert mock_api.auth.call_count == 1 @@ -93,15 +86,13 @@ async def test_device_setup_os_error(hass): device = get_device("Office") mock_api = device.get_mock_api() mock_api.auth.side_effect = OSError() - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) - with patch("broadlink.gendevice", return_value=mock_api), patch.object( + with patch.object( hass.config_entries, "async_forward_entry_setup" ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: - await hass.config_entries.async_setup(mock_entry.entry_id) + mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_entry.state == ENTRY_STATE_SETUP_RETRY assert mock_api.auth.call_count == 1 @@ -114,15 +105,13 @@ async def test_device_setup_broadlink_exception(hass): device = get_device("Office") mock_api = device.get_mock_api() mock_api.auth.side_effect = blke.BroadlinkException() - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) - with patch("broadlink.gendevice", return_value=mock_api), patch.object( + with patch.object( hass.config_entries, "async_forward_entry_setup" ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: - await hass.config_entries.async_setup(mock_entry.entry_id) + mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_entry.state == ENTRY_STATE_SETUP_ERROR assert mock_api.auth.call_count == 1 @@ -135,15 +124,13 @@ async def test_device_setup_update_device_offline(hass): device = get_device("Office") mock_api = device.get_mock_api() mock_api.check_sensors.side_effect = blke.DeviceOfflineError() - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) - with patch("broadlink.gendevice", return_value=mock_api), patch.object( + with patch.object( hass.config_entries, "async_forward_entry_setup" ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: - await hass.config_entries.async_setup(mock_entry.entry_id) + mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_entry.state == ENTRY_STATE_SETUP_RETRY assert mock_api.auth.call_count == 1 @@ -157,15 +144,13 @@ async def test_device_setup_update_authorization_error(hass): device = get_device("Office") mock_api = device.get_mock_api() mock_api.check_sensors.side_effect = (blke.AuthorizationError(), None) - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) - with patch("broadlink.gendevice", return_value=mock_api), patch.object( + with patch.object( hass.config_entries, "async_forward_entry_setup" ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: - await hass.config_entries.async_setup(mock_entry.entry_id) + mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_entry.state == ENTRY_STATE_LOADED assert mock_api.auth.call_count == 2 @@ -183,15 +168,13 @@ async def test_device_setup_update_authentication_error(hass): mock_api = device.get_mock_api() mock_api.check_sensors.side_effect = blke.AuthorizationError() mock_api.auth.side_effect = (None, blke.AuthenticationError()) - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) - with patch("broadlink.gendevice", return_value=mock_api), patch.object( + with patch.object( hass.config_entries, "async_forward_entry_setup" ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: - await hass.config_entries.async_setup(mock_entry.entry_id) + mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_entry.state == ENTRY_STATE_SETUP_RETRY assert mock_api.auth.call_count == 2 @@ -210,15 +193,13 @@ async def test_device_setup_update_broadlink_exception(hass): device = get_device("Living Room") mock_api = device.get_mock_api() mock_api.check_sensors.side_effect = blke.BroadlinkException() - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) - with patch("broadlink.gendevice", return_value=mock_api), patch.object( + with patch.object( hass.config_entries, "async_forward_entry_setup" ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: - await hass.config_entries.async_setup(mock_entry.entry_id) + mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_entry.state == ENTRY_STATE_SETUP_RETRY assert mock_api.auth.call_count == 1 @@ -232,13 +213,9 @@ async def test_device_setup_get_fwversion_broadlink_exception(hass): device = get_device("Office") mock_api = device.get_mock_api() mock_api.get_fwversion.side_effect = blke.BroadlinkException() - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) - with patch("broadlink.gendevice", return_value=mock_api), patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as mock_forward: - await hass.config_entries.async_setup(mock_entry.entry_id) + with patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward: + mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_entry.state == ENTRY_STATE_LOADED forward_entries = {c[1][1] for c in mock_forward.mock_calls} @@ -252,13 +229,9 @@ async def test_device_setup_get_fwversion_os_error(hass): device = get_device("Office") mock_api = device.get_mock_api() mock_api.get_fwversion.side_effect = OSError() - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) - with patch("broadlink.gendevice", return_value=mock_api), patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as mock_forward: - await hass.config_entries.async_setup(mock_entry.entry_id) + with patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward: + _, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_entry.state == ENTRY_STATE_LOADED forward_entries = {c[1][1] for c in mock_forward.mock_calls} @@ -270,16 +243,12 @@ async def test_device_setup_get_fwversion_os_error(hass): async def test_device_setup_registry(hass): """Test we register the device and the entries correctly.""" device = get_device("Office") - mock_api = device.get_mock_api() - 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_entry = await device.setup_entry(hass) + await hass.async_block_till_done() assert len(device_registry.devices) == 1 @@ -299,14 +268,9 @@ async def test_device_setup_registry(hass): async def test_device_unload_works(hass): """Test we unload the device.""" device = get_device("Office") - mock_api = device.get_mock_api() - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) - with patch("broadlink.gendevice", return_value=mock_api), patch.object( - hass.config_entries, "async_forward_entry_setup" - ): - await hass.config_entries.async_setup(mock_entry.entry_id) + with patch.object(hass.config_entries, "async_forward_entry_setup"): + mock_api, mock_entry = await device.setup_entry(hass) with patch.object( hass.config_entries, "async_forward_entry_unload", return_value=True @@ -325,13 +289,11 @@ async def test_device_unload_authentication_error(hass): device = get_device("Living Room") mock_api = device.get_mock_api() mock_api.auth.side_effect = blke.AuthenticationError() - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) - with patch("broadlink.gendevice", return_value=mock_api), patch.object( - hass.config_entries, "async_forward_entry_setup" - ), patch.object(hass.config_entries.flow, "async_init"): - await hass.config_entries.async_setup(mock_entry.entry_id) + with patch.object(hass.config_entries, "async_forward_entry_setup"), patch.object( + hass.config_entries.flow, "async_init" + ): + _, mock_entry = await device.setup_entry(hass, mock_api=mock_api) with patch.object( hass.config_entries, "async_forward_entry_unload", return_value=True @@ -347,13 +309,9 @@ async def test_device_unload_update_failed(hass): device = get_device("Office") mock_api = device.get_mock_api() mock_api.check_sensors.side_effect = blke.DeviceOfflineError() - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) - with patch("broadlink.gendevice", return_value=mock_api), patch.object( - hass.config_entries, "async_forward_entry_setup" - ): - await hass.config_entries.async_setup(mock_entry.entry_id) + with patch.object(hass.config_entries, "async_forward_entry_setup"): + _, mock_entry = await device.setup_entry(hass, mock_api=mock_api) with patch.object( hass.config_entries, "async_forward_entry_unload", return_value=True @@ -367,17 +325,16 @@ async def test_device_unload_update_failed(hass): async def test_device_update_listener(hass): """Test we update device and entity registry when the entry is renamed.""" device = get_device("Office") - mock_api = device.get_mock_api() - 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) + await hass.async_block_till_done() + with patch( + "homeassistant.components.broadlink.device.blk.gendevice", return_value=mock_api + ): hass.config_entries.async_update_entry(mock_entry, title="New Name") await hass.async_block_till_done() From a6d3ee90f077cea7f309ac851d555d9c941fa91b Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Fri, 11 Sep 2020 22:00:28 -0300 Subject: [PATCH 092/514] Improve tests for Broadlink config flow (#39894) --- .../components/broadlink/test_config_flow.py | 83 ++++++++++--------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/tests/components/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py index 4089c551ff5..2bb00c347c7 100644 --- a/tests/components/broadlink/test_config_flow.py +++ b/tests/components/broadlink/test_config_flow.py @@ -12,13 +12,16 @@ from . import get_device from tests.async_mock import call, patch +DEVICE_DISCOVERY = "homeassistant.components.broadlink.config_flow.blk.discover" +DEVICE_FACTORY = "homeassistant.components.broadlink.config_flow.blk.gendevice" + @pytest.fixture(autouse=True) def broadlink_setup_fixture(): """Mock broadlink entry setup.""" with patch( - "homeassistant.components.broadlink.async_setup_entry", return_value=True - ): + "homeassistant.components.broadlink.async_setup", return_value=True + ), patch("homeassistant.components.broadlink.async_setup_entry", return_value=True): yield @@ -38,7 +41,7 @@ async def test_flow_user_works(hass): assert result["step_id"] == "user" assert result["errors"] == {} - with patch("broadlink.discover", return_value=[mock_api]) as mock_discover: + with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -69,7 +72,7 @@ async def test_flow_user_already_in_progress(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[device.get_mock_api()]): + with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -79,7 +82,7 @@ async def test_flow_user_already_in_progress(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[device.get_mock_api()]): + with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -106,7 +109,7 @@ async def test_flow_user_mac_already_configured(hass): device.timeout = 20 mock_api = device.get_mock_api() - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -125,7 +128,7 @@ async def test_flow_user_invalid_ip_address(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", side_effect=OSError(errno.EINVAL, None)): + with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.EINVAL, None)): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "0.0.0.1"}, @@ -142,7 +145,7 @@ async def test_flow_user_invalid_hostname(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", side_effect=OSError(socket.EAI_NONAME, None)): + with patch(DEVICE_DISCOVERY, side_effect=OSError(socket.EAI_NONAME, None)): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "pancakemaster.local"}, @@ -161,7 +164,7 @@ async def test_flow_user_device_not_found(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[]): + with patch(DEVICE_DISCOVERY, return_value=[]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host}, @@ -178,7 +181,7 @@ async def test_flow_user_network_unreachable(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", side_effect=OSError(errno.ENETUNREACH, None)): + with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.ENETUNREACH, None)): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "192.168.1.32"}, @@ -195,7 +198,7 @@ async def test_flow_user_os_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", side_effect=OSError()): + with patch(DEVICE_DISCOVERY, side_effect=OSError()): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "192.168.1.32"}, @@ -216,7 +219,7 @@ async def test_flow_auth_authentication_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -237,7 +240,7 @@ async def test_flow_auth_device_offline(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host}, @@ -258,7 +261,7 @@ async def test_flow_auth_firmware_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host}, @@ -279,7 +282,7 @@ async def test_flow_auth_network_unreachable(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host}, @@ -300,7 +303,7 @@ async def test_flow_auth_os_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host}, @@ -321,13 +324,13 @@ async def test_flow_reset_works(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, ) - with patch("broadlink.discover", return_value=[device.get_mock_api()]): + with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -353,7 +356,7 @@ async def test_flow_unlock_works(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -392,7 +395,7 @@ async def test_flow_unlock_device_offline(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -419,7 +422,7 @@ async def test_flow_unlock_firmware_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -446,7 +449,7 @@ async def test_flow_unlock_network_unreachable(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -473,7 +476,7 @@ async def test_flow_unlock_os_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -499,7 +502,7 @@ async def test_flow_do_not_unlock(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -527,7 +530,7 @@ async def test_flow_import_works(hass): device = get_device("Living Room") mock_api = device.get_mock_api() - with patch("broadlink.discover", return_value=[mock_api]) as mock_discover: + with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -558,12 +561,12 @@ async def test_flow_import_already_in_progress(hass): device = get_device("Living Room") data = {"host": device.host} - with patch("broadlink.discover", return_value=[device.get_mock_api()]): + with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=data ) - with patch("broadlink.discover", return_value=[device.get_mock_api()]): + with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=data ) @@ -579,7 +582,7 @@ async def test_flow_import_host_already_configured(hass): mock_entry.add_to_hass(hass) mock_api = device.get_mock_api() - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -602,7 +605,7 @@ async def test_flow_import_mac_already_configured(hass): device.host = "192.168.1.16" mock_api = device.get_mock_api() - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -620,7 +623,7 @@ async def test_flow_import_mac_already_configured(hass): async def test_flow_import_device_not_found(hass): """Test we handle a device not found in the import step.""" - with patch("broadlink.discover", return_value=[]): + with patch(DEVICE_DISCOVERY, return_value=[]): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -633,7 +636,7 @@ async def test_flow_import_device_not_found(hass): async def test_flow_import_invalid_ip_address(hass): """Test we handle an invalid IP address in the import step.""" - with patch("broadlink.discover", side_effect=OSError(errno.EINVAL, None)): + with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.EINVAL, None)): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -646,7 +649,7 @@ async def test_flow_import_invalid_ip_address(hass): async def test_flow_import_invalid_hostname(hass): """Test we handle an invalid hostname in the import step.""" - with patch("broadlink.discover", side_effect=OSError(socket.EAI_NONAME, None)): + with patch(DEVICE_DISCOVERY, side_effect=OSError(socket.EAI_NONAME, None)): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -659,7 +662,7 @@ async def test_flow_import_invalid_hostname(hass): async def test_flow_import_network_unreachable(hass): """Test we handle a network unreachable in the import step.""" - with patch("broadlink.discover", side_effect=OSError(errno.ENETUNREACH, None)): + with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.ENETUNREACH, None)): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -672,7 +675,7 @@ async def test_flow_import_network_unreachable(hass): async def test_flow_import_os_error(hass): """Test we handle an OS error in the import step.""" - with patch("broadlink.discover", side_effect=OSError()): + with patch(DEVICE_DISCOVERY, side_effect=OSError()): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -692,7 +695,7 @@ async def test_flow_reauth_works(hass): mock_api.auth.side_effect = blke.AuthenticationError() data = {"name": device.name, **device.get_entry_data()} - with patch("broadlink.gendevice", return_value=mock_api): + with patch(DEVICE_FACTORY, return_value=mock_api): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "reauth"}, data=data ) @@ -702,7 +705,7 @@ async def test_flow_reauth_works(hass): mock_api = device.get_mock_api() - with patch("broadlink.discover", return_value=[mock_api]) as mock_discover: + with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -728,7 +731,7 @@ async def test_flow_reauth_invalid_host(hass): mock_api.auth.side_effect = blke.AuthenticationError() data = {"name": device.name, **device.get_entry_data()} - with patch("broadlink.gendevice", return_value=mock_api): + with patch(DEVICE_FACTORY, return_value=mock_api): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "reauth"}, data=data ) @@ -736,7 +739,7 @@ async def test_flow_reauth_invalid_host(hass): device.mac = get_device("Office").mac mock_api = device.get_mock_api() - with patch("broadlink.discover", return_value=[mock_api]) as mock_discover: + with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -762,7 +765,7 @@ async def test_flow_reauth_valid_host(hass): mock_api.auth.side_effect = blke.AuthenticationError() data = {"name": device.name, **device.get_entry_data()} - with patch("broadlink.gendevice", return_value=mock_api): + with patch(DEVICE_FACTORY, return_value=mock_api): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "reauth"}, data=data ) @@ -770,7 +773,7 @@ async def test_flow_reauth_valid_host(hass): device.host = "192.168.1.128" mock_api = device.get_mock_api() - with patch("broadlink.discover", return_value=[mock_api]) as mock_discover: + with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, From f93c0c5cd32300fe38bc86c3cda60b738db54590 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 12 Sep 2020 04:24:23 +0200 Subject: [PATCH 093/514] Use DEVICE_CLASS_WINDOW constant in various integrations (#39949) --- homeassistant/components/brunt/cover.py | 4 ++-- homeassistant/components/deconz/cover.py | 3 ++- homeassistant/components/fibaro/binary_sensor.py | 8 ++++++-- homeassistant/components/fritzbox/binary_sensor.py | 7 +++++-- homeassistant/components/maxcube/binary_sensor.py | 7 +++++-- homeassistant/components/notion/binary_sensor.py | 9 ++++++--- homeassistant/components/somfy_mylink/cover.py | 8 ++++++-- 7 files changed, 32 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index 7f36874c40e..ceb56ba03fa 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.components.cover import ( ATTR_POSITION, + DEVICE_CLASS_WINDOW, PLATFORM_SCHEMA, SUPPORT_CLOSE, SUPPORT_OPEN, @@ -19,7 +20,6 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) COVER_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION -DEVICE_CLASS = "window" ATTR_REQUEST_POSITION = "request_position" NOTIFICATION_ID = "brunt_notification" @@ -141,7 +141,7 @@ class BruntDevice(CoverEntity): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS + return DEVICE_CLASS_WINDOW @property def supported_features(self): diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index e01cfdbe5f8..996727d366b 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -1,6 +1,7 @@ """Support for deCONZ covers.""" from homeassistant.components.cover import ( ATTR_POSITION, + DEVICE_CLASS_WINDOW, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, @@ -74,7 +75,7 @@ class DeconzCover(DeconzDevice, CoverEntity): if self._device.type in DAMPERS: return "damper" if self._device.type in WINDOW_COVERS: - return "window" + return DEVICE_CLASS_WINDOW @property def supported_features(self): diff --git a/homeassistant/components/fibaro/binary_sensor.py b/homeassistant/components/fibaro/binary_sensor.py index 251bd1df6a3..bd327e7de3d 100644 --- a/homeassistant/components/fibaro/binary_sensor.py +++ b/homeassistant/components/fibaro/binary_sensor.py @@ -1,7 +1,11 @@ """Support for Fibaro binary sensors.""" import logging -from homeassistant.components.binary_sensor import DOMAIN, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_WINDOW, + DOMAIN, + BinarySensorEntity, +) from homeassistant.const import CONF_DEVICE_CLASS, CONF_ICON from . import FIBARO_DEVICES, FibaroDevice @@ -12,7 +16,7 @@ SENSOR_TYPES = { "com.fibaro.floodSensor": ["Flood", "mdi:water", "flood"], "com.fibaro.motionSensor": ["Motion", "mdi:run", "motion"], "com.fibaro.doorSensor": ["Door", "mdi:window-open", "door"], - "com.fibaro.windowSensor": ["Window", "mdi:window-open", "window"], + "com.fibaro.windowSensor": ["Window", "mdi:window-open", DEVICE_CLASS_WINDOW], "com.fibaro.smokeSensor": ["Smoke", "mdi:smoking", "smoke"], "com.fibaro.FGMS001": ["Motion", "mdi:run", "motion"], "com.fibaro.heatDetector": ["Heat", "mdi:fire", "heat"], diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index 7db216c32e1..1246eb4afaf 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -1,7 +1,10 @@ """Support for Fritzbox binary sensors.""" import requests -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_WINDOW, + BinarySensorEntity, +) from homeassistant.const import CONF_DEVICES from .const import CONF_CONNECTIONS, DOMAIN as FRITZBOX_DOMAIN, LOGGER @@ -53,7 +56,7 @@ class FritzboxBinarySensor(BinarySensorEntity): @property def device_class(self): """Return the class of this sensor.""" - return "window" + return DEVICE_CLASS_WINDOW @property def is_on(self): diff --git a/homeassistant/components/maxcube/binary_sensor.py b/homeassistant/components/maxcube/binary_sensor.py index b42c96f99c2..1897f3b7465 100644 --- a/homeassistant/components/maxcube/binary_sensor.py +++ b/homeassistant/components/maxcube/binary_sensor.py @@ -1,7 +1,10 @@ """Support for MAX! binary sensors via MAX! Cube.""" import logging -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_WINDOW, + BinarySensorEntity, +) from . import DATA_KEY @@ -30,7 +33,7 @@ class MaxCubeShutter(BinarySensorEntity): def __init__(self, handler, name, rf_address): """Initialize MAX! Cube BinarySensorEntity.""" self._name = name - self._sensor_type = "window" + self._sensor_type = DEVICE_CLASS_WINDOW self._rf_address = rf_address self._cubehandle = handler self._state = None diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index e798b538565..74d7cabddd3 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -2,7 +2,10 @@ import logging from typing import Callable -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_WINDOW, + BinarySensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -33,8 +36,8 @@ BINARY_SENSOR_TYPES = { SENSOR_SAFE: ("Safe", "door"), SENSOR_SLIDING: ("Sliding Door/Window", "door"), SENSOR_SMOKE_CO: ("Smoke/Carbon Monoxide Detector", "smoke"), - SENSOR_WINDOW_HINGED_HORIZONTAL: ("Hinged Window", "window"), - SENSOR_WINDOW_HINGED_VERTICAL: ("Hinged Window", "window"), + SENSOR_WINDOW_HINGED_HORIZONTAL: ("Hinged Window", DEVICE_CLASS_WINDOW), + SENSOR_WINDOW_HINGED_VERTICAL: ("Hinged Window", DEVICE_CLASS_WINDOW), } diff --git a/homeassistant/components/somfy_mylink/cover.py b/homeassistant/components/somfy_mylink/cover.py index b849a490940..ac3bf0673f1 100644 --- a/homeassistant/components/somfy_mylink/cover.py +++ b/homeassistant/components/somfy_mylink/cover.py @@ -1,7 +1,11 @@ """Cover Platform for the Somfy MyLink component.""" import logging -from homeassistant.components.cover import ENTITY_ID_FORMAT, CoverEntity +from homeassistant.components.cover import ( + DEVICE_CLASS_WINDOW, + ENTITY_ID_FORMAT, + CoverEntity, +) from homeassistant.util import slugify from . import CONF_DEFAULT_REVERSE, DATA_SOMFY_MYLINK @@ -49,7 +53,7 @@ class SomfyShade(CoverEntity): target_id, name="SomfyShade", reverse=False, - device_class="window", + device_class=DEVICE_CLASS_WINDOW, ): """Initialize the cover.""" self.somfy_mylink = somfy_mylink From 824f551969b7ccc775e93a0501de4016ab711e49 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 12 Sep 2020 09:08:04 +0200 Subject: [PATCH 094/514] Upgrade codecov to 2.1.9 (#39960) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index e36837edf63..72efb534c30 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,7 +5,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt asynctest==0.13.0 -codecov==2.1.0 +codecov==2.1.9 coverage==5.2.1 jsonpickle==1.4.1 mock-open==1.4.0 From ee6945d63d13665e0fdc1eb43d702b9bab286b80 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 12 Sep 2020 09:39:08 +0200 Subject: [PATCH 095/514] Upgrade pytest-cov to 2.10.1 (#39964) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 72efb534c30..ff60d861f66 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -16,7 +16,7 @@ astroid==2.4.2 pipdeptree==1.0.0 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 -pytest-cov==2.10.0 +pytest-cov==2.10.1 pytest-test-groups==1.0.3 pytest-sugar==0.9.3 pytest-timeout==1.3.4 From fbf0e695583fa66d08e7631e27b25cfdcd2fed98 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 12 Sep 2020 10:35:51 +0200 Subject: [PATCH 096/514] 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 3dda76115cf9a4a44538577ff46e15bf8dc9d61c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 12 Sep 2020 11:30:41 +0200 Subject: [PATCH 097/514] Upgrade pytest-sugar to 0.9.4 (#39966) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index ff60d861f66..ae98fe897c2 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -18,7 +18,7 @@ pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 pytest-cov==2.10.1 pytest-test-groups==1.0.3 -pytest-sugar==0.9.3 +pytest-sugar==0.9.4 pytest-timeout==1.3.4 pytest-xdist==1.32.0 pytest==5.4.3 From e746965b1c33fff281238a1a07963014a83ed141 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 12 Sep 2020 11:31:22 +0200 Subject: [PATCH 098/514] Upgrade mypy to 0.782 (#39967) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index ae98fe897c2..7958477f2ef 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ codecov==2.1.9 coverage==5.2.1 jsonpickle==1.4.1 mock-open==1.4.0 -mypy==0.780 +mypy==0.782 pre-commit==2.7.1 pylint==2.6.0 astroid==2.4.2 From aaa8083d495e80e50320e83e64b854746b2321c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 12 Sep 2020 07:20:21 -0500 Subject: [PATCH 099/514] 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 8e126c7c14c..733214749ef 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -562,7 +562,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.""" @@ -723,27 +722,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 cf6b84790ff7550c6eb4c3ee65ab9d0e698539bd Mon Sep 17 00:00:00 2001 From: On Freund Date: Sat, 12 Sep 2020 15:22:14 +0300 Subject: [PATCH 100/514] 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 a2256a6ea82..bc80165d46b 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 f4acc7ae79c..0b34d1f3333 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 1bb5d4754fc8b0e1da7375e7bdcaea56d404e199 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 12 Sep 2020 14:35:24 +0200 Subject: [PATCH 101/514] Use DEVICE_CLASS_DOOR and DEVICE_CLASS_SMOKE in various integrations (#39950) --- homeassistant/components/concord232/binary_sensor.py | 3 ++- homeassistant/components/fibaro/binary_sensor.py | 6 ++++-- homeassistant/components/notion/binary_sensor.py | 10 ++++++---- .../components/satel_integra/binary_sensor.py | 7 +++++-- homeassistant/components/spc/binary_sensor.py | 7 +++++-- homeassistant/components/tahoma/binary_sensor.py | 7 +++++-- homeassistant/components/wink/binary_sensor.py | 3 ++- 7 files changed, 29 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 806fb5c513d..5ff75526b27 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, DEVICE_CLASSES, PLATFORM_SCHEMA, BinarySensorEntity, @@ -90,7 +91,7 @@ def get_opening_type(zone): if "KEY" in zone["name"]: return DEVICE_CLASS_SAFETY if "SMOKE" in zone["name"]: - return "smoke" + return DEVICE_CLASS_SMOKE if "WATER" in zone["name"]: return "water" return "opening" diff --git a/homeassistant/components/fibaro/binary_sensor.py b/homeassistant/components/fibaro/binary_sensor.py index bd327e7de3d..42783df9df6 100644 --- a/homeassistant/components/fibaro/binary_sensor.py +++ b/homeassistant/components/fibaro/binary_sensor.py @@ -2,6 +2,8 @@ import logging from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_DOOR, + DEVICE_CLASS_SMOKE, DEVICE_CLASS_WINDOW, DOMAIN, BinarySensorEntity, @@ -15,9 +17,9 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { "com.fibaro.floodSensor": ["Flood", "mdi:water", "flood"], "com.fibaro.motionSensor": ["Motion", "mdi:run", "motion"], - "com.fibaro.doorSensor": ["Door", "mdi:window-open", "door"], + "com.fibaro.doorSensor": ["Door", "mdi:window-open", DEVICE_CLASS_DOOR], "com.fibaro.windowSensor": ["Window", "mdi:window-open", DEVICE_CLASS_WINDOW], - "com.fibaro.smokeSensor": ["Smoke", "mdi:smoking", "smoke"], + "com.fibaro.smokeSensor": ["Smoke", "mdi:smoking", DEVICE_CLASS_SMOKE], "com.fibaro.FGMS001": ["Motion", "mdi:run", "motion"], "com.fibaro.heatDetector": ["Heat", "mdi:fire", "heat"], } diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index 74d7cabddd3..3d02e364021 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -3,6 +3,8 @@ import logging from typing import Callable from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_DOOR, + DEVICE_CLASS_SMOKE, DEVICE_CLASS_WINDOW, BinarySensorEntity, ) @@ -29,13 +31,13 @@ _LOGGER = logging.getLogger(__name__) BINARY_SENSOR_TYPES = { SENSOR_BATTERY: ("Low Battery", "battery"), - SENSOR_DOOR: ("Door", "door"), + SENSOR_DOOR: ("Door", DEVICE_CLASS_DOOR), SENSOR_GARAGE_DOOR: ("Garage Door", "garage_door"), SENSOR_LEAK: ("Leak Detector", "moisture"), SENSOR_MISSING: ("Missing", "connectivity"), - SENSOR_SAFE: ("Safe", "door"), - SENSOR_SLIDING: ("Sliding Door/Window", "door"), - SENSOR_SMOKE_CO: ("Smoke/Carbon Monoxide Detector", "smoke"), + SENSOR_SAFE: ("Safe", DEVICE_CLASS_DOOR), + SENSOR_SLIDING: ("Sliding Door/Window", DEVICE_CLASS_DOOR), + SENSOR_SMOKE_CO: ("Smoke/Carbon Monoxide Detector", DEVICE_CLASS_SMOKE), SENSOR_WINDOW_HINGED_HORIZONTAL: ("Hinged Window", DEVICE_CLASS_WINDOW), SENSOR_WINDOW_HINGED_VERTICAL: ("Hinged Window", DEVICE_CLASS_WINDOW), } diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py index 19763903f27..ea9c19376f6 100644 --- a/homeassistant/components/satel_integra/binary_sensor.py +++ b/homeassistant/components/satel_integra/binary_sensor.py @@ -1,7 +1,10 @@ """Support for Satel Integra zone states- represented as binary sensors.""" import logging -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_SMOKE, + BinarySensorEntity, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -89,7 +92,7 @@ class SatelIntegraBinarySensor(BinarySensorEntity): @property def icon(self): """Icon for device by its type.""" - if self._zone_type == "smoke": + if self._zone_type == DEVICE_CLASS_SMOKE: return "mdi:fire" @property diff --git a/homeassistant/components/spc/binary_sensor.py b/homeassistant/components/spc/binary_sensor.py index 75256b60cfb..faa803b25dd 100644 --- a/homeassistant/components/spc/binary_sensor.py +++ b/homeassistant/components/spc/binary_sensor.py @@ -3,7 +3,10 @@ import logging from pyspcwebgw.const import ZoneInput, ZoneType -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_SMOKE, + BinarySensorEntity, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -16,7 +19,7 @@ def _get_device_class(zone_type): return { ZoneType.ALARM: "motion", ZoneType.ENTRY_EXIT: "opening", - ZoneType.FIRE: "smoke", + ZoneType.FIRE: DEVICE_CLASS_SMOKE, ZoneType.TECHNICAL: "power", }.get(zone_type) diff --git a/homeassistant/components/tahoma/binary_sensor.py b/homeassistant/components/tahoma/binary_sensor.py index 39e492601bd..af06bf5ca4c 100644 --- a/homeassistant/components/tahoma/binary_sensor.py +++ b/homeassistant/components/tahoma/binary_sensor.py @@ -2,7 +2,10 @@ from datetime import timedelta import logging -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_SMOKE, + BinarySensorEntity, +) from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice @@ -45,7 +48,7 @@ class TahomaBinarySensor(TahomaDevice, BinarySensorEntity): def device_class(self): """Return the class of the device.""" if self.tahoma_device.type == "rtds:RTDSSmokeSensor": - return "smoke" + return DEVICE_CLASS_SMOKE return None @property diff --git a/homeassistant/components/wink/binary_sensor.py b/homeassistant/components/wink/binary_sensor.py index 34bb4feee56..d4443f31ab6 100644 --- a/homeassistant/components/wink/binary_sensor.py +++ b/homeassistant/components/wink/binary_sensor.py @@ -4,6 +4,7 @@ import logging import pywink from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, DEVICE_CLASS_VIBRATION, BinarySensorEntity, @@ -25,7 +26,7 @@ SENSOR_TYPES = { "noise": DEVICE_CLASS_SOUND, "opened": "opening", "presence": "occupancy", - "smoke_detected": "smoke", + "smoke_detected": DEVICE_CLASS_SMOKE, "vibration": DEVICE_CLASS_VIBRATION, } From 6b966e2c47367c4a9ce8ee607536275e8438568b Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Sat, 12 Sep 2020 14:58:29 +0200 Subject: [PATCH 102/514] Upgrade youtube_dl to version 2020.09.06 (#39969) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 62d53b17c47..5ec4b3ac166 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -2,7 +2,7 @@ "domain": "media_extractor", "name": "Media Extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", - "requirements": ["youtube_dl==2020.07.28"], + "requirements": ["youtube_dl==2020.09.06"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/requirements_all.txt b/requirements_all.txt index bc80165d46b..255c1c23f39 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2281,7 +2281,7 @@ yeelight==0.5.3 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2020.07.28 +youtube_dl==2020.09.06 # homeassistant.components.zengge zengge==0.2 From 4c0f075d6ae9c9e057bdeb3602602eab63ae9db0 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 12 Sep 2020 08:54:00 -0500 Subject: [PATCH 103/514] 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 cee96ae207cff58c99ff263c684e8abe843694a6 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 12 Sep 2020 18:07:13 +0200 Subject: [PATCH 104/514] Use opening and occupancy device class in various integrations (#39965) --- .../components/bmw_connected_drive/binary_sensor.py | 9 ++++++--- homeassistant/components/concord232/binary_sensor.py | 3 ++- homeassistant/components/danfoss_air/binary_sensor.py | 7 +++++-- homeassistant/components/egardia/binary_sensor.py | 7 +++++-- homeassistant/components/hive/binary_sensor.py | 10 ++++++++-- homeassistant/components/nest/binary_sensor.py | 3 ++- homeassistant/components/nx584/binary_sensor.py | 5 ++++- homeassistant/components/ring/binary_sensor.py | 7 +++++-- homeassistant/components/skybell/binary_sensor.py | 8 ++++++-- homeassistant/components/sleepiq/binary_sensor.py | 7 +++++-- homeassistant/components/smartthings/binary_sensor.py | 5 +++-- homeassistant/components/spc/binary_sensor.py | 3 ++- homeassistant/components/wink/binary_sensor.py | 6 ++++-- homeassistant/components/xiaomi_aqara/binary_sensor.py | 7 +++++-- 14 files changed, 62 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index ee89873e8fe..5cc3964ccee 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -3,7 +3,10 @@ import logging from bimmer_connected.state import ChargingState, LockState -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OPENING, + BinarySensorEntity, +) from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_KILOMETERS from . import DOMAIN as BMW_DOMAIN @@ -12,8 +15,8 @@ from .const import ATTRIBUTION _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - "lids": ["Doors", "opening", "mdi:car-door-lock"], - "windows": ["Windows", "opening", "mdi:car-door"], + "lids": ["Doors", DEVICE_CLASS_OPENING, "mdi:car-door-lock"], + "windows": ["Windows", DEVICE_CLASS_OPENING, "mdi:car-door"], "door_lock_state": ["Door lock state", "lock", "mdi:car-key"], "lights_parking": ["Parking lights", "light", "mdi:car-parking-lights"], "condition_based_services": ["Condition based services", "problem", "mdi:wrench"], diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 5ff75526b27..3b2e980b777 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -7,6 +7,7 @@ import requests import voluptuous as vol from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OPENING, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASSES, @@ -94,7 +95,7 @@ def get_opening_type(zone): return DEVICE_CLASS_SMOKE if "WATER" in zone["name"]: return "water" - return "opening" + return DEVICE_CLASS_OPENING class Concord232ZoneSensor(BinarySensorEntity): diff --git a/homeassistant/components/danfoss_air/binary_sensor.py b/homeassistant/components/danfoss_air/binary_sensor.py index 7f6876a709b..9d3123185c4 100644 --- a/homeassistant/components/danfoss_air/binary_sensor.py +++ b/homeassistant/components/danfoss_air/binary_sensor.py @@ -1,7 +1,10 @@ """Support for the for Danfoss Air HRV binary sensors.""" from pydanfossair.commands import ReadCommand -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OPENING, + BinarySensorEntity, +) from . import DOMAIN as DANFOSS_AIR_DOMAIN @@ -11,7 +14,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data = hass.data[DANFOSS_AIR_DOMAIN] sensors = [ - ["Danfoss Air Bypass Active", ReadCommand.bypass, "opening"], + ["Danfoss Air Bypass Active", ReadCommand.bypass, DEVICE_CLASS_OPENING], ["Danfoss Air Away Mode Active", ReadCommand.away_mode, None], ] diff --git a/homeassistant/components/egardia/binary_sensor.py b/homeassistant/components/egardia/binary_sensor.py index 4be443a36f4..f4bb5097625 100644 --- a/homeassistant/components/egardia/binary_sensor.py +++ b/homeassistant/components/egardia/binary_sensor.py @@ -1,7 +1,10 @@ """Interfaces with Egardia/Woonveilig alarm control panel.""" import logging -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OPENING, + BinarySensorEntity, +) from homeassistant.const import STATE_OFF, STATE_ON from . import ATTR_DISCOVER_DEVICES, EGARDIA_DEVICE @@ -10,7 +13,7 @@ _LOGGER = logging.getLogger(__name__) EGARDIA_TYPE_TO_DEVICE_CLASS = { "IR Sensor": "motion", - "Door Contact": "opening", + "Door Contact": DEVICE_CLASS_OPENING, "IR": "motion", } diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index 27c648f554b..bc71939bf6a 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -1,9 +1,15 @@ """Support for the Hive binary sensors.""" -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OPENING, + BinarySensorEntity, +) from . import DATA_HIVE, DOMAIN, HiveEntity -DEVICETYPE_DEVICE_CLASS = {"motionsensor": "motion", "contactsensor": "opening"} +DEVICETYPE_DEVICE_CLASS = { + "motionsensor": "motion", + "contactsensor": DEVICE_CLASS_OPENING, +} def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py index d4c9b6ca35d..35acab6f504 100644 --- a/homeassistant/components/nest/binary_sensor.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -3,6 +3,7 @@ from itertools import chain import logging from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_SOUND, BinarySensorEntity, ) @@ -24,7 +25,7 @@ CLIMATE_BINARY_TYPES = { CAMERA_BINARY_TYPES = { "motion_detected": "motion", "sound_detected": DEVICE_CLASS_SOUND, - "person_detected": "occupancy", + "person_detected": DEVICE_CLASS_OCCUPANCY, } STRUCTURE_BINARY_TYPES = {"away": None} diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py index 127ce02b371..2db3531f879 100644 --- a/homeassistant/components/nx584/binary_sensor.py +++ b/homeassistant/components/nx584/binary_sensor.py @@ -8,6 +8,7 @@ import requests import voluptuous as vol from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OPENING, DEVICE_CLASSES, PLATFORM_SCHEMA, BinarySensorEntity, @@ -59,7 +60,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return False zone_sensors = { - zone["number"]: NX584ZoneSensor(zone, zone_types.get(zone["number"], "opening")) + zone["number"]: NX584ZoneSensor( + zone, zone_types.get(zone["number"], DEVICE_CLASS_OPENING) + ) for zone in zones if zone["number"] not in exclude } diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index fa303b94378..876caad3f85 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -2,7 +2,10 @@ from datetime import datetime import logging -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OCCUPANCY, + BinarySensorEntity, +) from homeassistant.core import callback from . import DOMAIN @@ -12,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) # Sensor types: Name, category, device_class SENSOR_TYPES = { - "ding": ["Ding", ["doorbots", "authorized_doorbots"], "occupancy"], + "ding": ["Ding", ["doorbots", "authorized_doorbots"], DEVICE_CLASS_OCCUPANCY], "motion": ["Motion", ["doorbots", "authorized_doorbots", "stickup_cams"], "motion"], } diff --git a/homeassistant/components/skybell/binary_sensor.py b/homeassistant/components/skybell/binary_sensor.py index a5c6681eb2b..38cf0a98f40 100644 --- a/homeassistant/components/skybell/binary_sensor.py +++ b/homeassistant/components/skybell/binary_sensor.py @@ -4,7 +4,11 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OCCUPANCY, + PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv @@ -16,7 +20,7 @@ SCAN_INTERVAL = timedelta(seconds=5) # Sensor types: Name, device_class, event SENSOR_TYPES = { - "button": ["Button", "occupancy", "device:sensor:button"], + "button": ["Button", DEVICE_CLASS_OCCUPANCY, "device:sensor:button"], "motion": ["Motion", "motion", "device:sensor:motion"], } diff --git a/homeassistant/components/sleepiq/binary_sensor.py b/homeassistant/components/sleepiq/binary_sensor.py index 39ae3e7c658..cfbd6f576be 100644 --- a/homeassistant/components/sleepiq/binary_sensor.py +++ b/homeassistant/components/sleepiq/binary_sensor.py @@ -1,5 +1,8 @@ """Support for SleepIQ sensors.""" -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OCCUPANCY, + BinarySensorEntity, +) from . import SleepIQSensor from .const import DOMAIN, IS_IN_BED, SENSOR_TYPES, SIDES @@ -39,7 +42,7 @@ class IsInBedBinarySensor(SleepIQSensor, BinarySensorEntity): @property def device_class(self): """Return the class of this sensor.""" - return "occupancy" + return DEVICE_CLASS_OCCUPANCY def update(self): """Get the latest data from SleepIQ and updates the states.""" diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index 4e555aece22..baeac063598 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -4,6 +4,7 @@ from typing import Optional, Sequence from pysmartthings import Attribute, Capability from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OPENING, DEVICE_CLASS_SOUND, BinarySensorEntity, ) @@ -24,13 +25,13 @@ CAPABILITY_TO_ATTRIB = { } ATTRIB_TO_CLASS = { Attribute.acceleration: "moving", - Attribute.contact: "opening", + Attribute.contact: DEVICE_CLASS_OPENING, Attribute.filter_status: "problem", Attribute.motion: "motion", Attribute.presence: "presence", Attribute.sound: DEVICE_CLASS_SOUND, Attribute.tamper: "problem", - Attribute.valve: "opening", + Attribute.valve: DEVICE_CLASS_OPENING, Attribute.water: "moisture", } diff --git a/homeassistant/components/spc/binary_sensor.py b/homeassistant/components/spc/binary_sensor.py index faa803b25dd..4ee8ffad77e 100644 --- a/homeassistant/components/spc/binary_sensor.py +++ b/homeassistant/components/spc/binary_sensor.py @@ -4,6 +4,7 @@ import logging from pyspcwebgw.const import ZoneInput, ZoneType from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE, BinarySensorEntity, ) @@ -18,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) def _get_device_class(zone_type): return { ZoneType.ALARM: "motion", - ZoneType.ENTRY_EXIT: "opening", + ZoneType.ENTRY_EXIT: DEVICE_CLASS_OPENING, ZoneType.FIRE: DEVICE_CLASS_SMOKE, ZoneType.TECHNICAL: "power", }.get(zone_type) diff --git a/homeassistant/components/wink/binary_sensor.py b/homeassistant/components/wink/binary_sensor.py index d4443f31ab6..742ed9d020d 100644 --- a/homeassistant/components/wink/binary_sensor.py +++ b/homeassistant/components/wink/binary_sensor.py @@ -4,6 +4,8 @@ import logging import pywink from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, DEVICE_CLASS_VIBRATION, @@ -24,8 +26,8 @@ SENSOR_TYPES = { "loudness": DEVICE_CLASS_SOUND, "motion": "motion", "noise": DEVICE_CLASS_SOUND, - "opened": "opening", - "presence": "occupancy", + "opened": DEVICE_CLASS_OPENING, + "presence": DEVICE_CLASS_OCCUPANCY, "smoke_detected": DEVICE_CLASS_SMOKE, "vibration": DEVICE_CLASS_VIBRATION, } diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py index fecdfd91a75..f8d1622ae92 100644 --- a/homeassistant/components/xiaomi_aqara/binary_sensor.py +++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py @@ -1,7 +1,10 @@ """Support for Xiaomi aqara binary sensors.""" import logging -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OPENING, + BinarySensorEntity, +) from homeassistant.core import callback from homeassistant.helpers.event import async_call_later @@ -299,7 +302,7 @@ class XiaomiDoorSensor(XiaomiBinarySensor): "Door Window Sensor", xiaomi_hub, data_key, - "opening", + DEVICE_CLASS_OPENING, config_entry, ) From 18e9e262e4b863a910c320d7640a4968ecb05a73 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 12 Sep 2020 19:33:44 +0200 Subject: [PATCH 105/514] Upgrade pytest-timeout to 1.4.2 (#39983) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 7958477f2ef..ffcdc16a0ad 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -19,7 +19,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.10.1 pytest-test-groups==1.0.3 pytest-sugar==0.9.4 -pytest-timeout==1.3.4 +pytest-timeout==1.4.2 pytest-xdist==1.32.0 pytest==5.4.3 requests_mock==1.8.0 From 6751a38b8e91a6e1086f2bfb38bef874effa0359 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 12 Sep 2020 19:40:10 +0200 Subject: [PATCH 106/514] Upgrade responses to 0.12.0 (#39986) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index ffcdc16a0ad..f6e2a2433a1 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -23,6 +23,6 @@ pytest-timeout==1.4.2 pytest-xdist==1.32.0 pytest==5.4.3 requests_mock==1.8.0 -responses==0.10.6 +responses==0.12.0 stdlib-list==0.7.0 tqdm==4.48.2 From 3d4ef8cfe1b254d132cfd312f8460699da77ef20 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 12 Sep 2020 20:21:57 +0200 Subject: [PATCH 107/514] Use connectivity device class constant in various integrations (#39972) --- .../components/ambient_station/__init__.py | 21 ++++++++++--------- .../components/cloud/binary_sensor.py | 7 +++++-- .../components/guardian/binary_sensor.py | 7 +++++-- .../components/hikvision/binary_sensor.py | 10 ++++++--- .../components/nest/binary_sensor.py | 3 ++- .../components/notion/binary_sensor.py | 3 ++- .../components/ping/binary_sensor.py | 9 +++++--- .../components/point/binary_sensor.py | 8 +++++-- .../components/uptimerobot/binary_sensor.py | 8 +++++-- .../components/zoneminder/binary_sensor.py | 7 +++++-- 10 files changed, 55 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 9428449dc75..f8b2cd7348d 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -6,6 +6,7 @@ from aioambient import Client from aioambient.errors import WebsocketError import voluptuous as vol +from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_LOCATION, @@ -175,16 +176,16 @@ SENSOR_TYPES = { 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), - TYPE_RELAY10: ("Relay 10", None, TYPE_BINARY_SENSOR, "connectivity"), - TYPE_RELAY1: ("Relay 1", None, TYPE_BINARY_SENSOR, "connectivity"), - TYPE_RELAY2: ("Relay 2", None, TYPE_BINARY_SENSOR, "connectivity"), - TYPE_RELAY3: ("Relay 3", None, TYPE_BINARY_SENSOR, "connectivity"), - TYPE_RELAY4: ("Relay 4", None, TYPE_BINARY_SENSOR, "connectivity"), - TYPE_RELAY5: ("Relay 5", None, TYPE_BINARY_SENSOR, "connectivity"), - TYPE_RELAY6: ("Relay 6", None, TYPE_BINARY_SENSOR, "connectivity"), - 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_RELAY10: ("Relay 10", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY1: ("Relay 1", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY2: ("Relay 2", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY3: ("Relay 3", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY4: ("Relay 4", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY5: ("Relay 5", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY6: ("Relay 6", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY7: ("Relay 7", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY8: ("Relay 8", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY9: ("Relay 9", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), 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"), diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py index baa63679d42..0e3b20fa011 100644 --- a/homeassistant/components/cloud/binary_sensor.py +++ b/homeassistant/components/cloud/binary_sensor.py @@ -1,7 +1,10 @@ """Support for Home Assistant Cloud binary sensors.""" import asyncio -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + BinarySensorEntity, +) from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN @@ -44,7 +47,7 @@ class CloudRemoteBinary(BinarySensorEntity): @property def device_class(self) -> str: """Return the class of this device, from component DEVICE_CLASSES.""" - return "connectivity" + return DEVICE_CLASS_CONNECTIVITY @property def available(self) -> bool: diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index c63d80163bc..4d8c429a9d1 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -3,7 +3,10 @@ from typing import Callable, Dict from aioguardian import Client -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + BinarySensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -22,7 +25,7 @@ ATTR_CONNECTED_CLIENTS = "connected_clients" SENSOR_KIND_AP_INFO = "ap_enabled" SENSOR_KIND_LEAK_DETECTED = "leak_detected" SENSORS = [ - (SENSOR_KIND_AP_INFO, "Onboard AP Enabled", "connectivity"), + (SENSOR_KIND_AP_INFO, "Onboard AP Enabled", DEVICE_CLASS_CONNECTIVITY), (SENSOR_KIND_LEAK_DETECTED, "Leak Detected", "moisture"), ] diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py index 779afa10cca..f10a11b317b 100644 --- a/homeassistant/components/hikvision/binary_sensor.py +++ b/homeassistant/components/hikvision/binary_sensor.py @@ -5,7 +5,11 @@ import logging from pyhik.hikvision import HikCamera import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import ( ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE, @@ -42,8 +46,8 @@ DEVICE_CLASS_MAP = { "Shelter Alarm": None, "Disk Full": None, "Disk Error": None, - "Net Interface Broken": "connectivity", - "IP Conflict": "connectivity", + "Net Interface Broken": DEVICE_CLASS_CONNECTIVITY, + "IP Conflict": DEVICE_CLASS_CONNECTIVITY, "Illegal Access": None, "Video Mismatch": None, "Bad Video": None, diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py index 35acab6f504..31603f37cc2 100644 --- a/homeassistant/components/nest/binary_sensor.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -3,6 +3,7 @@ from itertools import chain import logging from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_SOUND, BinarySensorEntity, @@ -13,7 +14,7 @@ from . import CONF_BINARY_SENSORS, DATA_NEST, DATA_NEST_CONFIG, NestSensorDevice _LOGGER = logging.getLogger(__name__) -BINARY_TYPES = {"online": "connectivity"} +BINARY_TYPES = {"online": DEVICE_CLASS_CONNECTIVITY} CLIMATE_BINARY_TYPES = { "fan": None, diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index 3d02e364021..5bae2592b6c 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -3,6 +3,7 @@ import logging from typing import Callable from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_DOOR, DEVICE_CLASS_SMOKE, DEVICE_CLASS_WINDOW, @@ -34,7 +35,7 @@ BINARY_SENSOR_TYPES = { SENSOR_DOOR: ("Door", DEVICE_CLASS_DOOR), SENSOR_GARAGE_DOOR: ("Garage Door", "garage_door"), SENSOR_LEAK: ("Leak Detector", "moisture"), - SENSOR_MISSING: ("Missing", "connectivity"), + SENSOR_MISSING: ("Missing", DEVICE_CLASS_CONNECTIVITY), SENSOR_SAFE: ("Safe", DEVICE_CLASS_DOOR), SENSOR_SLIDING: ("Sliding Door/Window", DEVICE_CLASS_DOOR), SENSOR_SMOKE_CO: ("Smoke/Carbon Monoxide Detector", DEVICE_CLASS_SMOKE), diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index ac73da0a13f..df93aaaf5f1 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -10,7 +10,11 @@ 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 +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + 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 @@ -30,7 +34,6 @@ CONF_PING_COUNT = "count" DEFAULT_NAME = "Ping" DEFAULT_PING_COUNT = 5 -DEFAULT_DEVICE_CLASS = "connectivity" SCAN_INTERVAL = timedelta(minutes=5) @@ -94,7 +97,7 @@ class PingBinarySensor(BinarySensorEntity): @property def device_class(self) -> str: """Return the class of this sensor.""" - return DEFAULT_DEVICE_CLASS + return DEVICE_CLASS_CONNECTIVITY @property def is_on(self) -> bool: diff --git a/homeassistant/components/point/binary_sensor.py b/homeassistant/components/point/binary_sensor.py index 5a780c2e57a..d82ecd096ee 100644 --- a/homeassistant/components/point/binary_sensor.py +++ b/homeassistant/components/point/binary_sensor.py @@ -1,7 +1,11 @@ """Support for Minut Point binary sensors.""" import logging -from homeassistant.components.binary_sensor import DOMAIN, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + DOMAIN, + BinarySensorEntity, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -116,7 +120,7 @@ class MinutPointBinarySensor(MinutPointEntity, BinarySensorEntity): @property def is_on(self): """Return the state of the binary sensor.""" - if self.device_class == "connectivity": + if self.device_class == DEVICE_CLASS_CONNECTIVITY: # connectivity is the other way around. return not self._is_on return self._is_on diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index 231dce9a402..e31d8b44b10 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -4,7 +4,11 @@ import logging from pyuptimerobot import UptimeRobot import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY import homeassistant.helpers.config_validation as cv @@ -68,7 +72,7 @@ class UptimeRobotBinarySensor(BinarySensorEntity): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return "connectivity" + return DEVICE_CLASS_CONNECTIVITY @property def device_state_attributes(self): diff --git a/homeassistant/components/zoneminder/binary_sensor.py b/homeassistant/components/zoneminder/binary_sensor.py index 739864fdea8..73d6877ef2d 100644 --- a/homeassistant/components/zoneminder/binary_sensor.py +++ b/homeassistant/components/zoneminder/binary_sensor.py @@ -1,5 +1,8 @@ """Support for ZoneMinder binary sensors.""" -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + BinarySensorEntity, +) from . import DOMAIN as ZONEMINDER_DOMAIN @@ -35,7 +38,7 @@ class ZMAvailabilitySensor(BinarySensorEntity): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return "connectivity" + return DEVICE_CLASS_CONNECTIVITY def update(self): """Update the state of this sensor (availability of ZoneMinder).""" From 827711bcd153279ec56527927eaba4815bcde1d4 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 12 Sep 2020 20:23:24 +0200 Subject: [PATCH 108/514] Use problem, presence and plug device class constants in various integrations (#39973) --- .../bmw_connected_drive/binary_sensor.py | 16 +++++++++++++--- homeassistant/components/opentherm_gw/const.py | 2 +- .../components/smappee/binary_sensor.py | 9 ++++++--- .../components/smartthings/binary_sensor.py | 8 +++++--- homeassistant/components/smarty/binary_sensor.py | 13 ++++++++++--- 5 files changed, 35 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 5cc3964ccee..31ef2dacf3a 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -5,6 +5,8 @@ from bimmer_connected.state import ChargingState, LockState from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OPENING, + DEVICE_CLASS_PLUG, + DEVICE_CLASS_PROBLEM, BinarySensorEntity, ) from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_KILOMETERS @@ -19,13 +21,21 @@ SENSOR_TYPES = { "windows": ["Windows", DEVICE_CLASS_OPENING, "mdi:car-door"], "door_lock_state": ["Door lock state", "lock", "mdi:car-key"], "lights_parking": ["Parking lights", "light", "mdi:car-parking-lights"], - "condition_based_services": ["Condition based services", "problem", "mdi:wrench"], - "check_control_messages": ["Control messages", "problem", "mdi:car-tire-alert"], + "condition_based_services": [ + "Condition based services", + DEVICE_CLASS_PROBLEM, + "mdi:wrench", + ], + "check_control_messages": [ + "Control messages", + DEVICE_CLASS_PROBLEM, + "mdi:car-tire-alert", + ], } SENSOR_TYPES_ELEC = { "charging_status": ["Charging status", "power", "mdi:ev-station"], - "connection_status": ["Connection status", "plug", "mdi:car-electric"], + "connection_status": ["Connection status", DEVICE_CLASS_PLUG, "mdi:car-electric"], } SENSOR_TYPES_ELEC.update(SENSOR_TYPES) diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index 0b696ed9339..a32a2199760 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -1,6 +1,7 @@ """Constants for the opentherm_gw integration.""" import pyotgw.vars as gw_vars +from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, PERCENTAGE, @@ -23,7 +24,6 @@ DATA_OPENTHERM_GW = "opentherm_gw" DEVICE_CLASS_COLD = "cold" DEVICE_CLASS_HEAT = "heat" -DEVICE_CLASS_PROBLEM = "problem" DOMAIN = "opentherm_gw" diff --git a/homeassistant/components/smappee/binary_sensor.py b/homeassistant/components/smappee/binary_sensor.py index ecc00f12370..49d21f2b2c1 100644 --- a/homeassistant/components/smappee/binary_sensor.py +++ b/homeassistant/components/smappee/binary_sensor.py @@ -1,7 +1,10 @@ """Support for monitoring a Smappee appliance binary sensor.""" import logging -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_PRESENCE, + BinarySensorEntity, +) from .const import DOMAIN @@ -58,7 +61,7 @@ class SmappeePresence(BinarySensorEntity): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return "presence" + return DEVICE_CLASS_PRESENCE @property def unique_id( @@ -68,7 +71,7 @@ class SmappeePresence(BinarySensorEntity): return ( f"{self._service_location.device_serial_number}-" f"{self._service_location.service_location_id}-" - f"presence" + f"{DEVICE_CLASS_PRESENCE}" ) @property diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index baeac063598..b51cc06b617 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -5,6 +5,8 @@ from pysmartthings import Attribute, Capability from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OPENING, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_PROBLEM, DEVICE_CLASS_SOUND, BinarySensorEntity, ) @@ -26,11 +28,11 @@ CAPABILITY_TO_ATTRIB = { ATTRIB_TO_CLASS = { Attribute.acceleration: "moving", Attribute.contact: DEVICE_CLASS_OPENING, - Attribute.filter_status: "problem", + Attribute.filter_status: DEVICE_CLASS_PROBLEM, Attribute.motion: "motion", - Attribute.presence: "presence", + Attribute.presence: DEVICE_CLASS_PRESENCE, Attribute.sound: DEVICE_CLASS_SOUND, - Attribute.tamper: "problem", + Attribute.tamper: DEVICE_CLASS_PROBLEM, Attribute.valve: DEVICE_CLASS_OPENING, Attribute.water: "moisture", } diff --git a/homeassistant/components/smarty/binary_sensor.py b/homeassistant/components/smarty/binary_sensor.py index f8b9114ae0e..965102f07f7 100644 --- a/homeassistant/components/smarty/binary_sensor.py +++ b/homeassistant/components/smarty/binary_sensor.py @@ -2,7 +2,10 @@ import logging -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_PROBLEM, + BinarySensorEntity, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -83,7 +86,9 @@ class AlarmSensor(SmartyBinarySensor): def __init__(self, name, smarty): """Alarm Sensor Init.""" - super().__init__(name=f"{name} Alarm", device_class="problem", smarty=smarty) + super().__init__( + name=f"{name} Alarm", device_class=DEVICE_CLASS_PROBLEM, smarty=smarty + ) def update(self) -> None: """Update state.""" @@ -96,7 +101,9 @@ class WarningSensor(SmartyBinarySensor): def __init__(self, name, smarty): """Warning Sensor Init.""" - super().__init__(name=f"{name} Warning", device_class="problem", smarty=smarty) + super().__init__( + name=f"{name} Warning", device_class=DEVICE_CLASS_PROBLEM, smarty=smarty + ) def update(self) -> None: """Update state.""" From b0ba0e77f87fb7e0970014df2f3ba13b25249426 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 109/514] 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 ca26c8bbd7190b7ed5b26650878533df57a0bac7 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 12 Sep 2020 22:31:01 +0200 Subject: [PATCH 110/514] Shelly: Power and Energy sensors in roller mode (#39709) --- homeassistant/components/shelly/sensor.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 8a24a6380ed..52c2a1b5228 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -58,6 +58,12 @@ SENSORS = { value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_POWER, ), + ("roller", "rollerPower"): 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, @@ -83,6 +89,12 @@ SENSORS = { value=lambda value: round(value / 60 / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, ), + ("roller", "rollerEnergy"): 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, From 02cb592917c833ea2a1833c7ae8c47d8e4ec6ee5 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 111/514] 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 255c1c23f39..433da7ae65d 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 0b34d1f3333..c973a8b0467 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 e55035b2f96da2f92296ef9c298609503db1fc9d Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 12 Sep 2020 23:20:30 +0200 Subject: [PATCH 112/514] Use DEVICE_CLASS_MOTION in various integrations (#39962) --- .../android_ip_webcam/binary_sensor.py | 7 ++++-- .../components/concord232/binary_sensor.py | 3 ++- .../components/demo/binary_sensor.py | 9 ++++++-- .../components/egardia/binary_sensor.py | 5 ++-- .../components/ffmpeg_motion/binary_sensor.py | 8 +++++-- .../components/fibaro/binary_sensor.py | 5 ++-- .../components/hikvision/binary_sensor.py | 23 ++++++++++--------- .../components/hive/binary_sensor.py | 3 ++- .../components/mysensors/binary_sensor.py | 3 ++- .../components/nest/binary_sensor.py | 5 ++-- .../components/ring/binary_sensor.py | 7 +++++- .../components/skybell/binary_sensor.py | 3 ++- .../components/smartthings/binary_sensor.py | 3 ++- homeassistant/components/spc/binary_sensor.py | 3 ++- .../components/wink/binary_sensor.py | 3 ++- 15 files changed, 59 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/android_ip_webcam/binary_sensor.py b/homeassistant/components/android_ip_webcam/binary_sensor.py index 14384565718..377ecfec667 100644 --- a/homeassistant/components/android_ip_webcam/binary_sensor.py +++ b/homeassistant/components/android_ip_webcam/binary_sensor.py @@ -1,5 +1,8 @@ """Support for Android IP Webcam binary sensors.""" -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, + BinarySensorEntity, +) from . import CONF_HOST, CONF_NAME, DATA_IP_WEBCAM, KEY_MAP, AndroidIPCamEntity @@ -47,4 +50,4 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorEntity): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return "motion" + return DEVICE_CLASS_MOTION diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 3b2e980b777..1288b9472ed 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -7,6 +7,7 @@ import requests import voluptuous as vol from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, DEVICE_CLASS_OPENING, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, @@ -88,7 +89,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def get_opening_type(zone): """Return the result of the type guessing from name.""" if "MOTION" in zone["name"]: - return "motion" + return DEVICE_CLASS_MOTION if "KEY" in zone["name"]: return DEVICE_CLASS_SAFETY if "SMOKE" in zone["name"]: diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index 04d8e72f9a8..26a7ca98a7e 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -1,5 +1,8 @@ """Demo platform that has two fake binary sensors.""" -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, + BinarySensorEntity, +) from . import DOMAIN @@ -9,7 +12,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities( [ DemoBinarySensor("binary_1", "Basement Floor Wet", False, "moisture"), - DemoBinarySensor("binary_2", "Movement Backyard", True, "motion"), + DemoBinarySensor( + "binary_2", "Movement Backyard", True, DEVICE_CLASS_MOTION + ), ] ) diff --git a/homeassistant/components/egardia/binary_sensor.py b/homeassistant/components/egardia/binary_sensor.py index f4bb5097625..6882171b67f 100644 --- a/homeassistant/components/egardia/binary_sensor.py +++ b/homeassistant/components/egardia/binary_sensor.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, DEVICE_CLASS_OPENING, BinarySensorEntity, ) @@ -12,9 +13,9 @@ from . import ATTR_DISCOVER_DEVICES, EGARDIA_DEVICE _LOGGER = logging.getLogger(__name__) EGARDIA_TYPE_TO_DEVICE_CLASS = { - "IR Sensor": "motion", + "IR Sensor": DEVICE_CLASS_MOTION, "Door Contact": DEVICE_CLASS_OPENING, - "IR": "motion", + "IR": DEVICE_CLASS_MOTION, } diff --git a/homeassistant/components/ffmpeg_motion/binary_sensor.py b/homeassistant/components/ffmpeg_motion/binary_sensor.py index a8842f9c401..9b4218c011c 100644 --- a/homeassistant/components/ffmpeg_motion/binary_sensor.py +++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py @@ -4,7 +4,11 @@ import logging import haffmpeg.sensor as ffmpeg_sensor import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, + PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.components.ffmpeg import ( CONF_EXTRA_ARGUMENTS, CONF_INITIAL_STATE, @@ -119,4 +123,4 @@ class FFmpegMotion(FFmpegBinarySensor): @property def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" - return "motion" + return DEVICE_CLASS_MOTION diff --git a/homeassistant/components/fibaro/binary_sensor.py b/homeassistant/components/fibaro/binary_sensor.py index 42783df9df6..21ce22ea8e3 100644 --- a/homeassistant/components/fibaro/binary_sensor.py +++ b/homeassistant/components/fibaro/binary_sensor.py @@ -3,6 +3,7 @@ import logging from homeassistant.components.binary_sensor import ( DEVICE_CLASS_DOOR, + DEVICE_CLASS_MOTION, DEVICE_CLASS_SMOKE, DEVICE_CLASS_WINDOW, DOMAIN, @@ -16,11 +17,11 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { "com.fibaro.floodSensor": ["Flood", "mdi:water", "flood"], - "com.fibaro.motionSensor": ["Motion", "mdi:run", "motion"], + "com.fibaro.motionSensor": ["Motion", "mdi:run", DEVICE_CLASS_MOTION], "com.fibaro.doorSensor": ["Door", "mdi:window-open", DEVICE_CLASS_DOOR], "com.fibaro.windowSensor": ["Window", "mdi:window-open", DEVICE_CLASS_WINDOW], "com.fibaro.smokeSensor": ["Smoke", "mdi:smoking", DEVICE_CLASS_SMOKE], - "com.fibaro.FGMS001": ["Motion", "mdi:run", "motion"], + "com.fibaro.FGMS001": ["Motion", "mdi:run", DEVICE_CLASS_MOTION], "com.fibaro.heatDetector": ["Heat", "mdi:fire", "heat"], } diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py index f10a11b317b..c0da593a039 100644 --- a/homeassistant/components/hikvision/binary_sensor.py +++ b/homeassistant/components/hikvision/binary_sensor.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_MOTION, PLATFORM_SCHEMA, BinarySensorEntity, ) @@ -38,11 +39,11 @@ DEFAULT_DELAY = 0 ATTR_DELAY = "delay" DEVICE_CLASS_MAP = { - "Motion": "motion", - "Line Crossing": "motion", - "Field Detection": "motion", + "Motion": DEVICE_CLASS_MOTION, + "Line Crossing": DEVICE_CLASS_MOTION, + "Field Detection": DEVICE_CLASS_MOTION, "Video Loss": None, - "Tamper Detection": "motion", + "Tamper Detection": DEVICE_CLASS_MOTION, "Shelter Alarm": None, "Disk Full": None, "Disk Error": None, @@ -51,15 +52,15 @@ DEVICE_CLASS_MAP = { "Illegal Access": None, "Video Mismatch": None, "Bad Video": None, - "PIR Alarm": "motion", - "Face Detection": "motion", - "Scene Change Detection": "motion", + "PIR Alarm": DEVICE_CLASS_MOTION, + "Face Detection": DEVICE_CLASS_MOTION, + "Scene Change Detection": DEVICE_CLASS_MOTION, "I/O": None, - "Unattended Baggage": "motion", - "Attended Baggage": "motion", + "Unattended Baggage": DEVICE_CLASS_MOTION, + "Attended Baggage": DEVICE_CLASS_MOTION, "Recording Failure": None, - "Exiting Region": "motion", - "Entering Region": "motion", + "Exiting Region": DEVICE_CLASS_MOTION, + "Entering Region": DEVICE_CLASS_MOTION, } CUSTOMIZE_SCHEMA = vol.Schema( diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index bc71939bf6a..120148a8f81 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -1,5 +1,6 @@ """Support for the Hive binary sensors.""" from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, DEVICE_CLASS_OPENING, BinarySensorEntity, ) @@ -7,7 +8,7 @@ from homeassistant.components.binary_sensor import ( from . import DATA_HIVE, DOMAIN, HiveEntity DEVICETYPE_DEVICE_CLASS = { - "motionsensor": "motion", + "motionsensor": DEVICE_CLASS_MOTION, "contactsensor": DEVICE_CLASS_OPENING, } diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py index 406f82c845e..fc9d0263441 100644 --- a/homeassistant/components/mysensors/binary_sensor.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -1,6 +1,7 @@ """Support for MySensors binary sensors.""" from homeassistant.components import mysensors from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SOUND, DEVICE_CLASS_VIBRATION, @@ -12,7 +13,7 @@ from homeassistant.const import STATE_ON SENSORS = { "S_DOOR": "door", - "S_MOTION": "motion", + "S_MOTION": DEVICE_CLASS_MOTION, "S_SMOKE": "smoke", "S_SPRINKLER": DEVICE_CLASS_SAFETY, "S_WATER_LEAK": DEVICE_CLASS_SAFETY, diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py index 31603f37cc2..0e9198a0220 100644 --- a/homeassistant/components/nest/binary_sensor.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -4,6 +4,7 @@ import logging from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_MOTION, DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_SOUND, BinarySensorEntity, @@ -24,7 +25,7 @@ CLIMATE_BINARY_TYPES = { } CAMERA_BINARY_TYPES = { - "motion_detected": "motion", + "motion_detected": DEVICE_CLASS_MOTION, "sound_detected": DEVICE_CLASS_SOUND, "person_detected": DEVICE_CLASS_OCCUPANCY, } @@ -158,7 +159,7 @@ class NestActivityZoneSensor(NestBinarySensor): @property def device_class(self): """Return the device class of the binary sensor.""" - return "motion" + return DEVICE_CLASS_MOTION def update(self): """Retrieve latest state.""" diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 876caad3f85..ccec9e6ad36 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -3,6 +3,7 @@ from datetime import datetime import logging from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, DEVICE_CLASS_OCCUPANCY, BinarySensorEntity, ) @@ -16,7 +17,11 @@ _LOGGER = logging.getLogger(__name__) # Sensor types: Name, category, device_class SENSOR_TYPES = { "ding": ["Ding", ["doorbots", "authorized_doorbots"], DEVICE_CLASS_OCCUPANCY], - "motion": ["Motion", ["doorbots", "authorized_doorbots", "stickup_cams"], "motion"], + "motion": [ + "Motion", + ["doorbots", "authorized_doorbots", "stickup_cams"], + DEVICE_CLASS_MOTION, + ], } diff --git a/homeassistant/components/skybell/binary_sensor.py b/homeassistant/components/skybell/binary_sensor.py index 38cf0a98f40..94f64a4eb43 100644 --- a/homeassistant/components/skybell/binary_sensor.py +++ b/homeassistant/components/skybell/binary_sensor.py @@ -5,6 +5,7 @@ import logging import voluptuous as vol from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, DEVICE_CLASS_OCCUPANCY, PLATFORM_SCHEMA, BinarySensorEntity, @@ -21,7 +22,7 @@ SCAN_INTERVAL = timedelta(seconds=5) # Sensor types: Name, device_class, event SENSOR_TYPES = { "button": ["Button", DEVICE_CLASS_OCCUPANCY, "device:sensor:button"], - "motion": ["Motion", "motion", "device:sensor:motion"], + "motion": ["Motion", DEVICE_CLASS_MOTION, "device:sensor:motion"], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index b51cc06b617..47ca6879701 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -4,6 +4,7 @@ from typing import Optional, Sequence from pysmartthings import Attribute, Capability from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, DEVICE_CLASS_OPENING, DEVICE_CLASS_PRESENCE, DEVICE_CLASS_PROBLEM, @@ -29,7 +30,7 @@ ATTRIB_TO_CLASS = { Attribute.acceleration: "moving", Attribute.contact: DEVICE_CLASS_OPENING, Attribute.filter_status: DEVICE_CLASS_PROBLEM, - Attribute.motion: "motion", + Attribute.motion: DEVICE_CLASS_MOTION, Attribute.presence: DEVICE_CLASS_PRESENCE, Attribute.sound: DEVICE_CLASS_SOUND, Attribute.tamper: DEVICE_CLASS_PROBLEM, diff --git a/homeassistant/components/spc/binary_sensor.py b/homeassistant/components/spc/binary_sensor.py index 4ee8ffad77e..1fda59207ec 100644 --- a/homeassistant/components/spc/binary_sensor.py +++ b/homeassistant/components/spc/binary_sensor.py @@ -4,6 +4,7 @@ import logging from pyspcwebgw.const import ZoneInput, ZoneType from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE, BinarySensorEntity, @@ -18,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) def _get_device_class(zone_type): return { - ZoneType.ALARM: "motion", + ZoneType.ALARM: DEVICE_CLASS_MOTION, ZoneType.ENTRY_EXIT: DEVICE_CLASS_OPENING, ZoneType.FIRE: DEVICE_CLASS_SMOKE, ZoneType.TECHNICAL: "power", diff --git a/homeassistant/components/wink/binary_sensor.py b/homeassistant/components/wink/binary_sensor.py index 742ed9d020d..04117758c54 100644 --- a/homeassistant/components/wink/binary_sensor.py +++ b/homeassistant/components/wink/binary_sensor.py @@ -4,6 +4,7 @@ import logging import pywink from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE, @@ -24,7 +25,7 @@ SENSOR_TYPES = { "co_detected": "gas", "liquid_detected": "moisture", "loudness": DEVICE_CLASS_SOUND, - "motion": "motion", + "motion": DEVICE_CLASS_MOTION, "noise": DEVICE_CLASS_SOUND, "opened": DEVICE_CLASS_OPENING, "presence": DEVICE_CLASS_OCCUPANCY, From 285408b46c9ae20084bd37ba6da4b676f3c7367d Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sat, 12 Sep 2020 14:53:41 -0700 Subject: [PATCH 113/514] 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 433da7ae65d..3168fc2615c 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 c973a8b0467..659af5d6686 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 13df452dd4c1641dcf96ac67b0af37ff7d14a805 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sun, 13 Sep 2020 01:13:57 +0200 Subject: [PATCH 114/514] 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 b05c88f1bc6eb4c0a57419abfefa724c6c9d4228 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 13 Sep 2020 00:04:18 +0000 Subject: [PATCH 115/514] [ci skip] Translation update --- .../almond/translations/zh-Hant.json | 3 ++- .../home_connect/translations/zh-Hant.json | 3 ++- .../homekit_controller/translations/ru.json | 20 +++++++++++++++++++ .../translations/zh-Hant.json | 20 +++++++++++++++++++ .../netatmo/translations/zh-Hant.json | 1 + .../smappee/translations/zh-Hant.json | 3 ++- .../somfy/translations/zh-Hant.json | 3 ++- .../spotify/translations/zh-Hant.json | 1 + .../components/toon/translations/zh-Hant.json | 3 ++- .../withings/translations/zh-Hant.json | 3 ++- 10 files changed, 54 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/almond/translations/zh-Hant.json b/homeassistant/components/almond/translations/zh-Hant.json index 96e3d92e060..d5ea73a873d 100644 --- a/homeassistant/components/almond/translations/zh-Hant.json +++ b/homeassistant/components/almond/translations/zh-Hant.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Almond \u5e33\u865f\u3002", "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Almond \u4f3a\u670d\u5668\u3002", - "missing_configuration": "\u8acb\u53c3\u8003\u76f8\u95dc\u6587\u4ef6\u4ee5\u4e86\u89e3\u5982\u4f55\u8a2d\u5b9a Almond\u3002" + "missing_configuration": "\u8acb\u53c3\u8003\u76f8\u95dc\u6587\u4ef6\u4ee5\u4e86\u89e3\u5982\u4f55\u8a2d\u5b9a Almond\u3002", + "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/home_connect/translations/zh-Hant.json b/homeassistant/components/home_connect/translations/zh-Hant.json index 5132dedd515..1d68766d2c7 100644 --- a/homeassistant/components/home_connect/translations/zh-Hant.json +++ b/homeassistant/components/home_connect/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "missing_configuration": "Home Connect \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + "missing_configuration": "Home Connect \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Home Connect \u8a2d\u5099\u3002" diff --git a/homeassistant/components/homekit_controller/translations/ru.json b/homeassistant/components/homekit_controller/translations/ru.json index fb9612ef981..62120d98459 100644 --- a/homeassistant/components/homekit_controller/translations/ru.json +++ b/homeassistant/components/homekit_controller/translations/ru.json @@ -53,5 +53,25 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button1": "\u041a\u043d\u043e\u043f\u043a\u0430 1", + "button10": "\u041a\u043d\u043e\u043f\u043a\u0430 10", + "button2": "\u041a\u043d\u043e\u043f\u043a\u0430 2", + "button3": "\u041a\u043d\u043e\u043f\u043a\u0430 3", + "button4": "\u041a\u043d\u043e\u043f\u043a\u0430 4", + "button5": "\u041a\u043d\u043e\u043f\u043a\u0430 5", + "button6": "\u041a\u043d\u043e\u043f\u043a\u0430 6", + "button7": "\u041a\u043d\u043e\u043f\u043a\u0430 7", + "button8": "\u041a\u043d\u043e\u043f\u043a\u0430 8", + "button9": "\u041a\u043d\u043e\u043f\u043a\u0430 9", + "doorbell": "\u0414\u0432\u0435\u0440\u043d\u043e\u0439 \u0437\u0432\u043e\u043d\u043e\u043a" + }, + "trigger_type": { + "double_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430\u0436\u0434\u044b", + "long_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0438 \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f", + "single_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430" + } + }, "title": "HomeKit Controller" } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/zh-Hant.json b/homeassistant/components/homekit_controller/translations/zh-Hant.json index 51f521d5c35..d70aa1b79b5 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hant.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hant.json @@ -53,5 +53,25 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button1": "\u6309\u9215 1", + "button10": "\u6309\u9215 10", + "button2": "\u6309\u9215 2", + "button3": "\u6309\u9215 3", + "button4": "\u6309\u9215 4", + "button5": "\u6309\u9215 5", + "button6": "\u6309\u9215 6", + "button7": "\u6309\u9215 7", + "button8": "\u6309\u9215 8", + "button9": "\u6309\u9215 9", + "doorbell": "\u9580\u9234" + }, + "trigger_type": { + "double_press": "\"{subtype}\" \u6309\u4e0b\u5169\u6b21", + "long_press": "\"{subtype}\" \u6309\u4e0b\u4e26\u6309\u4f4f", + "single_press": "\"{subtype}\" \u6309\u4e0b" + } + }, "title": "HomeKit \u63a7\u5236\u5668" } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/zh-Hant.json b/homeassistant/components/netatmo/translations/zh-Hant.json index fd528ed4cfc..588675c670e 100644 --- a/homeassistant/components/netatmo/translations/zh-Hant.json +++ b/homeassistant/components/netatmo/translations/zh-Hant.json @@ -3,6 +3,7 @@ "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", + "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" }, "create_entry": { diff --git a/homeassistant/components/smappee/translations/zh-Hant.json b/homeassistant/components/smappee/translations/zh-Hant.json index 7636ea5b34b..5374c535a12 100644 --- a/homeassistant/components/smappee/translations/zh-Hant.json +++ b/homeassistant/components/smappee/translations/zh-Hant.json @@ -6,7 +6,8 @@ "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" + "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})" }, "flow_title": "Smappee\uff1a{name}", "step": { diff --git a/homeassistant/components/somfy/translations/zh-Hant.json b/homeassistant/components/somfy/translations/zh-Hant.json index b5875aaf088..c0f1aefd157 100644 --- a/homeassistant/components/somfy/translations/zh-Hant.json +++ b/homeassistant/components/somfy/translations/zh-Hant.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Somfy \u5e33\u865f\u3002", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", - "missing_configuration": "Somfy \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + "missing_configuration": "Somfy \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Somfy \u8a2d\u5099\u3002" diff --git a/homeassistant/components/spotify/translations/zh-Hant.json b/homeassistant/components/spotify/translations/zh-Hant.json index a4fca36205f..d7e52012c36 100644 --- a/homeassistant/components/spotify/translations/zh-Hant.json +++ b/homeassistant/components/spotify/translations/zh-Hant.json @@ -4,6 +4,7 @@ "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", + "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", "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": { diff --git a/homeassistant/components/toon/translations/zh-Hant.json b/homeassistant/components/toon/translations/zh-Hant.json index a6d5227afc6..020938792d6 100644 --- a/homeassistant/components/toon/translations/zh-Hant.json +++ b/homeassistant/components/toon/translations/zh-Hant.json @@ -5,7 +5,8 @@ "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", "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_agreements": "\u6b64\u5e33\u865f\u4e26\u672a\u64c1\u6709 Toon \u986f\u793a\u8a2d\u5099\u3002", + "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})" }, "step": { "agreement": { diff --git a/homeassistant/components/withings/translations/zh-Hant.json b/homeassistant/components/withings/translations/zh-Hant.json index 3f31c0585f8..02e6d6f669c 100644 --- a/homeassistant/components/withings/translations/zh-Hant.json +++ b/homeassistant/components/withings/translations/zh-Hant.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "\u6b64\u500b\u4eba\u8a2d\u7f6e\u8a2d\u5b9a\u5df2\u66f4\u65b0\u3002", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", - "missing_configuration": "Withings \u6574\u5408\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + "missing_configuration": "Withings \u6574\u5408\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Withings \u8a2d\u5099\u3002" From bab4ad4f170bc7c7f703636e3b000c58e7405acc Mon Sep 17 00:00:00 2001 From: Quentame Date: Sun, 13 Sep 2020 03:36:39 +0200 Subject: [PATCH 116/514] Add timeout config option to Synology DSM (#40000) --- homeassistant/components/synology_dsm/__init__.py | 2 ++ homeassistant/components/synology_dsm/config_flow.py | 10 +++++++++- homeassistant/components/synology_dsm/const.py | 1 + homeassistant/components/synology_dsm/strings.json | 3 ++- .../components/synology_dsm/translations/en.json | 3 ++- tests/components/synology_dsm/test_config_flow.py | 6 +++++- 6 files changed, 21 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 223235a4121..6dcac767e5c 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_PORT, CONF_SCAN_INTERVAL, CONF_SSL, + CONF_TIMEOUT, CONF_USERNAME, ) from homeassistant.core import callback @@ -250,6 +251,7 @@ class SynoApi: self._entry.data[CONF_USERNAME], self._entry.data[CONF_PASSWORD], self._entry.data[CONF_SSL], + timeout=self._entry.options.get(CONF_TIMEOUT), device_token=self._entry.data.get("device_token"), ) diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index a9df6f362cc..65be8e64ebe 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_PORT, CONF_SCAN_INTERVAL, CONF_SSL, + CONF_TIMEOUT, CONF_USERNAME, ) from homeassistant.core import callback @@ -34,6 +35,7 @@ from .const import ( DEFAULT_PORT_SSL, DEFAULT_SCAN_INTERVAL, DEFAULT_SSL, + DEFAULT_TIMEOUT, ) from .const import DOMAIN # pylint: disable=unused-import @@ -250,7 +252,13 @@ class SynologyDSMOptionsFlowHandler(config_entries.OptionsFlow): default=self.config_entry.options.get( CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ), - ): cv.positive_int + ): cv.positive_int, + vol.Optional( + CONF_TIMEOUT, + default=self.config_entry.options.get( + CONF_TIMEOUT, DEFAULT_TIMEOUT + ), + ): cv.positive_int, } ) return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 693d8b2cd50..f993e0ece73 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -27,6 +27,7 @@ DEFAULT_PORT = 5000 DEFAULT_PORT_SSL = 5001 # Options DEFAULT_SCAN_INTERVAL = 15 # min +DEFAULT_TIMEOUT = 10 # sec ENTITY_NAME = "name" diff --git a/homeassistant/components/synology_dsm/strings.json b/homeassistant/components/synology_dsm/strings.json index c46f645719f..2bb81f6711d 100644 --- a/homeassistant/components/synology_dsm/strings.json +++ b/homeassistant/components/synology_dsm/strings.json @@ -44,7 +44,8 @@ "step": { "init": { "data": { - "scan_interval": "Minutes between scans" + "scan_interval": "Minutes between scans", + "timeout": "Timeout (seconds)" } } } diff --git a/homeassistant/components/synology_dsm/translations/en.json b/homeassistant/components/synology_dsm/translations/en.json index 48a8118528a..fc78d3cfa66 100644 --- a/homeassistant/components/synology_dsm/translations/en.json +++ b/homeassistant/components/synology_dsm/translations/en.json @@ -44,7 +44,8 @@ "step": { "init": { "data": { - "scan_interval": "Minutes between scans" + "scan_interval": "Minutes between scans", + "timeout": "Timeout (seconds)" } } } diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 64527c70964..738a5bed332 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -19,6 +19,7 @@ from homeassistant.components.synology_dsm.const import ( DEFAULT_PORT_SSL, DEFAULT_SCAN_INTERVAL, DEFAULT_SSL, + DEFAULT_TIMEOUT, DOMAIN, ) from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER @@ -30,6 +31,7 @@ from homeassistant.const import ( CONF_PORT, CONF_SCAN_INTERVAL, CONF_SSL, + CONF_TIMEOUT, CONF_USERNAME, ) from homeassistant.helpers.typing import HomeAssistantType @@ -426,12 +428,14 @@ async def test_options_flow(hass: HomeAssistantType, service: MagicMock): ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options[CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + assert config_entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT # 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}, + user_input={CONF_SCAN_INTERVAL: 2, CONF_TIMEOUT: 30}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options[CONF_SCAN_INTERVAL] == 2 + assert config_entry.options[CONF_TIMEOUT] == 30 From e3c51f0350efb82c2ed027d6792b1496fccc232d Mon Sep 17 00:00:00 2001 From: Xiaonan Shen Date: Sun, 13 Sep 2020 14:44:32 +0800 Subject: [PATCH 117/514] Fix xiaomi_aqara duplicated battery sensors (#39961) --- homeassistant/components/xiaomi_aqara/sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py index 4a6c7ac14fd..576e99eab4b 100644 --- a/homeassistant/components/xiaomi_aqara/sensor.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -86,8 +86,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): _LOGGER.warning("Unmapped Device Model") # Set up battery sensors + seen_sids = set() # Set of device sids that are already seen for devices in gateway.devices.values(): for device in devices: + if device["sid"] in seen_sids: + continue + seen_sids.add(device["sid"]) if device["model"] in BATTERY_MODELS: entities.append( XiaomiBatterySensor(device, "Battery", gateway, config_entry) From eac9c3c4f2f6a813cbf75738403dbb8ce2a6bd81 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 118/514] 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 1d12d4d54cc51822c97f2de74ac1f821fbd25e23 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 119/514] 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 d6bb7042c41..203c8741821 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 3168fc2615c..f8400f590ae 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 659af5d6686..377e5f5df04 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 11d74124cd06f48c1faf68df9a2ab9034130fafa Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 13 Sep 2020 15:38:02 +0200 Subject: [PATCH 120/514] Update azure-pipelines-wheels.yml --- 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 77d90ac94f1..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' From 84578f515d90f3f909a4cf21e6cfeb920ac1f172 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2020 09:12:10 -0500 Subject: [PATCH 121/514] 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 babfac05718..3f9f93450e0 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 ff4bb962c4b61f29310b54568b41e1991332fcda Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2020 09:21:11 -0500 Subject: [PATCH 122/514] Cleanup and reduce duplicate code from recent template changes (#40012) As a result of refactoring, there is duplicate code we can now reduce. Additionally `_wrap_state` can be removed because it had unreachable checks for `None` --- homeassistant/helpers/template.py | 33 +++++++++++-------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index a0512178fdc..79f372e3ff1 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -6,6 +6,7 @@ from functools import wraps import json import logging import math +from operator import attrgetter import random import re from typing import Any, Iterable, List, Optional, Union @@ -424,12 +425,7 @@ class AllStates: def __iter__(self): """Return all states.""" self._collect_all() - return iter( - _wrap_state(self._hass, state) - for state in sorted( - self._hass.states.async_all(), key=lambda state: state.entity_id - ) - ) + return _state_iterator(self._hass, None) def __len__(self) -> int: """Return number of states.""" @@ -469,15 +465,7 @@ class DomainStates: def __iter__(self): """Return the iteration over all the states.""" self._collect_domain() - return iter( - sorted( - ( - _wrap_state(self._hass, state) - for state in self._hass.states.async_all(self._domain) - ), - key=lambda state: state.entity_id, - ) - ) + return _state_iterator(self._hass, self._domain) def __len__(self) -> int: """Return number of states.""" @@ -492,6 +480,8 @@ class DomainStates: class TemplateState(State): """Class to represent a state object in a template.""" + __slots__ = ("_hass", "_state") + # Inheritance is done so functions that check against State keep working # pylint: disable=super-init-not-called def __init__(self, hass, state): @@ -547,11 +537,12 @@ def _collect_state(hass: HomeAssistantType, entity_id: str) -> None: entity_collect.entities.add(entity_id) -def _wrap_state( - hass: HomeAssistantType, state: Optional[State] -) -> Optional[TemplateState]: - """Wrap a state.""" - return None if state is None else TemplateState(hass, state) +def _state_iterator(hass: HomeAssistantType, domain: Optional[str]) -> Iterable: + """Create an state iterator for a domain or all states.""" + return iter( + TemplateState(hass, state) + for state in sorted(hass.states.async_all(domain), key=attrgetter("entity_id")) + ) def _get_state(hass: HomeAssistantType, entity_id: str) -> Optional[TemplateState]: @@ -561,7 +552,7 @@ def _get_state(hass: HomeAssistantType, entity_id: str) -> Optional[TemplateStat # access to the state properties in the state wrapper. _collect_state(hass, entity_id) return None - return _wrap_state(hass, state) + return TemplateState(hass, state) def _resolve_state( From ceeea52915cf99f28311029854cab124810179ac Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 13 Sep 2020 16:31:39 +0200 Subject: [PATCH 123/514] 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 f8400f590ae..e49c2e3e6a6 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 377e5f5df04..5f909427fd0 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 621526bbae04603970747b6a640fff1985edc384 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 13 Sep 2020 16:33:54 +0200 Subject: [PATCH 124/514] Use moisture and moving device class in various integrations (#39963) --- homeassistant/components/ads/binary_sensor.py | 3 ++- homeassistant/components/bloomsky/binary_sensor.py | 8 ++++++-- homeassistant/components/demo/binary_sensor.py | 5 ++++- homeassistant/components/digital_ocean/binary_sensor.py | 9 ++++++--- homeassistant/components/guardian/binary_sensor.py | 3 ++- homeassistant/components/linode/binary_sensor.py | 9 ++++++--- homeassistant/components/mysensors/binary_sensor.py | 3 ++- homeassistant/components/notion/binary_sensor.py | 3 ++- homeassistant/components/smartthings/binary_sensor.py | 6 ++++-- homeassistant/components/wink/binary_sensor.py | 3 ++- homeassistant/components/xiaomi_aqara/binary_sensor.py | 3 ++- 11 files changed, 38 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/ads/binary_sensor.py b/homeassistant/components/ads/binary_sensor.py index df8a74dc1d5..d481420b4d4 100644 --- a/homeassistant/components/ads/binary_sensor.py +++ b/homeassistant/components/ads/binary_sensor.py @@ -4,6 +4,7 @@ import logging import voluptuous as vol from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOVING, DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorEntity, @@ -43,7 +44,7 @@ class AdsBinarySensor(AdsEntity, BinarySensorEntity): def __init__(self, ads_hub, name, ads_var, device_class): """Initialize ADS binary sensor.""" super().__init__(ads_hub, name, ads_var) - self._device_class = device_class or "moving" + self._device_class = device_class or DEVICE_CLASS_MOVING async def async_added_to_hass(self): """Register device notification.""" diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py index 077171006bf..43c5614679c 100644 --- a/homeassistant/components/bloomsky/binary_sensor.py +++ b/homeassistant/components/bloomsky/binary_sensor.py @@ -3,7 +3,11 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOISTURE, + PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv @@ -11,7 +15,7 @@ from . import DOMAIN _LOGGER = logging.getLogger(__name__) -SENSOR_TYPES = {"Rain": "moisture", "Night": None} +SENSOR_TYPES = {"Rain": DEVICE_CLASS_MOISTURE, "Night": None} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index 26a7ca98a7e..c4186eae505 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -1,5 +1,6 @@ """Demo platform that has two fake binary sensors.""" from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, BinarySensorEntity, ) @@ -11,7 +12,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up the Demo binary sensor platform.""" async_add_entities( [ - DemoBinarySensor("binary_1", "Basement Floor Wet", False, "moisture"), + DemoBinarySensor( + "binary_1", "Basement Floor Wet", False, DEVICE_CLASS_MOISTURE + ), DemoBinarySensor( "binary_2", "Movement Backyard", True, DEVICE_CLASS_MOTION ), diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py index d076dae9210..eb1345df45c 100644 --- a/homeassistant/components/digital_ocean/binary_sensor.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -3,7 +3,11 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOVING, + PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import ATTR_ATTRIBUTION import homeassistant.helpers.config_validation as cv @@ -25,7 +29,6 @@ from . import ( _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Droplet" -DEFAULT_DEVICE_CLASS = "moving" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string])} ) @@ -73,7 +76,7 @@ class DigitalOceanBinarySensor(BinarySensorEntity): @property def device_class(self): """Return the class of this sensor.""" - return DEFAULT_DEVICE_CLASS + return DEVICE_CLASS_MOVING @property def device_state_attributes(self): diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index 4d8c429a9d1..7942dba361e 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -5,6 +5,7 @@ from aioguardian import Client from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_MOISTURE, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -26,7 +27,7 @@ SENSOR_KIND_AP_INFO = "ap_enabled" SENSOR_KIND_LEAK_DETECTED = "leak_detected" SENSORS = [ (SENSOR_KIND_AP_INFO, "Onboard AP Enabled", DEVICE_CLASS_CONNECTIVITY), - (SENSOR_KIND_LEAK_DETECTED, "Leak Detected", "moisture"), + (SENSOR_KIND_LEAK_DETECTED, "Leak Detected", DEVICE_CLASS_MOISTURE), ] diff --git a/homeassistant/components/linode/binary_sensor.py b/homeassistant/components/linode/binary_sensor.py index c4a14210d32..bb81a022891 100644 --- a/homeassistant/components/linode/binary_sensor.py +++ b/homeassistant/components/linode/binary_sensor.py @@ -3,7 +3,11 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOVING, + PLATFORM_SCHEMA, + BinarySensorEntity, +) import homeassistant.helpers.config_validation as cv from . import ( @@ -22,7 +26,6 @@ from . import ( _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Node" -DEFAULT_DEVICE_CLASS = "moving" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Required(CONF_NODES): vol.All(cv.ensure_list, [cv.string])} ) @@ -69,7 +72,7 @@ class LinodeBinarySensor(BinarySensorEntity): @property def device_class(self): """Return the class of this sensor.""" - return DEFAULT_DEVICE_CLASS + return DEVICE_CLASS_MOVING @property def device_state_attributes(self): diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py index fc9d0263441..4ec3c6e0abd 100644 --- a/homeassistant/components/mysensors/binary_sensor.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -1,6 +1,7 @@ """Support for MySensors binary sensors.""" from homeassistant.components import mysensors from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SOUND, @@ -19,7 +20,7 @@ SENSORS = { "S_WATER_LEAK": DEVICE_CLASS_SAFETY, "S_SOUND": DEVICE_CLASS_SOUND, "S_VIBRATION": DEVICE_CLASS_VIBRATION, - "S_MOISTURE": "moisture", + "S_MOISTURE": DEVICE_CLASS_MOISTURE, } diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index 5bae2592b6c..3702688eb94 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -5,6 +5,7 @@ from typing import Callable from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_DOOR, + DEVICE_CLASS_MOISTURE, DEVICE_CLASS_SMOKE, DEVICE_CLASS_WINDOW, BinarySensorEntity, @@ -34,7 +35,7 @@ BINARY_SENSOR_TYPES = { SENSOR_BATTERY: ("Low Battery", "battery"), SENSOR_DOOR: ("Door", DEVICE_CLASS_DOOR), SENSOR_GARAGE_DOOR: ("Garage Door", "garage_door"), - SENSOR_LEAK: ("Leak Detector", "moisture"), + SENSOR_LEAK: ("Leak Detector", DEVICE_CLASS_MOISTURE), SENSOR_MISSING: ("Missing", DEVICE_CLASS_CONNECTIVITY), SENSOR_SAFE: ("Safe", DEVICE_CLASS_DOOR), SENSOR_SLIDING: ("Sliding Door/Window", DEVICE_CLASS_DOOR), diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index 47ca6879701..41e915d5c95 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -4,7 +4,9 @@ from typing import Optional, Sequence from pysmartthings import Attribute, Capability from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, DEVICE_CLASS_OPENING, DEVICE_CLASS_PRESENCE, DEVICE_CLASS_PROBLEM, @@ -27,7 +29,7 @@ CAPABILITY_TO_ATTRIB = { Capability.water_sensor: Attribute.water, } ATTRIB_TO_CLASS = { - Attribute.acceleration: "moving", + Attribute.acceleration: DEVICE_CLASS_MOVING, Attribute.contact: DEVICE_CLASS_OPENING, Attribute.filter_status: DEVICE_CLASS_PROBLEM, Attribute.motion: DEVICE_CLASS_MOTION, @@ -35,7 +37,7 @@ ATTRIB_TO_CLASS = { Attribute.sound: DEVICE_CLASS_SOUND, Attribute.tamper: DEVICE_CLASS_PROBLEM, Attribute.valve: DEVICE_CLASS_OPENING, - Attribute.water: "moisture", + Attribute.water: DEVICE_CLASS_MOISTURE, } diff --git a/homeassistant/components/wink/binary_sensor.py b/homeassistant/components/wink/binary_sensor.py index 04117758c54..77ff464a5bf 100644 --- a/homeassistant/components/wink/binary_sensor.py +++ b/homeassistant/components/wink/binary_sensor.py @@ -4,6 +4,7 @@ import logging import pywink from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_OPENING, @@ -23,7 +24,7 @@ SENSOR_TYPES = { "capturing_audio": DEVICE_CLASS_SOUND, "capturing_video": None, "co_detected": "gas", - "liquid_detected": "moisture", + "liquid_detected": DEVICE_CLASS_MOISTURE, "loudness": DEVICE_CLASS_SOUND, "motion": DEVICE_CLASS_MOTION, "noise": DEVICE_CLASS_SOUND, diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py index f8d1622ae92..d63be7824ed 100644 --- a/homeassistant/components/xiaomi_aqara/binary_sensor.py +++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOISTURE, DEVICE_CLASS_OPENING, BinarySensorEntity, ) @@ -352,7 +353,7 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor): "Water Leak Sensor", xiaomi_hub, data_key, - "moisture", + DEVICE_CLASS_MOISTURE, config_entry, ) From f3d50e2104d14dec45d7ad5ac1ffdf1c146e6763 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2020 09:44:37 -0500 Subject: [PATCH 125/514] 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 df93aaaf5f1..afbfe80b43f 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -213,7 +213,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 fc1fb0ab7cbe71137c79b6dfe6f58976bcecf8db Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 13 Sep 2020 17:11:24 +0200 Subject: [PATCH 126/514] Revert 'Use STATE_UNKNOWN constant in dlink and ecobee' (#40022) --- homeassistant/components/dlink/switch.py | 5 ++--- homeassistant/components/ecobee/sensor.py | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index 4fd21f200dd..c173c879ad1 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -13,7 +13,6 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_USERNAME, - STATE_UNKNOWN, TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv @@ -146,14 +145,14 @@ class SmartPlugData: _LOGGER.warning("Waiting %s s to retry", retry_seconds) return - _state = STATE_UNKNOWN + _state = "unknown" try: self._last_tried = dt_util.now() _state = self.smartplug.state except urllib.error.HTTPError: _LOGGER.error("D-Link connection problem") - if _state == STATE_UNKNOWN: + if _state == "unknown": self._n_tried += 1 self.available = False _LOGGER.warning("Failed to connect to D-Link switch") diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 4351b230538..a3276c53e3b 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -5,7 +5,6 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, - STATE_UNKNOWN, TEMP_FAHRENHEIT, ) from homeassistant.helpers.entity import Entity @@ -112,7 +111,7 @@ class EcobeeSensor(Entity): if self._state in [ ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN, - STATE_UNKNOWN, + "unknown", ]: return None From da19854520bc6ca16bcccc4ec0c2c3ad53e10cef 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 127/514] 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 17efa1bda5e173862741aa347a96bb70fc124a0a Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 13 Sep 2020 11:32:41 -0500 Subject: [PATCH 128/514] Improve canary tests (#39956) --- homeassistant/components/canary/__init__.py | 4 + tests/components/canary/__init__.py | 60 ++++ tests/components/canary/conftest.py | 34 +++ tests/components/canary/test_init.py | 92 ++---- tests/components/canary/test_sensor.py | 298 +++++++++----------- 5 files changed, 261 insertions(+), 227 deletions(-) create mode 100644 tests/components/canary/conftest.py diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index 165787a7f46..0a4ae770006 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -76,6 +76,10 @@ class CanaryData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self, **kwargs): + """Get the latest data from py-canary with a throttle.""" + self._update(**kwargs) + + def _update(self, **kwargs): """Get the latest data from py-canary.""" for location in self._api.get_locations(): location_id = location.location_id diff --git a/tests/components/canary/__init__.py b/tests/components/canary/__init__.py index cc85edd806a..2ad275165d6 100644 --- a/tests/components/canary/__init__.py +++ b/tests/components/canary/__init__.py @@ -1 +1,61 @@ """Tests for the canary component.""" +from unittest.mock import MagicMock, PropertyMock + +from canary.api import SensorType + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + + +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, + ) + await hass.async_block_till_done() + + +def mock_device(device_id, name, is_online=True, device_type_name=None): + """Mock Canary Device class.""" + device = MagicMock() + type(device).device_id = PropertyMock(return_value=device_id) + type(device).name = PropertyMock(return_value=name) + type(device).is_online = PropertyMock(return_value=is_online) + type(device).device_type = PropertyMock( + return_value={"id": 1, "name": device_type_name} + ) + return device + + +def mock_location(location_id, name, is_celsius=True, devices=None): + """Mock Canary Location class.""" + location = MagicMock() + type(location).location_id = PropertyMock(return_value=location_id) + type(location).name = PropertyMock(return_value=name) + type(location).is_celsius = PropertyMock(return_value=is_celsius) + type(location).devices = PropertyMock(return_value=devices or []) + return location + + +def mock_mode(mode_id, name): + """Mock Canary Mode class.""" + mode = MagicMock() + type(mode).mode_id = PropertyMock(return_value=mode_id) + type(mode).name = PropertyMock(return_value=name) + type(mode).resource_url = PropertyMock(return_value=f"/v1/modes/{mode_id}") + return mode + + +def mock_reading(sensor_type, sensor_value): + """Mock Canary Reading class.""" + reading = MagicMock() + type(reading).sensor_type = SensorType(sensor_type) + type(reading).value = PropertyMock(return_value=sensor_value) + return reading diff --git a/tests/components/canary/conftest.py b/tests/components/canary/conftest.py new file mode 100644 index 00000000000..41873e0c25f --- /dev/null +++ b/tests/components/canary/conftest.py @@ -0,0 +1,34 @@ +"""Define fixtures available for all tests.""" +from canary.api import Api +from pytest import fixture + +from tests.async_mock import MagicMock, patch + + +def mock_canary_update(self, **kwargs): + """Get the latest data from py-canary.""" + self._update(**kwargs) + + +@fixture +def canary(hass): + """Mock the CanaryApi for easier testing.""" + with patch.object(Api, "login", return_value=True), patch( + "homeassistant.components.canary.CanaryData.update", mock_canary_update + ), patch("homeassistant.components.canary.Api") as mock_canary: + instance = mock_canary.return_value = Api( + "test-username", + "test-password", + 1, + ) + + instance.login = MagicMock(return_value=True) + instance.get_entries = MagicMock(return_value=[]) + instance.get_locations = MagicMock(return_value=[]) + instance.get_location = MagicMock(return_value=None) + instance.get_modes = MagicMock(return_value=[]) + instance.get_readings = MagicMock(return_value=[]) + instance.get_latest_readings = MagicMock(return_value=[]) + instance.set_location_mode = MagicMock(return_value=None) + + yield mock_canary diff --git a/tests/components/canary/test_init.py b/tests/components/canary/test_init.py index 0cfbfd56de6..ab0d8e5ab7a 100644 --- a/tests/components/canary/test_init.py +++ b/tests/components/canary/test_init.py @@ -1,73 +1,37 @@ """The tests for the Canary component.""" -import unittest +from requests import HTTPError -from homeassistant import setup -import homeassistant.components.canary as canary +from homeassistant.components.canary import DOMAIN +from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, PropertyMock, patch -from tests.common import get_test_home_assistant +from tests.async_mock import patch -def mock_device(device_id, name, is_online=True, device_type_name=None): - """Mock Canary Device class.""" - device = MagicMock() - type(device).device_id = PropertyMock(return_value=device_id) - type(device).name = PropertyMock(return_value=name) - type(device).is_online = PropertyMock(return_value=is_online) - type(device).device_type = PropertyMock( - return_value={"id": 1, "name": device_type_name} - ) - return device +async def test_setup_with_valid_config(hass, canary) -> None: + """Test setup with valid YAML.""" + await async_setup_component(hass, "persistent_notification", {}) + config = {DOMAIN: {"username": "test-username", "password": "test-password"}} + + with patch( + "homeassistant.components.canary.alarm_control_panel.setup_platform", + return_value=True, + ), patch( + "homeassistant.components.canary.camera.setup_platform", + return_value=True, + ), patch( + "homeassistant.components.canary.sensor.setup_platform", + return_value=True, + ): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() -def mock_location(name, is_celsius=True, devices=None): - """Mock Canary Location class.""" - location = MagicMock() - type(location).name = PropertyMock(return_value=name) - type(location).is_celsius = PropertyMock(return_value=is_celsius) - type(location).devices = PropertyMock(return_value=devices or []) - return location +async def test_setup_with_http_error(hass, canary) -> None: + """Test setup with HTTP error.""" + await async_setup_component(hass, "persistent_notification", {}) + config = {DOMAIN: {"username": "test-username", "password": "test-password"}} + canary.side_effect = HTTPError() -def mock_reading(sensor_type, sensor_value): - """Mock Canary Reading class.""" - reading = MagicMock() - type(reading).sensor_type = PropertyMock(return_value=sensor_type) - type(reading).value = PropertyMock(return_value=sensor_value) - return reading - - -class TestCanary(unittest.TestCase): - """Tests the Canary component.""" - - def setUp(self): - """Initialize values for this test case class.""" - self.hass = get_test_home_assistant() - self.addCleanup(self.tear_down_cleanup) - - def tear_down_cleanup(self): - """Stop everything that was started.""" - self.hass.stop() - - @patch("homeassistant.components.canary.CanaryData.update") - @patch("canary.api.Api.login") - def test_setup_with_valid_config(self, mock_login, mock_update): - """Test setup component.""" - config = {"canary": {"username": "foo@bar.org", "password": "bar"}} - - assert setup.setup_component(self.hass, canary.DOMAIN, config) - - mock_update.assert_called_once_with() - mock_login.assert_called_once_with() - - def test_setup_with_missing_password(self): - """Test setup component.""" - config = {"canary": {"username": "foo@bar.org"}} - - assert not setup.setup_component(self.hass, canary.DOMAIN, config) - - def test_setup_with_missing_username(self): - """Test setup component.""" - config = {"canary": {"password": "bar"}} - - assert not setup.setup_component(self.hass, canary.DOMAIN, config) + assert not await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index f328fb5a976..02d2dc5cc24 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -1,203 +1,175 @@ """The tests for the Canary sensor platform.""" -import copy -import unittest - -from homeassistant.components.canary import DATA_CANARY, sensor as canary +from homeassistant.components.canary import DOMAIN from homeassistant.components.canary.sensor import ( ATTR_AIR_QUALITY, - SENSOR_TYPES, STATE_AIR_QUALITY_ABNORMAL, STATE_AIR_QUALITY_NORMAL, STATE_AIR_QUALITY_VERY_ABNORMAL, - CanarySensor, ) -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, TEMP_CELSIUS +from homeassistant.setup import async_setup_component -from tests.async_mock import Mock -from tests.common import get_test_home_assistant -from tests.components.canary.test_init import mock_device, mock_location +from . import mock_device, mock_location, mock_reading -VALID_CONFIG = {"canary": {"username": "foo@bar.org", "password": "bar"}} +from tests.async_mock import patch +from tests.common import mock_registry -class TestCanarySensorSetup(unittest.TestCase): - """Test the Canary platform.""" +async def test_sensors_pro(hass, canary) -> None: + """Test the creation and values of the sensors for Canary Pro.""" + await async_setup_component(hass, "persistent_notification", {}) - DEVICES = [] + registry = mock_registry(hass) + online_device_at_home = mock_device(20, "Dining Room", True, "Canary Pro") - def add_entities(self, devices, action): - """Mock add devices.""" - for device in devices: - self.DEVICES.append(device) + instance = canary.return_value + instance.get_locations.return_value = [ + mock_location(100, "Home", True, devices=[online_device_at_home]), + ] - def setUp(self): - """Initialize values for this testcase class.""" - self.hass = get_test_home_assistant() - self.config = copy.deepcopy(VALID_CONFIG) - self.addCleanup(self.hass.stop) + instance.get_latest_readings.return_value = [ + mock_reading("temperature", "21.12"), + mock_reading("humidity", "50.46"), + mock_reading("air_quality", "0.59"), + ] - def test_setup_sensors(self): - """Test the sensor setup.""" - online_device_at_home = mock_device(20, "Dining Room", True, "Canary Pro") - offline_device_at_home = mock_device(21, "Front Yard", False, "Canary Pro") - online_device_at_work = mock_device(22, "Office", True, "Canary Pro") + config = {DOMAIN: {"username": "test-username", "password": "test-password"}} + with patch("homeassistant.components.canary.CANARY_COMPONENTS", ["sensor"]): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() - self.hass.data[DATA_CANARY] = Mock() - self.hass.data[DATA_CANARY].locations = [ - mock_location( - "Home", True, devices=[online_device_at_home, offline_device_at_home] - ), - mock_location("Work", True, devices=[online_device_at_work]), - ] + sensors = { + "home_dining_room_temperature": ( + "20_temperature", + "21.12", + TEMP_CELSIUS, + None, + "mdi:thermometer", + ), + "home_dining_room_humidity": ( + "20_humidity", + "50.46", + PERCENTAGE, + None, + "mdi:water-percent", + ), + "home_dining_room_air_quality": ( + "20_air_quality", + "0.59", + None, + None, + "mdi:weather-windy", + ), + } - canary.setup_platform(self.hass, self.config, self.add_entities, None) + for (sensor_id, data) in sensors.items(): + entity_entry = registry.async_get(f"sensor.{sensor_id}") + assert entity_entry + assert entity_entry.device_class == data[3] + assert entity_entry.unique_id == data[0] + assert entity_entry.original_icon == data[4] - assert len(self.DEVICES) == 6 + state = hass.states.get(f"sensor.{sensor_id}") + assert state + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == data[2] + assert state.state == data[1] - def test_temperature_sensor(self): - """Test temperature sensor with fahrenheit.""" - device = mock_device(10, "Family Room", "Canary Pro") - location = mock_location("Home", False) - data = Mock() - data.get_reading.return_value = 21.1234 +async def test_sensors_attributes_pro(hass, canary) -> None: + """Test the creation and values of the sensors attributes for Canary Pro.""" + await async_setup_component(hass, "persistent_notification", {}) - sensor = CanarySensor(data, SENSOR_TYPES[0], location, device) - sensor.update() + online_device_at_home = mock_device(20, "Dining Room", True, "Canary Pro") - assert sensor.name == "Home Family Room Temperature" - assert sensor.unit_of_measurement == TEMP_CELSIUS - assert sensor.state == 21.12 - assert sensor.icon == "mdi:thermometer" + instance = canary.return_value + instance.get_locations.return_value = [ + mock_location(100, "Home", True, devices=[online_device_at_home]), + ] - def test_temperature_sensor_with_none_sensor_value(self): - """Test temperature sensor with fahrenheit.""" - device = mock_device(10, "Family Room", "Canary Pro") - location = mock_location("Home", False) + instance.get_latest_readings.return_value = [ + mock_reading("temperature", "21.12"), + mock_reading("humidity", "50.46"), + mock_reading("air_quality", "0.59"), + ] - data = Mock() - data.get_reading.return_value = None + config = {DOMAIN: {"username": "test-username", "password": "test-password"}} + with patch("homeassistant.components.canary.CANARY_COMPONENTS", ["sensor"]): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() - sensor = CanarySensor(data, SENSOR_TYPES[0], location, device) - sensor.update() + entity_id = "sensor.home_dining_room_air_quality" + state = hass.states.get(entity_id) + assert state + assert state.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_ABNORMAL - assert sensor.state is None + instance.get_latest_readings.return_value = [ + mock_reading("temperature", "21.12"), + mock_reading("humidity", "50.46"), + mock_reading("air_quality", "0.4"), + ] - def test_humidity_sensor(self): - """Test humidity sensor.""" - device = mock_device(10, "Family Room", "Canary Pro") - location = mock_location("Home") + await hass.helpers.entity_component.async_update_entity(entity_id) + await hass.async_block_till_done() - data = Mock() - data.get_reading.return_value = 50.4567 + state = hass.states.get(entity_id) + assert state + assert state.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_VERY_ABNORMAL - sensor = CanarySensor(data, SENSOR_TYPES[1], location, device) - sensor.update() + instance.get_latest_readings.return_value = [ + mock_reading("temperature", "21.12"), + mock_reading("humidity", "50.46"), + mock_reading("air_quality", "1.0"), + ] - assert sensor.name == "Home Family Room Humidity" - assert sensor.unit_of_measurement == PERCENTAGE - assert sensor.state == 50.46 - assert sensor.icon == "mdi:water-percent" + await hass.helpers.entity_component.async_update_entity(entity_id) + await hass.async_block_till_done() - def test_air_quality_sensor_with_very_abnormal_reading(self): - """Test air quality sensor.""" - device = mock_device(10, "Family Room", "Canary Pro") - location = mock_location("Home") + state = hass.states.get(entity_id) + assert state + assert state.attributes[ATTR_AIR_QUALITY] == STATE_AIR_QUALITY_NORMAL - data = Mock() - data.get_reading.return_value = 0.4 - sensor = CanarySensor(data, SENSOR_TYPES[2], location, device) - sensor.update() +async def test_sensors_flex(hass, canary) -> None: + """Test the creation and values of the sensors for Canary Flex.""" + await async_setup_component(hass, "persistent_notification", {}) - assert sensor.name == "Home Family Room Air Quality" - assert sensor.unit_of_measurement is None - assert sensor.state == 0.4 - assert sensor.icon == "mdi:weather-windy" + registry = mock_registry(hass) + online_device_at_home = mock_device(20, "Dining Room", True, "Canary Flex") - air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY] - assert air_quality == STATE_AIR_QUALITY_VERY_ABNORMAL + instance = canary.return_value + instance.get_locations.return_value = [ + mock_location(100, "Home", True, devices=[online_device_at_home]), + ] - def test_air_quality_sensor_with_abnormal_reading(self): - """Test air quality sensor.""" - device = mock_device(10, "Family Room", "Canary Pro") - location = mock_location("Home") + instance.get_latest_readings.return_value = [ + mock_reading("battery", "70.4567"), + mock_reading("wifi", "-57"), + ] - data = Mock() - data.get_reading.return_value = 0.59 + config = {DOMAIN: {"username": "test-username", "password": "test-password"}} + with patch("homeassistant.components.canary.CANARY_COMPONENTS", ["sensor"]): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() - sensor = CanarySensor(data, SENSOR_TYPES[2], location, device) - sensor.update() + sensors = { + "home_dining_room_battery": ( + "20_battery", + "70.46", + PERCENTAGE, + None, + "mdi:battery-70", + ), + "home_dining_room_wifi": ("20_wifi", "-57.0", "dBm", None, "mdi:wifi"), + } - assert sensor.name == "Home Family Room Air Quality" - assert sensor.unit_of_measurement is None - assert sensor.state == 0.59 - assert sensor.icon == "mdi:weather-windy" + for (sensor_id, data) in sensors.items(): + entity_entry = registry.async_get(f"sensor.{sensor_id}") + assert entity_entry + assert entity_entry.device_class == data[3] + assert entity_entry.unique_id == data[0] + assert entity_entry.original_icon == data[4] - air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY] - assert air_quality == STATE_AIR_QUALITY_ABNORMAL - - def test_air_quality_sensor_with_normal_reading(self): - """Test air quality sensor.""" - device = mock_device(10, "Family Room", "Canary Pro") - location = mock_location("Home") - - data = Mock() - data.get_reading.return_value = 1.0 - - sensor = CanarySensor(data, SENSOR_TYPES[2], location, device) - sensor.update() - - assert sensor.name == "Home Family Room Air Quality" - assert sensor.unit_of_measurement is None - assert sensor.state == 1.0 - assert sensor.icon == "mdi:weather-windy" - - air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY] - assert air_quality == STATE_AIR_QUALITY_NORMAL - - def test_air_quality_sensor_with_none_sensor_value(self): - """Test air quality sensor.""" - device = mock_device(10, "Family Room", "Canary Pro") - location = mock_location("Home") - - data = Mock() - data.get_reading.return_value = None - - sensor = CanarySensor(data, SENSOR_TYPES[2], location, device) - sensor.update() - - assert sensor.state is None - assert sensor.device_state_attributes is None - - def test_battery_sensor(self): - """Test battery sensor.""" - device = mock_device(10, "Family Room", "Canary Flex") - location = mock_location("Home") - - data = Mock() - data.get_reading.return_value = 70.4567 - - sensor = CanarySensor(data, SENSOR_TYPES[4], location, device) - sensor.update() - - assert sensor.name == "Home Family Room Battery" - assert sensor.unit_of_measurement == PERCENTAGE - assert sensor.state == 70.46 - assert sensor.icon == "mdi:battery-70" - - def test_wifi_sensor(self): - """Test battery sensor.""" - device = mock_device(10, "Family Room", "Canary Flex") - location = mock_location("Home") - - data = Mock() - data.get_reading.return_value = -57 - - sensor = CanarySensor(data, SENSOR_TYPES[3], location, device) - sensor.update() - - assert sensor.name == "Home Family Room Wifi" - assert sensor.unit_of_measurement == "dBm" - assert sensor.state == -57 - assert sensor.icon == "mdi:wifi" + state = hass.states.get(f"sensor.{sensor_id}") + assert state + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == data[2] + assert state.state == data[1] From c32f69867103bcb40a5ff16dd64d72985feb3505 Mon Sep 17 00:00:00 2001 From: AJ Schmidt Date: Sun, 13 Sep 2020 13:29:25 -0400 Subject: [PATCH 129/514] Add Config Flow to AlarmDecoder (#37998) --- .coveragerc | 6 +- .../components/alarmdecoder/__init__.py | 242 ++++------ .../alarmdecoder/alarm_control_panel.py | 86 ++-- .../components/alarmdecoder/binary_sensor.py | 40 +- .../components/alarmdecoder/config_flow.py | 356 +++++++++++++++ .../components/alarmdecoder/const.py | 49 ++ .../components/alarmdecoder/manifest.json | 3 +- .../components/alarmdecoder/sensor.py | 19 +- .../components/alarmdecoder/services.yaml | 6 + .../components/alarmdecoder/strings.json | 72 +++ .../alarmdecoder/translations/en.json | 72 +++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + .../alarmdecoder/test_config_flow.py | 424 ++++++++++++++++++ 14 files changed, 1169 insertions(+), 210 deletions(-) create mode 100644 homeassistant/components/alarmdecoder/config_flow.py create mode 100644 homeassistant/components/alarmdecoder/const.py create mode 100644 homeassistant/components/alarmdecoder/strings.json create mode 100644 homeassistant/components/alarmdecoder/translations/en.json create mode 100644 tests/components/alarmdecoder/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 3785240a387..5a360e89226 100644 --- a/.coveragerc +++ b/.coveragerc @@ -33,7 +33,11 @@ omit = homeassistant/components/airvisual/air_quality.py homeassistant/components/airvisual/sensor.py homeassistant/components/aladdin_connect/cover.py - homeassistant/components/alarmdecoder/* + homeassistant/components/alarmdecoder/__init__.py + homeassistant/components/alarmdecoder/alarm_control_panel.py + homeassistant/components/alarmdecoder/binary_sensor.py + homeassistant/components/alarmdecoder/const.py + homeassistant/components/alarmdecoder/sensor.py homeassistant/components/alpha_vantage/sensor.py homeassistant/components/amazon_polly/tts.py homeassistant/components/ambiclimate/climate.py diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index 0aa9fcc29ec..b69b60b82c4 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -1,167 +1,82 @@ """Support for AlarmDecoder devices.""" +import asyncio from datetime import timedelta import logging from adext import AdExt -from alarmdecoder.devices import SerialDevice, SocketDevice, USBDevice +from alarmdecoder.devices import SerialDevice, SocketDevice from alarmdecoder.util import NoDeviceError -import voluptuous as vol -from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA -from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + CONF_PROTOCOL, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util +from .const import ( + CONF_DEVICE_BAUD, + CONF_DEVICE_PATH, + DATA_AD, + DATA_REMOVE_STOP_LISTENER, + DATA_REMOVE_UPDATE_LISTENER, + DATA_RESTART, + DOMAIN, + PROTOCOL_SERIAL, + PROTOCOL_SOCKET, + SIGNAL_PANEL_MESSAGE, + SIGNAL_REL_MESSAGE, + SIGNAL_RFX_MESSAGE, + SIGNAL_ZONE_FAULT, + SIGNAL_ZONE_RESTORE, +) + _LOGGER = logging.getLogger(__name__) -DOMAIN = "alarmdecoder" - -DATA_AD = "alarmdecoder" - -CONF_DEVICE = "device" -CONF_DEVICE_BAUD = "baudrate" -CONF_DEVICE_PATH = "path" -CONF_DEVICE_PORT = "port" -CONF_DEVICE_TYPE = "type" -CONF_AUTO_BYPASS = "autobypass" -CONF_PANEL_DISPLAY = "panel_display" -CONF_ZONE_NAME = "name" -CONF_ZONE_TYPE = "type" -CONF_ZONE_LOOP = "loop" -CONF_ZONE_RFID = "rfid" -CONF_ZONES = "zones" -CONF_RELAY_ADDR = "relayaddr" -CONF_RELAY_CHAN = "relaychan" -CONF_CODE_ARM_REQUIRED = "code_arm_required" - -DEFAULT_DEVICE_TYPE = "socket" -DEFAULT_DEVICE_HOST = "localhost" -DEFAULT_DEVICE_PORT = 10000 -DEFAULT_DEVICE_PATH = "/dev/ttyUSB0" -DEFAULT_DEVICE_BAUD = 115200 - -DEFAULT_AUTO_BYPASS = False -DEFAULT_PANEL_DISPLAY = False -DEFAULT_CODE_ARM_REQUIRED = True - -DEFAULT_ZONE_TYPE = "opening" - -SIGNAL_PANEL_MESSAGE = "alarmdecoder.panel_message" -SIGNAL_PANEL_ARM_AWAY = "alarmdecoder.panel_arm_away" -SIGNAL_PANEL_ARM_HOME = "alarmdecoder.panel_arm_home" -SIGNAL_PANEL_DISARM = "alarmdecoder.panel_disarm" - -SIGNAL_ZONE_FAULT = "alarmdecoder.zone_fault" -SIGNAL_ZONE_RESTORE = "alarmdecoder.zone_restore" -SIGNAL_RFX_MESSAGE = "alarmdecoder.rfx_message" -SIGNAL_REL_MESSAGE = "alarmdecoder.rel_message" - -DEVICE_SOCKET_SCHEMA = vol.Schema( - { - vol.Required(CONF_DEVICE_TYPE): "socket", - vol.Optional(CONF_HOST, default=DEFAULT_DEVICE_HOST): cv.string, - vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_DEVICE_PORT): cv.port, - } -) - -DEVICE_SERIAL_SCHEMA = vol.Schema( - { - vol.Required(CONF_DEVICE_TYPE): "serial", - vol.Optional(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): cv.string, - vol.Optional(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): cv.string, - } -) - -DEVICE_USB_SCHEMA = vol.Schema({vol.Required(CONF_DEVICE_TYPE): "usb"}) - -ZONE_SCHEMA = vol.Schema( - { - vol.Required(CONF_ZONE_NAME): cv.string, - vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): vol.Any( - DEVICE_CLASSES_SCHEMA - ), - vol.Optional(CONF_ZONE_RFID): cv.string, - vol.Optional(CONF_ZONE_LOOP): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)), - vol.Inclusive( - CONF_RELAY_ADDR, - "relaylocation", - "Relay address and channel must exist together", - ): cv.byte, - vol.Inclusive( - CONF_RELAY_CHAN, - "relaylocation", - "Relay address and channel must exist together", - ): cv.byte, - } -) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_DEVICE): vol.Any( - DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA, DEVICE_USB_SCHEMA - ), - vol.Optional( - CONF_PANEL_DISPLAY, default=DEFAULT_PANEL_DISPLAY - ): cv.boolean, - vol.Optional(CONF_AUTO_BYPASS, default=DEFAULT_AUTO_BYPASS): cv.boolean, - vol.Optional( - CONF_CODE_ARM_REQUIRED, default=DEFAULT_CODE_ARM_REQUIRED - ): cv.boolean, - vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA}, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) +PLATFORMS = ["alarm_control_panel", "sensor", "binary_sensor"] -def setup(hass, config): +async def async_setup(hass, config): """Set up for the AlarmDecoder devices.""" - conf = config.get(DOMAIN) + return True - restart = False - device = conf[CONF_DEVICE] - display = conf[CONF_PANEL_DISPLAY] - auto_bypass = conf[CONF_AUTO_BYPASS] - code_arm_required = conf[CONF_CODE_ARM_REQUIRED] - zones = conf.get(CONF_ZONES) - device_type = device[CONF_DEVICE_TYPE] - host = DEFAULT_DEVICE_HOST - port = DEFAULT_DEVICE_PORT - path = DEFAULT_DEVICE_PATH - baud = DEFAULT_DEVICE_BAUD +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Set up AlarmDecoder config flow.""" + undo_listener = entry.add_update_listener(_update_listener) + + ad_connection = entry.data + protocol = ad_connection[CONF_PROTOCOL] def stop_alarmdecoder(event): """Handle the shutdown of AlarmDecoder.""" + if not hass.data.get(DOMAIN): + return _LOGGER.debug("Shutting down alarmdecoder") - nonlocal restart - restart = False + hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = False controller.close() def open_connection(now=None): """Open a connection to AlarmDecoder.""" - nonlocal restart try: controller.open(baud) except NoDeviceError: - _LOGGER.debug("Failed to connect. Retrying in 5 seconds") + _LOGGER.debug("Failed to connect. Retrying in 5 seconds") hass.helpers.event.track_point_in_time( open_connection, dt_util.utcnow() + timedelta(seconds=5) ) return _LOGGER.debug("Established a connection with the alarmdecoder") - restart = True + hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = True def handle_closed_connection(event): """Restart after unexpected loss of connection.""" - nonlocal restart - if not restart: + if not hass.data[DOMAIN][entry.entry_id][DATA_RESTART]: return - restart = False + hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = False _LOGGER.warning("AlarmDecoder unexpectedly lost connection") hass.add_job(open_connection) @@ -186,17 +101,14 @@ def setup(hass, config): hass.helpers.dispatcher.dispatcher_send(SIGNAL_REL_MESSAGE, message) controller = False - if device_type == "socket": - host = device[CONF_HOST] - port = device[CONF_DEVICE_PORT] + baud = ad_connection[CONF_DEVICE_BAUD] + if protocol == PROTOCOL_SOCKET: + host = ad_connection[CONF_HOST] + port = ad_connection[CONF_PORT] controller = AdExt(SocketDevice(interface=(host, port))) - elif device_type == "serial": - path = device[CONF_DEVICE_PATH] - baud = device[CONF_DEVICE_BAUD] + if protocol == PROTOCOL_SERIAL: + path = ad_connection[CONF_DEVICE_PATH] controller = AdExt(SerialDevice(interface=path)) - elif device_type == "usb": - AdExt(USBDevice.find()) - return False controller.on_message += handle_message controller.on_rfx_message += handle_rfx_message @@ -205,24 +117,56 @@ def setup(hass, config): controller.on_close += handle_closed_connection controller.on_expander_message += handle_rel_message - hass.data[DATA_AD] = controller + remove_stop_listener = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder + ) + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + DATA_AD: controller, + DATA_REMOVE_UPDATE_LISTENER: undo_listener, + DATA_REMOVE_STOP_LISTENER: remove_stop_listener, + DATA_RESTART: False, + } open_connection() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder) + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + return True - load_platform( - hass, - "alarm_control_panel", - DOMAIN, - {CONF_AUTO_BYPASS: auto_bypass, CONF_CODE_ARM_REQUIRED: code_arm_required}, - config, + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Unload a AlarmDecoder entry.""" + hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = False + + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) ) - if zones: - load_platform(hass, "binary_sensor", DOMAIN, {CONF_ZONES: zones}, config) + if not unload_ok: + return False - if display: - load_platform(hass, "sensor", DOMAIN, conf, config) + hass.data[DOMAIN][entry.entry_id][DATA_REMOVE_UPDATE_LISTENER]() + hass.data[DOMAIN][entry.entry_id][DATA_REMOVE_STOP_LISTENER]() + hass.data[DOMAIN][entry.entry_id][DATA_AD].close() + + if hass.data[DOMAIN][entry.entry_id]: + hass.data[DOMAIN].pop(entry.entry_id) + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) return True + + +async def _update_listener(hass: HomeAssistantType, entry: ConfigEntry): + """Handle options update.""" + _LOGGER.debug("AlarmDecoder options updated: %s", entry.as_dict()["options"]) + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 117374552f3..83288c93c67 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -12,74 +12,90 @@ from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_HOME, SUPPORT_ALARM_ARM_NIGHT, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CODE, + ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) +from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType -from . import ( +from .const import ( + CONF_ALT_NIGHT_MODE, CONF_AUTO_BYPASS, CONF_CODE_ARM_REQUIRED, DATA_AD, + DEFAULT_ARM_OPTIONS, DOMAIN, + OPTIONS_ARM, SIGNAL_PANEL_MESSAGE, ) _LOGGER = logging.getLogger(__name__) SERVICE_ALARM_TOGGLE_CHIME = "alarm_toggle_chime" -ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({vol.Required(ATTR_CODE): cv.string}) +ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID, default=[]): cv.entity_ids, + vol.Required(ATTR_CODE): cv.string, + } +) SERVICE_ALARM_KEYPRESS = "alarm_keypress" ATTR_KEYPRESS = "keypress" -ALARM_KEYPRESS_SCHEMA = vol.Schema({vol.Required(ATTR_KEYPRESS): cv.string}) +ALARM_KEYPRESS_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID, default=[]): cv.entity_ids, + vol.Required(ATTR_KEYPRESS): cv.string, + } +) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +): """Set up for AlarmDecoder alarm panels.""" - if discovery_info is None: - return + options = entry.options + arm_options = options.get(OPTIONS_ARM, DEFAULT_ARM_OPTIONS) + client = hass.data[DOMAIN][entry.entry_id][DATA_AD] - auto_bypass = discovery_info[CONF_AUTO_BYPASS] - code_arm_required = discovery_info[CONF_CODE_ARM_REQUIRED] - entity = AlarmDecoderAlarmPanel(auto_bypass, code_arm_required) - add_entities([entity]) + entity = AlarmDecoderAlarmPanel( + client=client, + auto_bypass=arm_options[CONF_AUTO_BYPASS], + code_arm_required=arm_options[CONF_CODE_ARM_REQUIRED], + alt_night_mode=arm_options[CONF_ALT_NIGHT_MODE], + ) + async_add_entities([entity]) - def alarm_toggle_chime_handler(service): - """Register toggle chime handler.""" - code = service.data.get(ATTR_CODE) - entity.alarm_toggle_chime(code) + platform = entity_platform.current_platform.get() - hass.services.register( - DOMAIN, + platform.async_register_entity_service( SERVICE_ALARM_TOGGLE_CHIME, - alarm_toggle_chime_handler, - schema=ALARM_TOGGLE_CHIME_SCHEMA, + ALARM_TOGGLE_CHIME_SCHEMA, + "alarm_toggle_chime", ) - def alarm_keypress_handler(service): - """Register keypress handler.""" - keypress = service.data[ATTR_KEYPRESS] - entity.alarm_keypress(keypress) - - hass.services.register( - DOMAIN, + platform.async_register_entity_service( SERVICE_ALARM_KEYPRESS, - alarm_keypress_handler, - schema=ALARM_KEYPRESS_SCHEMA, + ALARM_KEYPRESS_SCHEMA, + "alarm_keypress", ) + return True + class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): """Representation of an AlarmDecoder-based alarm panel.""" - def __init__(self, auto_bypass, code_arm_required): + def __init__(self, client, auto_bypass, code_arm_required, alt_night_mode): """Initialize the alarm panel.""" + self._client = client self._display = "" self._name = "Alarm Panel" self._state = None @@ -95,6 +111,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): self._zone_bypassed = None self._auto_bypass = auto_bypass self._code_arm_required = code_arm_required + self._alt_night_mode = alt_night_mode async def async_added_to_hass(self): """Register callbacks.""" @@ -180,11 +197,11 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): def alarm_disarm(self, code=None): """Send disarm command.""" if code: - self.hass.data[DATA_AD].send(f"{code!s}1") + self._client.send(f"{code!s}1") def alarm_arm_away(self, code=None): """Send arm away command.""" - self.hass.data[DATA_AD].arm_away( + self._client.arm_away( code=code, code_arm_required=self._code_arm_required, auto_bypass=self._auto_bypass, @@ -192,7 +209,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): def alarm_arm_home(self, code=None): """Send arm home command.""" - self.hass.data[DATA_AD].arm_home( + self._client.arm_home( code=code, code_arm_required=self._code_arm_required, auto_bypass=self._auto_bypass, @@ -200,18 +217,19 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): def alarm_arm_night(self, code=None): """Send arm night command.""" - self.hass.data[DATA_AD].arm_night( + self._client.arm_night( code=code, code_arm_required=self._code_arm_required, + alt_night_mode=self._alt_night_mode, auto_bypass=self._auto_bypass, ) def alarm_toggle_chime(self, code=None): """Send toggle chime command.""" if code: - self.hass.data[DATA_AD].send(f"{code!s}9") + self._client.send(f"{code!s}9") def alarm_keypress(self, keypress): """Send custom keypresses.""" if keypress: - self.hass.data[DATA_AD].send(keypress) + self._client.send(keypress) diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index cec1b8356b0..417dfd6f96a 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -2,20 +2,23 @@ import logging from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType -from . import ( +from .const import ( CONF_RELAY_ADDR, CONF_RELAY_CHAN, CONF_ZONE_LOOP, CONF_ZONE_NAME, + CONF_ZONE_NUMBER, CONF_ZONE_RFID, CONF_ZONE_TYPE, - CONF_ZONES, + DEFAULT_ZONE_OPTIONS, + OPTIONS_ZONES, SIGNAL_REL_MESSAGE, SIGNAL_RFX_MESSAGE, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE, - ZONE_SCHEMA, ) _LOGGER = logging.getLogger(__name__) @@ -30,26 +33,28 @@ ATTR_RF_LOOP4 = "rf_loop4" ATTR_RF_LOOP1 = "rf_loop1" -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the AlarmDecoder binary sensor devices.""" - configured_zones = discovery_info[CONF_ZONES] +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +): + """Set up for AlarmDecoder sensor.""" + + zones = entry.options.get(OPTIONS_ZONES, DEFAULT_ZONE_OPTIONS) devices = [] - for zone_num in configured_zones: - device_config_data = ZONE_SCHEMA(configured_zones[zone_num]) - zone_type = device_config_data[CONF_ZONE_TYPE] - zone_name = device_config_data[CONF_ZONE_NAME] - zone_rfid = device_config_data.get(CONF_ZONE_RFID) - zone_loop = device_config_data.get(CONF_ZONE_LOOP) - relay_addr = device_config_data.get(CONF_RELAY_ADDR) - relay_chan = device_config_data.get(CONF_RELAY_CHAN) + for zone_num in zones: + zone_info = zones[zone_num] + zone_type = zone_info[CONF_ZONE_TYPE] + zone_name = zone_info[CONF_ZONE_NAME] + zone_rfid = zone_info.get(CONF_ZONE_RFID) + zone_loop = zone_info.get(CONF_ZONE_LOOP) + relay_addr = zone_info.get(CONF_RELAY_ADDR) + relay_chan = zone_info.get(CONF_RELAY_CHAN) device = AlarmDecoderBinarySensor( zone_num, zone_name, zone_type, zone_rfid, zone_loop, relay_addr, relay_chan ) devices.append(device) - add_entities(devices) - + async_add_entities(devices) return True @@ -67,7 +72,7 @@ class AlarmDecoderBinarySensor(BinarySensorEntity): relay_chan, ): """Initialize the binary_sensor.""" - self._zone_number = zone_number + self._zone_number = int(zone_number) self._zone_type = zone_type self._state = None self._name = zone_name @@ -117,6 +122,7 @@ class AlarmDecoderBinarySensor(BinarySensorEntity): def device_state_attributes(self): """Return the state attributes.""" attr = {} + attr[CONF_ZONE_NUMBER] = self._zone_number if self._rfid and self._rfstate is not None: attr[ATTR_RF_BIT0] = bool(self._rfstate & 0x01) attr[ATTR_RF_LOW_BAT] = bool(self._rfstate & 0x02) diff --git a/homeassistant/components/alarmdecoder/config_flow.py b/homeassistant/components/alarmdecoder/config_flow.py new file mode 100644 index 00000000000..1f6f049fcb3 --- /dev/null +++ b/homeassistant/components/alarmdecoder/config_flow.py @@ -0,0 +1,356 @@ +"""Config flow for AlarmDecoder.""" +import logging + +from adext import AdExt +from alarmdecoder.devices import SerialDevice, SocketDevice +from alarmdecoder.util import NoDeviceError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.binary_sensor import DEVICE_CLASSES +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL +from homeassistant.core import callback + +from .const import ( # pylint: disable=unused-import + CONF_ALT_NIGHT_MODE, + CONF_AUTO_BYPASS, + CONF_CODE_ARM_REQUIRED, + CONF_DEVICE_BAUD, + CONF_DEVICE_PATH, + CONF_RELAY_ADDR, + CONF_RELAY_CHAN, + CONF_ZONE_LOOP, + CONF_ZONE_NAME, + CONF_ZONE_NUMBER, + CONF_ZONE_RFID, + CONF_ZONE_TYPE, + DEFAULT_ARM_OPTIONS, + DEFAULT_DEVICE_BAUD, + DEFAULT_DEVICE_HOST, + DEFAULT_DEVICE_PATH, + DEFAULT_DEVICE_PORT, + DEFAULT_ZONE_OPTIONS, + DEFAULT_ZONE_TYPE, + DOMAIN, + OPTIONS_ARM, + OPTIONS_ZONES, + PROTOCOL_SERIAL, + PROTOCOL_SOCKET, +) + +EDIT_KEY = "edit_selection" +EDIT_ZONES = "Zones" +EDIT_SETTINGS = "Arming Settings" + +_LOGGER = logging.getLogger(__name__) + + +class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a AlarmDecoder config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + def __init__(self): + """Initialize AlarmDecoder ConfigFlow.""" + self.protocol = None + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for AlarmDecoder.""" + return AlarmDecoderOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if user_input is not None: + self.protocol = user_input[CONF_PROTOCOL] + return await self.async_step_protocol() + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_PROTOCOL): vol.In( + [PROTOCOL_SOCKET, PROTOCOL_SERIAL] + ), + } + ), + ) + + async def async_step_protocol(self, user_input=None): + """Handle AlarmDecoder protocol setup.""" + errors = {} + if user_input is not None: + if _device_already_added( + self._async_current_entries(), user_input, self.protocol + ): + return self.async_abort(reason="already_configured") + connection = {} + if self.protocol == PROTOCOL_SOCKET: + baud = connection[CONF_DEVICE_BAUD] = None + host = connection[CONF_HOST] = user_input[CONF_HOST] + port = connection[CONF_PORT] = user_input[CONF_PORT] + title = f"{host}:{port}" + device = SocketDevice(interface=(host, port)) + if self.protocol == PROTOCOL_SERIAL: + path = connection[CONF_DEVICE_PATH] = user_input[CONF_DEVICE_PATH] + baud = connection[CONF_DEVICE_BAUD] = user_input[CONF_DEVICE_BAUD] + title = path + device = SerialDevice(interface=path) + + controller = AdExt(device) + try: + with controller: + controller.open(baudrate=baud) + return self.async_create_entry( + title=title, data={CONF_PROTOCOL: self.protocol, **connection} + ) + except NoDeviceError: + errors["base"] = "service_unavailable" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception during AlarmDecoder setup") + errors["base"] = "unknown" + + if self.protocol == PROTOCOL_SOCKET: + schema = vol.Schema( + { + vol.Required(CONF_HOST, default=DEFAULT_DEVICE_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_DEVICE_PORT): int, + } + ) + if self.protocol == PROTOCOL_SERIAL: + schema = vol.Schema( + { + vol.Required(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): str, + vol.Required(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): int, + } + ) + + return self.async_show_form( + step_id="protocol", + data_schema=schema, + errors=errors, + ) + + +class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow): + """Handle AlarmDecoder options.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize AlarmDecoder options flow.""" + self.arm_options = config_entry.options.get(OPTIONS_ARM, DEFAULT_ARM_OPTIONS) + self.zone_options = config_entry.options.get( + OPTIONS_ZONES, DEFAULT_ZONE_OPTIONS + ) + self.selected_zone = None + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + if user_input[EDIT_KEY] == EDIT_SETTINGS: + return await self.async_step_arm_settings() + if user_input[EDIT_KEY] == EDIT_ZONES: + return await self.async_step_zone_select() + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required(EDIT_KEY, default=EDIT_SETTINGS): vol.In( + [EDIT_SETTINGS, EDIT_ZONES] + ) + }, + ), + ) + + async def async_step_arm_settings(self, user_input=None): + """Arming options form.""" + if user_input is not None: + return self.async_create_entry( + title="", + data={OPTIONS_ARM: user_input, OPTIONS_ZONES: self.zone_options}, + ) + + return self.async_show_form( + step_id="arm_settings", + data_schema=vol.Schema( + { + vol.Optional( + CONF_ALT_NIGHT_MODE, + default=self.arm_options[CONF_ALT_NIGHT_MODE], + ): bool, + vol.Optional( + CONF_AUTO_BYPASS, default=self.arm_options[CONF_AUTO_BYPASS] + ): bool, + vol.Optional( + CONF_CODE_ARM_REQUIRED, + default=self.arm_options[CONF_CODE_ARM_REQUIRED], + ): bool, + }, + ), + ) + + async def async_step_zone_select(self, user_input=None): + """Zone selection form.""" + errors = _validate_zone_input(user_input) + + if user_input is not None and not errors: + self.selected_zone = str( + int(user_input[CONF_ZONE_NUMBER]) + ) # remove leading zeros + return await self.async_step_zone_details() + + return self.async_show_form( + step_id="zone_select", + data_schema=vol.Schema({vol.Required(CONF_ZONE_NUMBER): str}), + errors=errors, + ) + + async def async_step_zone_details(self, user_input=None): + """Zone details form.""" + errors = _validate_zone_input(user_input) + + if user_input is not None and not errors: + zone_options = self.zone_options.copy() + zone_id = self.selected_zone + zone_options[zone_id] = _fix_input_types(user_input) + + # Delete zone entry if zone_name is omitted + if CONF_ZONE_NAME not in zone_options[zone_id]: + zone_options.pop(zone_id) + + return self.async_create_entry( + title="", + data={OPTIONS_ARM: self.arm_options, OPTIONS_ZONES: zone_options}, + ) + + existing_zone_settings = self.zone_options.get(self.selected_zone, {}) + + return self.async_show_form( + step_id="zone_details", + description_placeholders={CONF_ZONE_NUMBER: self.selected_zone}, + data_schema=vol.Schema( + { + vol.Optional( + CONF_ZONE_NAME, + description={ + "suggested_value": existing_zone_settings.get( + CONF_ZONE_NAME + ) + }, + ): str, + vol.Optional( + CONF_ZONE_TYPE, + default=existing_zone_settings.get( + CONF_ZONE_TYPE, DEFAULT_ZONE_TYPE + ), + ): vol.In(DEVICE_CLASSES), + vol.Optional( + CONF_ZONE_RFID, + description={ + "suggested_value": existing_zone_settings.get( + CONF_ZONE_RFID + ) + }, + ): str, + vol.Optional( + CONF_ZONE_LOOP, + description={ + "suggested_value": existing_zone_settings.get( + CONF_ZONE_LOOP + ) + }, + ): str, + vol.Optional( + CONF_RELAY_ADDR, + description={ + "suggested_value": existing_zone_settings.get( + CONF_RELAY_ADDR + ) + }, + ): str, + vol.Optional( + CONF_RELAY_CHAN, + description={ + "suggested_value": existing_zone_settings.get( + CONF_RELAY_CHAN + ) + }, + ): str, + } + ), + errors=errors, + ) + + +def _validate_zone_input(zone_input): + if not zone_input: + return {} + errors = {} + + # CONF_RELAY_ADDR & CONF_RELAY_CHAN are inclusive + if (CONF_RELAY_ADDR in zone_input and CONF_RELAY_CHAN not in zone_input) or ( + CONF_RELAY_ADDR not in zone_input and CONF_RELAY_CHAN in zone_input + ): + errors["base"] = "relay_inclusive" + + # The following keys must be int + for key in [CONF_ZONE_NUMBER, CONF_ZONE_LOOP, CONF_RELAY_ADDR, CONF_RELAY_CHAN]: + if key in zone_input: + try: + int(zone_input[key]) + except ValueError: + errors[key] = "int" + + # CONF_ZONE_LOOP depends on CONF_ZONE_RFID + if CONF_ZONE_LOOP in zone_input and CONF_ZONE_RFID not in zone_input: + errors[CONF_ZONE_LOOP] = "loop_rfid" + + # CONF_ZONE_LOOP must be 1-4 + if ( + CONF_ZONE_LOOP in zone_input + and zone_input[CONF_ZONE_LOOP].isdigit() + and int(zone_input[CONF_ZONE_LOOP]) not in list(range(1, 5)) + ): + errors[CONF_ZONE_LOOP] = "loop_range" + + return errors + + +def _fix_input_types(zone_input): + """Convert necessary keys to int. + + Since ConfigFlow inputs of type int cannot default to an empty string, we collect the values below as + strings and then convert them to ints. + """ + + for key in [CONF_ZONE_LOOP, CONF_RELAY_ADDR, CONF_RELAY_CHAN]: + if key in zone_input: + zone_input[key] = int(zone_input[key]) + + return zone_input + + +def _device_already_added(current_entries, user_input, protocol): + """Determine if entry has already been added to HA.""" + user_host = user_input.get(CONF_HOST) + user_port = user_input.get(CONF_PORT) + user_path = user_input.get(CONF_DEVICE_PATH) + user_baud = user_input.get(CONF_DEVICE_BAUD) + + for entry in current_entries: + entry_host = entry.data.get(CONF_HOST) + entry_port = entry.data.get(CONF_PORT) + entry_path = entry.data.get(CONF_DEVICE_PATH) + entry_baud = entry.data.get(CONF_DEVICE_BAUD) + + if protocol == PROTOCOL_SOCKET: + if user_host == entry_host and user_port == entry_port: + return True + + if protocol == PROTOCOL_SERIAL: + if user_baud == entry_baud and user_path == entry_path: + return True + + return False diff --git a/homeassistant/components/alarmdecoder/const.py b/homeassistant/components/alarmdecoder/const.py new file mode 100644 index 00000000000..f1bfb66f0d4 --- /dev/null +++ b/homeassistant/components/alarmdecoder/const.py @@ -0,0 +1,49 @@ +"""Constants for the AlarmDecoder component.""" + +CONF_ALT_NIGHT_MODE = "alt_night_mode" +CONF_AUTO_BYPASS = "auto_bypass" +CONF_CODE_ARM_REQUIRED = "code_arm_required" +CONF_DEVICE_BAUD = "device_baudrate" +CONF_DEVICE_PATH = "device_path" +CONF_RELAY_ADDR = "zone_relayaddr" +CONF_RELAY_CHAN = "zone_relaychan" +CONF_ZONE_LOOP = "zone_loop" +CONF_ZONE_NAME = "zone_name" +CONF_ZONE_NUMBER = "zone_number" +CONF_ZONE_RFID = "zone_rfid" +CONF_ZONE_TYPE = "zone_type" + +DATA_AD = "alarmdecoder" +DATA_REMOVE_STOP_LISTENER = "rm_stop_listener" +DATA_REMOVE_UPDATE_LISTENER = "rm_update_listener" +DATA_RESTART = "restart" + +DEFAULT_ALT_NIGHT_MODE = False +DEFAULT_AUTO_BYPASS = False +DEFAULT_CODE_ARM_REQUIRED = True +DEFAULT_DEVICE_BAUD = 115200 +DEFAULT_DEVICE_HOST = "alarmdecoder" +DEFAULT_DEVICE_PATH = "/dev/ttyUSB0" +DEFAULT_DEVICE_PORT = 10000 +DEFAULT_ZONE_TYPE = "window" + +DEFAULT_ARM_OPTIONS = { + CONF_ALT_NIGHT_MODE: DEFAULT_ALT_NIGHT_MODE, + CONF_AUTO_BYPASS: DEFAULT_AUTO_BYPASS, + CONF_CODE_ARM_REQUIRED: DEFAULT_CODE_ARM_REQUIRED, +} +DEFAULT_ZONE_OPTIONS = {} + +DOMAIN = "alarmdecoder" + +OPTIONS_ARM = "arm_options" +OPTIONS_ZONES = "zone_options" + +PROTOCOL_SERIAL = "serial" +PROTOCOL_SOCKET = "socket" + +SIGNAL_PANEL_MESSAGE = "alarmdecoder.panel_message" +SIGNAL_REL_MESSAGE = "alarmdecoder.rel_message" +SIGNAL_RFX_MESSAGE = "alarmdecoder.rfx_message" +SIGNAL_ZONE_FAULT = "alarmdecoder.zone_fault" +SIGNAL_ZONE_RESTORE = "alarmdecoder.zone_restore" diff --git a/homeassistant/components/alarmdecoder/manifest.json b/homeassistant/components/alarmdecoder/manifest.json index ea2c3fb01c8..1697858718d 100644 --- a/homeassistant/components/alarmdecoder/manifest.json +++ b/homeassistant/components/alarmdecoder/manifest.json @@ -3,5 +3,6 @@ "name": "AlarmDecoder", "documentation": "https://www.home-assistant.io/integrations/alarmdecoder", "requirements": ["adext==0.3"], - "codeowners": ["@ajschmidt8"] + "codeowners": ["@ajschmidt8"], + "config_flow": true } diff --git a/homeassistant/components/alarmdecoder/sensor.py b/homeassistant/components/alarmdecoder/sensor.py index 96e5feb532d..4ce953af1d4 100644 --- a/homeassistant/components/alarmdecoder/sensor.py +++ b/homeassistant/components/alarmdecoder/sensor.py @@ -1,26 +1,29 @@ """Support for AlarmDecoder sensors (Shows Panel Display).""" import logging +from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType -from . import SIGNAL_PANEL_MESSAGE +from .const import SIGNAL_PANEL_MESSAGE _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up for AlarmDecoder sensor devices.""" - _LOGGER.debug("AlarmDecoderSensor: setup_platform") +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +): + """Set up for AlarmDecoder sensor.""" - device = AlarmDecoderSensor(hass) - - add_entities([device]) + entity = AlarmDecoderSensor() + async_add_entities([entity]) + return True class AlarmDecoderSensor(Entity): """Representation of an AlarmDecoder keypad.""" - def __init__(self, hass): + def __init__(self): """Initialize the alarm panel.""" self._display = "" self._state = None diff --git a/homeassistant/components/alarmdecoder/services.yaml b/homeassistant/components/alarmdecoder/services.yaml index bcf5a927713..37c7ddf210c 100644 --- a/homeassistant/components/alarmdecoder/services.yaml +++ b/homeassistant/components/alarmdecoder/services.yaml @@ -1,6 +1,9 @@ alarm_keypress: description: Send custom keypresses to the alarm. fields: + entity_id: + description: Name of alarm control panel to deliver keypress. + example: "alarm_control_panel.main" keypress: description: "String to send to the alarm panel." example: "*71" @@ -8,6 +11,9 @@ alarm_keypress: alarm_toggle_chime: description: Send the alarm the toggle chime command. fields: + entity_id: + description: Name of alarm control panel to toggle chime. + example: "alarm_control_panel.main" code: description: A required code to toggle the alarm control panel chime with. example: 1234 diff --git a/homeassistant/components/alarmdecoder/strings.json b/homeassistant/components/alarmdecoder/strings.json new file mode 100644 index 00000000000..73e4cc760f2 --- /dev/null +++ b/homeassistant/components/alarmdecoder/strings.json @@ -0,0 +1,72 @@ +{ + "config": { + "step": { + "user": { + "title": "Choose AlarmDecoder Protocol", + "data": { + "protocol": "Protocol" + } + }, + "protocol": { + "title": "Configure connection settings", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "device_baudrate": "Device Baud Rate", + "device_path": "Device Path" + } + } + }, + "error": { + "service_unavailable": "[%key:common::config_flow::error::cannot_connect%]" + }, + "create_entry": { "default": "Successfully connected to AlarmDecoder." }, + "abort": { + "already_configured": "AlarmDecoder device is already configured." + } + }, + "options": { + "step": { + "init": { + "title": "Configure AlarmDecoder", + "description": "What would you like to edit?", + "data": { + "edit_select": "Edit" + } + }, + "arm_settings": { + "title": "Configure AlarmDecoder", + "data": { + "auto_bypass": "Auto Bypass on Arm", + "code_arm_required": "Code Required for Arming", + "alt_night_mode": "Alternative Night Mode" + } + }, + "zone_select": { + "title": "Configure AlarmDecoder", + "description": "Enter the zone number you'd like to to add, edit, or remove.", + "data": { + "zone_number": "Zone Number" + } + }, + "zone_details": { + "title": "Configure AlarmDecoder", + "description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave Zone Name blank.", + "data": { + "zone_name": "Zone Name", + "zone_type": "Zone Type", + "zone_rfid": "RF Serial", + "zone_loop": "RF Loop", + "zone_relayaddr": "Relay Address", + "zone_relaychan": "Relay Channel" + } + } + }, + "error": { + "relay_inclusive": "Relay Address and Relay Channel are codependent and must be included together.", + "int": "The field below must be an integer.", + "loop_rfid": "RF Loop cannot be used without RF Serial.", + "loop_range": "RF Loop must be an integer between 1 and 4." + } + } +} diff --git a/homeassistant/components/alarmdecoder/translations/en.json b/homeassistant/components/alarmdecoder/translations/en.json new file mode 100644 index 00000000000..8592cde2065 --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/en.json @@ -0,0 +1,72 @@ +{ + "config": { + "step": { + "user": { + "title": "Choose AlarmDecoder Protocol", + "data": { + "protocol": "Protocol" + } + }, + "protocol": { + "title": "Configure connection settings", + "data": { + "host": "Host", + "port": "Port", + "device_baudrate": "Device Baud Rate", + "device_path": "Device Path" + } + } + }, + "error": { + "service_unavailable": "Failed to connect" + }, + "create_entry": { "default": "Successfully connected to AlarmDecoder." }, + "abort": { + "already_configured": "AlarmDecoder device is already configured." + } + }, + "options": { + "step": { + "init": { + "title": "Configure AlarmDecoder", + "description": "What would you like to edit?", + "data": { + "edit_select": "Edit" + } + }, + "arm_settings": { + "title": "Configure AlarmDecoder", + "data": { + "auto_bypass": "Auto Bypass on Arm", + "code_arm_required": "Code Required for Arming", + "alt_night_mode": "Alternative Night Mode" + } + }, + "zone_select": { + "title": "Configure AlarmDecoder", + "description": "Enter the zone number you'd like to to add, edit, or remove.", + "data": { + "zone_number": "Zone Number" + } + }, + "zone_details": { + "title": "Configure AlarmDecoder", + "description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave Zone Name blank.", + "data": { + "zone_name": "Zone Name", + "zone_type": "Zone Type", + "zone_rfid": "RF Serial", + "zone_loop": "RF Loop", + "zone_relayaddr": "Relay Address", + "zone_relaychan": "Relay Channel" + } + } + }, + "error": { + "relay_inclusive": "Relay Address and Relay Channel are codependent and must be included together.", + "int": "The field below must be an integer.", + "loop_rfid": "RF Loop cannot be used without RF Serial.", + "loop_range": "RF Loop must be an integer between 1 and 4." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 86d778db825..bcb1b898754 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -13,6 +13,7 @@ FLOWS = [ "agent_dvr", "airly", "airvisual", + "alarmdecoder", "almond", "ambiclimate", "ambient_station", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5f909427fd0..b425b10cc50 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -50,6 +50,9 @@ accuweather==0.0.10 # homeassistant.components.androidtv adb-shell[async]==0.2.1 +# homeassistant.components.alarmdecoder +adext==0.3 + # homeassistant.components.adguard adguardhome==0.4.2 diff --git a/tests/components/alarmdecoder/test_config_flow.py b/tests/components/alarmdecoder/test_config_flow.py new file mode 100644 index 00000000000..dd7091fd0ef --- /dev/null +++ b/tests/components/alarmdecoder/test_config_flow.py @@ -0,0 +1,424 @@ +"""Test the AlarmDecoder config flow.""" +from alarmdecoder.util import NoDeviceError +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.alarmdecoder import config_flow +from homeassistant.components.alarmdecoder.const import ( + CONF_ALT_NIGHT_MODE, + CONF_AUTO_BYPASS, + CONF_CODE_ARM_REQUIRED, + CONF_DEVICE_BAUD, + CONF_DEVICE_PATH, + CONF_RELAY_ADDR, + CONF_RELAY_CHAN, + CONF_ZONE_LOOP, + CONF_ZONE_NAME, + CONF_ZONE_NUMBER, + CONF_ZONE_RFID, + CONF_ZONE_TYPE, + DEFAULT_ARM_OPTIONS, + DEFAULT_ZONE_OPTIONS, + DOMAIN, + OPTIONS_ARM, + OPTIONS_ZONES, + PROTOCOL_SERIAL, + PROTOCOL_SOCKET, +) +from homeassistant.components.binary_sensor import DEVICE_CLASS_WINDOW +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL +from homeassistant.core import HomeAssistant + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "protocol,connection,baud,title", + [ + ( + PROTOCOL_SOCKET, + { + CONF_HOST: "alarmdecoder123", + CONF_PORT: 10001, + }, + None, + "alarmdecoder123:10001", + ), + ( + PROTOCOL_SERIAL, + { + CONF_DEVICE_PATH: "/dev/ttyUSB123", + CONF_DEVICE_BAUD: 115000, + }, + 115000, + "/dev/ttyUSB123", + ), + ], +) +async def test_setups(hass: HomeAssistant, protocol, connection, baud, title): + """Test flow for setting up the available AlarmDecoder protocols.""" + + 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["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PROTOCOL: protocol}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "protocol" + + with patch("homeassistant.components.alarmdecoder.config_flow.AdExt.open"), patch( + "homeassistant.components.alarmdecoder.config_flow.AdExt.close" + ), patch( + "homeassistant.components.alarmdecoder.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.alarmdecoder.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], connection + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == title + assert result["data"] == { + **connection, + CONF_PROTOCOL: protocol, + CONF_DEVICE_BAUD: baud, + } + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_setup_connection_error(hass: HomeAssistant): + """Test flow for setup with a connection error.""" + + port = 1001 + host = "alarmdecoder" + protocol = PROTOCOL_SOCKET + connection_settings = {CONF_HOST: host, CONF_PORT: port} + + 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["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PROTOCOL: protocol}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "protocol" + + with patch( + "homeassistant.components.alarmdecoder.config_flow.AdExt.open", + side_effect=NoDeviceError, + ), patch("homeassistant.components.alarmdecoder.config_flow.AdExt.close"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], connection_settings + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "service_unavailable"} + + +async def test_options_arm_flow(hass: HomeAssistant): + """Test arm options flow.""" + user_input = { + CONF_ALT_NIGHT_MODE: True, + CONF_AUTO_BYPASS: True, + CONF_CODE_ARM_REQUIRED: True, + } + entry = MockConfigEntry(domain=DOMAIN) + 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={"edit_selection": "Arming Settings"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "arm_settings" + + with patch( + "homeassistant.components.alarmdecoder.async_setup_entry", return_value=True + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=user_input, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert entry.options == { + OPTIONS_ARM: user_input, + OPTIONS_ZONES: DEFAULT_ZONE_OPTIONS, + } + + +async def test_options_zone_flow(hass: HomeAssistant): + """Test options flow for adding/deleting zones.""" + zone_number = "2" + zone_settings = {CONF_ZONE_NAME: "Front Entry", CONF_ZONE_TYPE: DEVICE_CLASS_WINDOW} + entry = MockConfigEntry(domain=DOMAIN) + 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={"edit_selection": "Zones"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "zone_select" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_ZONE_NUMBER: zone_number}, + ) + + with patch( + "homeassistant.components.alarmdecoder.async_setup_entry", return_value=True + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=zone_settings, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert entry.options == { + OPTIONS_ARM: DEFAULT_ARM_OPTIONS, + OPTIONS_ZONES: {zone_number: zone_settings}, + } + + # Make sure zone can be removed... + 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={"edit_selection": "Zones"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "zone_select" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_ZONE_NUMBER: zone_number}, + ) + + with patch( + "homeassistant.components.alarmdecoder.async_setup_entry", return_value=True + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert entry.options == { + OPTIONS_ARM: DEFAULT_ARM_OPTIONS, + OPTIONS_ZONES: {}, + } + + +async def test_options_zone_flow_validation(hass: HomeAssistant): + """Test input validation for zone options flow.""" + zone_number = "2" + zone_settings = {CONF_ZONE_NAME: "Front Entry", CONF_ZONE_TYPE: DEVICE_CLASS_WINDOW} + entry = MockConfigEntry(domain=DOMAIN) + 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={"edit_selection": "Zones"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "zone_select" + + # Zone Number must be int + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_ZONE_NUMBER: "asd"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "zone_select" + assert result["errors"] == {CONF_ZONE_NUMBER: "int"} + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_ZONE_NUMBER: zone_number}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "zone_details" + + # CONF_RELAY_ADDR & CONF_RELAY_CHAN are inclusive + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={**zone_settings, CONF_RELAY_ADDR: "1"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "zone_details" + assert result["errors"] == {"base": "relay_inclusive"} + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={**zone_settings, CONF_RELAY_CHAN: "1"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "zone_details" + assert result["errors"] == {"base": "relay_inclusive"} + + # CONF_RELAY_ADDR, CONF_RELAY_CHAN must be int + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={**zone_settings, CONF_RELAY_ADDR: "abc", CONF_RELAY_CHAN: "abc"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "zone_details" + assert result["errors"] == { + CONF_RELAY_ADDR: "int", + CONF_RELAY_CHAN: "int", + } + + # CONF_ZONE_LOOP depends on CONF_ZONE_RFID + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={**zone_settings, CONF_ZONE_LOOP: "1"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "zone_details" + assert result["errors"] == {CONF_ZONE_LOOP: "loop_rfid"} + + # CONF_ZONE_LOOP must be int + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={**zone_settings, CONF_ZONE_RFID: "rfid123", CONF_ZONE_LOOP: "ab"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "zone_details" + assert result["errors"] == {CONF_ZONE_LOOP: "int"} + + # CONF_ZONE_LOOP must be between [1,4] + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={**zone_settings, CONF_ZONE_RFID: "rfid123", CONF_ZONE_LOOP: "5"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "zone_details" + assert result["errors"] == {CONF_ZONE_LOOP: "loop_range"} + + # All valid settings + with patch( + "homeassistant.components.alarmdecoder.async_setup_entry", return_value=True + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + **zone_settings, + CONF_ZONE_RFID: "rfid123", + CONF_ZONE_LOOP: "2", + CONF_RELAY_ADDR: "12", + CONF_RELAY_CHAN: "1", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert entry.options == { + OPTIONS_ARM: DEFAULT_ARM_OPTIONS, + OPTIONS_ZONES: { + zone_number: { + **zone_settings, + CONF_ZONE_RFID: "rfid123", + CONF_ZONE_LOOP: 2, + CONF_RELAY_ADDR: 12, + CONF_RELAY_CHAN: 1, + } + }, + } + + +@pytest.mark.parametrize( + "protocol,connection", + [ + ( + PROTOCOL_SOCKET, + { + CONF_HOST: "alarmdecoder123", + CONF_PORT: 10001, + }, + ), + ( + PROTOCOL_SERIAL, + { + CONF_DEVICE_PATH: "/dev/ttyUSB123", + CONF_DEVICE_BAUD: 115000, + }, + ), + ], +) +async def test_one_device_allowed(hass, protocol, connection): + """Test that only one AlarmDecoder device is allowed.""" + flow = config_flow.AlarmDecoderFlowHandler() + flow.hass = hass + + MockConfigEntry( + domain=DOMAIN, + data=connection, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PROTOCOL: protocol}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "protocol" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], connection + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" From 36ce8ba79ea092a33e591f688c9a207a3654e77c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2020 14:55:49 -0500 Subject: [PATCH 130/514] 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 1f8c1f151d47723a39c27dbf42fbfea69f855623 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 131/514] 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 cd26384634170aeead9c0ad4f5a2c8d35215d4c0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Sep 2020 22:05:45 +0200 Subject: [PATCH 132/514] 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 46f9c0fb8a1cb5507ec14f61deadd730b97f1a20 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2020 15:26:04 -0500 Subject: [PATCH 133/514] Update phrasing and pin validation for homekit_controller (#40006) --- .../homekit_controller/config_flow.py | 33 ++++- .../homekit_controller/strings.json | 10 +- .../homekit_controller/translations/en.json | 140 +++++++++--------- .../homekit_controller/test_config_flow.py | 10 +- 4 files changed, 110 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 9ca247382c7..9881ef15dcb 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -23,11 +23,29 @@ HOMEKIT_BRIDGE_MODEL = "Home Assistant HomeKit Bridge" PAIRING_FILE = "pairing.json" +MDNS_SUFFIX = "._hap._tcp.local." + PIN_FORMAT = re.compile(r"^(\d{3})-{0,1}(\d{2})-{0,1}(\d{3})$") _LOGGER = logging.getLogger(__name__) +DISALLOWED_CODES = { + "00000000", + "11111111", + "22222222", + "33333333", + "44444444", + "55555555", + "66666666", + "77777777", + "88888888", + "99999999", + "12345678", + "87654321", +} + + def normalize_hkid(hkid): """Normalize a hkid so that it is safe to compare with other normalized hkids.""" return hkid.lower() @@ -49,9 +67,12 @@ def ensure_pin_format(pin): If incorrect code is entered, an exception is raised. """ - match = PIN_FORMAT.search(pin) + match = PIN_FORMAT.search(pin.strip()) if not match: raise aiohomekit.exceptions.MalformedPinError(f"Invalid PIN code f{pin}") + pin_without_dashes = "".join(match.groups()) + if pin_without_dashes in DISALLOWED_CODES: + raise aiohomekit.exceptions.MalformedPinError(f"Invalid PIN code f{pin}") return "-".join(match.groups()) @@ -66,6 +87,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): """Initialize the homekit_controller flow.""" self.model = None self.hkid = None + self.name = None self.devices = {} self.controller = None self.finish_pairing = None @@ -83,9 +105,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): key = user_input["device"] self.hkid = self.devices[key].device_id self.model = self.devices[key].info["md"] + self.name = key[: -len(MDNS_SUFFIX)] if key.endswith(MDNS_SUFFIX) else key await self.async_set_unique_id( normalize_hkid(self.hkid), raise_on_progress=False ) + return await self.async_step_pair() if self.controller is None: @@ -222,7 +246,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["hkid"] = hkid - self.context["title_placeholders"] = {"name": name} if paired: # Device is paired but not to us - ignore it @@ -235,6 +258,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): if await self._hkid_is_homekit_bridge(hkid): return self.async_abort(reason="ignored_model") + self.name = name self.model = model self.hkid = hkid @@ -355,9 +379,14 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): @callback def _async_step_pair_show_form(self, errors=None): + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + placeholders = {"name": self.name} + self.context["title_placeholders"] = {"name": self.name} + return self.async_show_form( step_id="pair", errors=errors or {}, + description_placeholders=placeholders, data_schema=vol.Schema( {vol.Required("pairing_code"): vol.All(str, vol.Strip)} ), diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index 3f9f93450e0..62fb51709bc 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -1,18 +1,18 @@ { "title": "HomeKit Controller", "config": { - "flow_title": "HomeKit Accessory: {name}", + "flow_title": "{name} via HomeKit Accessory Protocol", "step": { "user": { - "title": "Pair with HomeKit Accessory", - "description": "Select the device you want to pair with", + "title": "Device selection", + "description": "HomeKit Controller communicates over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. 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", + "title": "Pair with a device via HomeKit Accessory Protocol", + "description": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.", "data": { "pairing_code": "Pairing Code" } diff --git a/homeassistant/components/homekit_controller/translations/en.json b/homeassistant/components/homekit_controller/translations/en.json index eb0a5cbf6b6..62fb51709bc 100644 --- a/homeassistant/components/homekit_controller/translations/en.json +++ b/homeassistant/components/homekit_controller/translations/en.json @@ -1,77 +1,71 @@ { - "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" - } + "title": "HomeKit Controller", + "config": { + "flow_title": "{name} via HomeKit Accessory Protocol", + "step": { + "user": { + "title": "Device selection", + "description": "HomeKit Controller communicates over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Select the device you want to pair with:", + "data": { + "device": "Device" } - }, - "device_automation": { - "trigger_subtype": { - "button1": "Button 1", - "button10": "Button 10", - "button2": "Button 2", - "button3": "Button 3", - "button4": "Button 4", - "button5": "Button 5", - "button6": "Button 6", - "button7": "Button 7", - "button8": "Button 8", - "button9": "Button 9", - "doorbell": "Doorbell" - }, - "trigger_type": { - "double_press": "\"{subtype}\" pressed twice", - "long_press": "\"{subtype}\" pressed and held", - "single_press": "\"{subtype}\" pressed" + }, + "pair": { + "title": "Pair with a device via HomeKit Accessory Protocol", + "description": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.", + "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." + } }, - "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.", + "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.", + "invalid_properties": "Invalid properties announced by device.", + "already_in_progress": "Config flow for device is already in progress." + } + }, + "device_automation": { + "trigger_type": { + "single_press": "\"{subtype}\" pressed", + "double_press": "\"{subtype}\" pressed twice", + "long_press": "\"{subtype}\" pressed and held" + }, + "trigger_subtype": { + "doorbell": "Doorbell", + "button1": "Button 1", + "button2": "Button 2", + "button3": "Button 3", + "button4": "Button 4", + "button5": "Button 5", + "button6": "Button 6", + "button7": "Button 7", + "button8": "Button 8", + "button9": "Button 9", + "button10": "Button 10" + } + } +} diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index e5c8e381a5f..a8eb869abf4 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -52,14 +52,17 @@ INVALID_PAIRING_CODES = [ "111-11-111 ", "111-11-111a", "1111111", + "22222222", ] VALID_PAIRING_CODES = [ - "111-11-111", - "123-45-678", - "11111111", + "114-11-111", + "123-45-679", + "123-45-679 ", + "11121111", "98765432", + " 98765432 ", ] @@ -548,6 +551,7 @@ async def test_user_works(hass, controller): assert get_flow_context(hass, result) == { "source": "user", "unique_id": "00:00:00:00:00:00", + "title_placeholders": {"name": "TestDevice"}, } result = await hass.config_entries.flow.async_configure( From 00acb180d618472d71e66fdc9f7a533ca4b5c75f Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 13 Sep 2020 16:36:26 -0500 Subject: [PATCH 134/514] Add canary alarm_control_panel tests (#40029) * add canary alarm_control_panel tests * Update .coveragerc * Create test_alarm_control_panel.py * Update __init__.py * Update __init__.py * Update test_alarm_control_panel.py * Update test_alarm_control_panel.py * Update test_alarm_control_panel.py * Update test_alarm_control_panel.py * Update __init__.py * Update __init__.py * Update test_alarm_control_panel.py * Update test_alarm_control_panel.py * Update test_alarm_control_panel.py * Update test_alarm_control_panel.py * Update test_alarm_control_panel.py * Update test_alarm_control_panel.py * Update test_alarm_control_panel.py * Update test_alarm_control_panel.py * Update test_alarm_control_panel.py * Update test_alarm_control_panel.py --- .coveragerc | 1 - tests/components/canary/__init__.py | 24 +-- .../canary/test_alarm_control_panel.py | 170 ++++++++++++++++++ 3 files changed, 175 insertions(+), 20 deletions(-) create mode 100644 tests/components/canary/test_alarm_control_panel.py diff --git a/.coveragerc b/.coveragerc index 5a360e89226..c95f47815e8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -121,7 +121,6 @@ omit = homeassistant/components/buienradar/util.py homeassistant/components/buienradar/weather.py homeassistant/components/caldav/calendar.py - homeassistant/components/canary/alarm_control_panel.py homeassistant/components/canary/camera.py homeassistant/components/cast/* homeassistant/components/cert_expiry/helper.py diff --git a/tests/components/canary/__init__.py b/tests/components/canary/__init__.py index 2ad275165d6..e8effcb4c3f 100644 --- a/tests/components/canary/__init__.py +++ b/tests/components/canary/__init__.py @@ -3,24 +3,6 @@ from unittest.mock import MagicMock, PropertyMock from canary.api import SensorType -from homeassistant.components.homeassistant import ( - DOMAIN as HA_DOMAIN, - SERVICE_UPDATE_ENTITY, -) -from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import HomeAssistant - - -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, - ) - await hass.async_block_till_done() - def mock_device(device_id, name, is_online=True, device_type_name=None): """Mock Canary Device class.""" @@ -34,13 +16,17 @@ def mock_device(device_id, name, is_online=True, device_type_name=None): return device -def mock_location(location_id, name, is_celsius=True, devices=None): +def mock_location( + location_id, name, is_celsius=True, devices=None, mode=None, is_private=False +): """Mock Canary Location class.""" location = MagicMock() type(location).location_id = PropertyMock(return_value=location_id) type(location).name = PropertyMock(return_value=name) type(location).is_celsius = PropertyMock(return_value=is_celsius) + type(location).is_private = PropertyMock(return_value=is_private) type(location).devices = PropertyMock(return_value=devices or []) + type(location).mode = PropertyMock(return_value=mode) return location diff --git a/tests/components/canary/test_alarm_control_panel.py b/tests/components/canary/test_alarm_control_panel.py new file mode 100644 index 00000000000..ebf4b0ba385 --- /dev/null +++ b/tests/components/canary/test_alarm_control_panel.py @@ -0,0 +1,170 @@ +"""The tests for the Canary alarm_control_panel platform.""" +from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT + +from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN +from homeassistant.components.canary import DOMAIN +from homeassistant.const import ( + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_UNKNOWN, +) +from homeassistant.setup import async_setup_component + +from . import mock_device, mock_location, mock_mode + +from tests.async_mock import PropertyMock, patch +from tests.common import mock_registry + + +async def test_alarm_control_panel(hass, canary) -> None: + """Test the creation and values of the alarm_control_panel for Canary.""" + await async_setup_component(hass, "persistent_notification", {}) + + registry = mock_registry(hass) + online_device_at_home = mock_device(20, "Dining Room", True, "Canary Pro") + + mocked_location = mock_location( + location_id=100, + name="Home", + is_celsius=True, + is_private=False, + mode=mock_mode(7, "standby"), + devices=[online_device_at_home], + ) + + instance = canary.return_value + instance.get_locations.return_value = [mocked_location] + + config = {DOMAIN: {"username": "test-username", "password": "test-password"}} + with patch( + "homeassistant.components.canary.CANARY_COMPONENTS", ["alarm_control_panel"] + ): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + entity_id = "alarm_control_panel.home" + entity_entry = registry.async_get(entity_id) + assert not entity_entry + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNKNOWN + assert not state.attributes["private"] + + # test private system + type(mocked_location).is_private = PropertyMock(return_value=True) + + await hass.helpers.entity_component.async_update_entity(entity_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ALARM_DISARMED + assert state.attributes["private"] + + type(mocked_location).is_private = PropertyMock(return_value=False) + + # test armed home + type(mocked_location).mode = PropertyMock( + return_value=mock_mode(4, LOCATION_MODE_HOME) + ) + + await hass.helpers.entity_component.async_update_entity(entity_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ALARM_ARMED_HOME + + # test armed away + type(mocked_location).mode = PropertyMock( + return_value=mock_mode(5, LOCATION_MODE_AWAY) + ) + + await hass.helpers.entity_component.async_update_entity(entity_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ALARM_ARMED_AWAY + + # test armed night + type(mocked_location).mode = PropertyMock( + return_value=mock_mode(6, LOCATION_MODE_NIGHT) + ) + + await hass.helpers.entity_component.async_update_entity(entity_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ALARM_ARMED_NIGHT + + +async def test_alarm_control_panel_services(hass, canary) -> None: + """Test the services of the alarm_control_panel for Canary.""" + await async_setup_component(hass, "persistent_notification", {}) + + online_device_at_home = mock_device(20, "Dining Room", True, "Canary Pro") + + mocked_location = mock_location( + location_id=100, + name="Home", + is_celsius=True, + mode=mock_mode(1, "disarmed"), + devices=[online_device_at_home], + ) + + instance = canary.return_value + instance.get_locations.return_value = [mocked_location] + + config = {DOMAIN: {"username": "test-username", "password": "test-password"}} + with patch( + "homeassistant.components.canary.CANARY_COMPONENTS", ["alarm_control_panel"] + ): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + entity_id = "alarm_control_panel.home" + + # test arm away + await hass.services.async_call( + ALARM_DOMAIN, + SERVICE_ALARM_ARM_AWAY, + service_data={"entity_id": entity_id}, + blocking=True, + ) + instance.set_location_mode.assert_called_with(100, LOCATION_MODE_AWAY, False) + + # test arm home + await hass.services.async_call( + ALARM_DOMAIN, + SERVICE_ALARM_ARM_HOME, + service_data={"entity_id": entity_id}, + blocking=True, + ) + instance.set_location_mode.assert_called_with(100, LOCATION_MODE_HOME, False) + + # test arm night + await hass.services.async_call( + ALARM_DOMAIN, + SERVICE_ALARM_ARM_NIGHT, + service_data={"entity_id": entity_id}, + blocking=True, + ) + instance.set_location_mode.assert_called_with(100, LOCATION_MODE_NIGHT, False) + + # test disarm + await hass.services.async_call( + ALARM_DOMAIN, + SERVICE_ALARM_DISARM, + service_data={"entity_id": entity_id}, + blocking=True, + ) + instance.set_location_mode.assert_called_with(100, "disarmed", True) From 7b016063ca80e013d23c05de40bb40d150a8919a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2020 18:06:19 -0500 Subject: [PATCH 135/514] Refactor zeroconf setup to be async (#39955) * Refactor zeroconf setup to be async Most of the setup was calling back to async because we were setting up listeners. Since we only need to jump into the executor to create the zeroconf instance, its much faster to setup in async. In testing this cut the setup time in half or better. * partial revert to after_deps --- homeassistant/components/zeroconf/__init__.py | 112 +++++++++--------- .../cast/test_home_assistant_cast.py | 7 +- tests/components/default_config/test_init.py | 9 +- tests/components/discovery/test_init.py | 4 +- tests/components/homekit/conftest.py | 7 -- tests/components/homekit/test_homekit.py | 1 + tests/components/zeroconf/conftest.py | 11 -- tests/components/zeroconf/test_init.py | 12 +- tests/components/zeroconf/test_usage.py | 5 + tests/conftest.py | 7 ++ 10 files changed, 84 insertions(+), 91 deletions(-) delete mode 100644 tests/components/zeroconf/conftest.py diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 51da3638a9e..68300adbcfe 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,6 +1,6 @@ """Support for exposing Home Assistant via Zeroconf.""" -import asyncio import fnmatch +from functools import partial import ipaddress import logging import socket @@ -81,26 +81,21 @@ CONFIG_SCHEMA = vol.Schema( @singleton(DOMAIN) async def async_get_instance(hass): """Zeroconf instance to be shared with other integrations that use it.""" - return await hass.async_add_executor_job(_get_instance, hass) + return await _async_get_instance(hass) -def _get_instance(hass, default_interface=False, ipv6=True): - """Create an instance.""" +async def _async_get_instance(hass, **zcargs): logging.getLogger("zeroconf").setLevel(logging.NOTSET) - zc_args = {} - if default_interface: - zc_args["interfaces"] = InterfaceChoice.Default - if not ipv6: - zc_args["ip_version"] = IPVersion.V4Only + zeroconf = await hass.async_add_executor_job(partial(HaZeroconf, **zcargs)) - zeroconf = HaZeroconf(**zc_args) + install_multiple_zeroconf_catcher(zeroconf) - def stop_zeroconf(_): + def _stop_zeroconf(_): """Stop Zeroconf.""" zeroconf.ha_close() - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zeroconf) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_zeroconf) return zeroconf @@ -135,24 +130,42 @@ class HaZeroconf(Zeroconf): ha_close = Zeroconf.close -def setup(hass, config): +async def async_setup(hass, config): """Set up Zeroconf and make Home Assistant discoverable.""" zc_config = config.get(DOMAIN, {}) - zeroconf = hass.data[DOMAIN] = _get_instance( - hass, - default_interface=zc_config.get( - CONF_DEFAULT_INTERFACE, DEFAULT_DEFAULT_INTERFACE - ), - ipv6=zc_config.get(CONF_IPV6, DEFAULT_IPV6), + zc_args = {} + if zc_config.get(CONF_DEFAULT_INTERFACE, DEFAULT_DEFAULT_INTERFACE): + zc_args["interfaces"] = InterfaceChoice.Default + if not zc_config.get(CONF_IPV6, DEFAULT_IPV6): + zc_args["ip_version"] = IPVersion.V4Only + + zeroconf = hass.data[DOMAIN] = await _async_get_instance(hass, **zc_args) + + async def _async_zeroconf_hass_start(_event): + """Expose Home Assistant on zeroconf when it starts. + + Wait till started or otherwise HTTP is not up and running. + """ + uuid = await hass.helpers.instance_id.async_get() + await hass.async_add_executor_job( + _register_hass_zc_service, hass, zeroconf, uuid + ) + + async def _async_zeroconf_hass_started(_event): + """Start the service browser.""" + + await _async_start_zeroconf_browser(hass, zeroconf) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_zeroconf_hass_start) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, _async_zeroconf_hass_started ) - install_multiple_zeroconf_catcher(zeroconf) + return True + +def _register_hass_zc_service(hass, zeroconf, uuid): # Get instance UUID - uuid = asyncio.run_coroutine_threadsafe( - hass.helpers.instance_id.async_get(), hass.loop - ).result() - valid_location_name = _truncate_location_name_to_valid(hass.config.location_name) params = { @@ -199,23 +212,25 @@ def setup(hass, config): properties=params, ) - def zeroconf_hass_start(_event): - """Expose Home Assistant on zeroconf when it starts. + _LOGGER.info("Starting Zeroconf broadcast") + try: + zeroconf.register_service(info) + except NonUniqueNameException: + _LOGGER.error( + "Home Assistant instance with identical name present in the local network" + ) - Wait till started or otherwise HTTP is not up and running. - """ - _LOGGER.info("Starting Zeroconf broadcast") - try: - zeroconf.register_service(info) - except NonUniqueNameException: - _LOGGER.error( - "Home Assistant instance with identical name present in the local network" - ) - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, zeroconf_hass_start) +async def _async_start_zeroconf_browser(hass, zeroconf): + """Start the zeroconf browser.""" - zeroconf_types = {} - homekit_models = {} + zeroconf_types = await async_get_zeroconf(hass) + homekit_models = await async_get_homekit(hass) + + types = list(zeroconf_types) + + if HOMEKIT_TYPE not in zeroconf_types: + types.append(HOMEKIT_TYPE) def service_update(zeroconf, service_type, name, state_change): """Service state changed.""" @@ -292,25 +307,8 @@ def setup(hass, config): ) ) - async def zeroconf_hass_started(_event): - """Start the service browser.""" - nonlocal zeroconf_types - nonlocal homekit_models - - zeroconf_types = await async_get_zeroconf(hass) - homekit_models = await async_get_homekit(hass) - - types = list(zeroconf_types) - - if HOMEKIT_TYPE not in zeroconf_types: - types.append(HOMEKIT_TYPE) - - _LOGGER.debug("Starting Zeroconf browser") - HaServiceBrowser(zeroconf, types, handlers=[service_update]) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STARTED, zeroconf_hass_started) - - return True + _LOGGER.debug("Starting Zeroconf browser") + HaServiceBrowser(zeroconf, types, handlers=[service_update]) def handle_homekit(hass, homekit_models, info) -> bool: diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index 2842ddc23f0..2fff760bb70 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -1,4 +1,5 @@ """Test Home Assistant Cast.""" + from homeassistant.components.cast import home_assistant_cast from homeassistant.config import async_process_ha_core_config @@ -6,7 +7,7 @@ from tests.async_mock import patch from tests.common import MockConfigEntry, async_mock_signal -async def test_service_show_view(hass): +async def test_service_show_view(hass, mock_zeroconf): """Test we don't set app id in prod.""" await async_process_ha_core_config( hass, @@ -33,7 +34,7 @@ async def test_service_show_view(hass): assert url_path is None -async def test_service_show_view_dashboard(hass): +async def test_service_show_view_dashboard(hass, mock_zeroconf): """Test casting a specific dashboard.""" await async_process_ha_core_config( hass, @@ -60,7 +61,7 @@ async def test_service_show_view_dashboard(hass): assert url_path == "mock-dashboard" -async def test_use_cloud_url(hass): +async def test_use_cloud_url(hass, mock_zeroconf): """Test that we fall back to cloud url.""" await async_process_ha_core_config( hass, diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index 7edf1dc7d60..6bf9cc44c56 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -6,13 +6,6 @@ from homeassistant.setup import async_setup_component from tests.async_mock import patch -@pytest.fixture(autouse=True) -def mock_zeroconf(): - """Mock zeroconf.""" - with patch("homeassistant.components.zeroconf.HaZeroconf"): - yield - - @pytest.fixture(autouse=True) def mock_ssdp(): """Mock ssdp.""" @@ -34,6 +27,6 @@ def recorder_url_mock(): yield -async def test_setup(hass): +async def test_setup(hass, mock_zeroconf): """Test setup.""" assert await async_setup_component(hass, "default_config", {"foo": "bar"}) diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py index 26f79e3e62f..8003f83d996 100644 --- a/tests/components/discovery/test_init.py +++ b/tests/components/discovery/test_init.py @@ -37,7 +37,9 @@ def netdisco_mock(): async def mock_discovery(hass, discoveries, config=BASE_CONFIG): """Mock discoveries.""" - with patch("homeassistant.components.zeroconf.async_get_instance"): + with patch("homeassistant.components.zeroconf.async_get_instance"), patch( + "homeassistant.components.zeroconf.async_setup", return_value=True + ): assert await async_setup_component(hass, "discovery", config) await hass.async_block_till_done() await hass.async_start() diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 79c15344b17..0cb31e1b701 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -29,10 +29,3 @@ def events(hass): EVENT_HOMEKIT_CHANGED, ha_callback(lambda e: events.append(e)) ) yield events - - -@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/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 757281af1e9..c22d6286e76 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1018,6 +1018,7 @@ async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf): data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT}, options={}, ) + assert await async_setup_component(hass, "zeroconf", {"zeroconf": {}}) system_zc = await zeroconf.async_get_instance(hass) with patch("pyhap.accessory_driver.AccessoryDriver.start_service"), patch( diff --git a/tests/components/zeroconf/conftest.py b/tests/components/zeroconf/conftest.py deleted file mode 100644 index e7d7b030aaf..00000000000 --- a/tests/components/zeroconf/conftest.py +++ /dev/null @@ -1,11 +0,0 @@ -"""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 ae1f6d5fd98..8767953b363 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -9,7 +9,11 @@ from zeroconf import ( from homeassistant.components import zeroconf from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE, CONF_IPV6 -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.generated import zeroconf as zc_gen from homeassistant.setup import async_setup_component @@ -128,7 +132,7 @@ async def test_setup_with_overly_long_url_and_name(hass, mock_zeroconf, caplog): """Test we still setup with long urls and names.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock - ) as mock_service_browser, patch( + ), patch( "homeassistant.components.zeroconf.get_url", return_value="https://this.url.is.way.too.long/very/deep/path/that/will/make/us/go/over/the/maximum/string/length/and/would/cause/zeroconf/to/fail/to/startup/because/the/key/and/value/can/only/be/255/bytes/and/this/string/is/a/bit/longer/than/the/maximum/length/that/we/allow/for/a/value", ), patch.object( @@ -138,10 +142,9 @@ async def test_setup_with_overly_long_url_and_name(hass, mock_zeroconf, caplog): ): mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - assert len(mock_service_browser.mock_calls) == 1 assert "https://this.url.is.way.too.long" in caplog.text assert "German Umlaut" in caplog.text @@ -461,6 +464,7 @@ async def test_info_from_service_with_addresses(hass): async def test_get_instance(hass, mock_zeroconf): """Test we get an instance.""" + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert await hass.components.zeroconf.async_get_instance() is mock_zeroconf hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() diff --git a/tests/components/zeroconf/test_usage.py b/tests/components/zeroconf/test_usage.py index e45a93a38b3..9cf953f4c8d 100644 --- a/tests/components/zeroconf/test_usage.py +++ b/tests/components/zeroconf/test_usage.py @@ -3,12 +3,16 @@ import zeroconf from homeassistant.components.zeroconf import async_get_instance from homeassistant.components.zeroconf.usage import install_multiple_zeroconf_catcher +from homeassistant.setup import async_setup_component from tests.async_mock import Mock, patch +DOMAIN = "zeroconf" + async def test_multiple_zeroconf_instances(hass, mock_zeroconf, caplog): """Test creating multiple zeroconf throws without an integration.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) zeroconf_instance = await async_get_instance(hass) @@ -22,6 +26,7 @@ async def test_multiple_zeroconf_instances(hass, mock_zeroconf, caplog): async def test_multiple_zeroconf_instances_gives_shared(hass, mock_zeroconf, caplog): """Test creating multiple zeroconf gives the shared instance to an integration.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) zeroconf_instance = await async_get_instance(hass) diff --git a/tests/conftest.py b/tests/conftest.py index 9008359e539..d6da979bb5e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -395,6 +395,13 @@ async def mqtt_mock(hass, mqtt_client_mock, mqtt_config): return component +@pytest.fixture +def mock_zeroconf(): + """Mock zeroconf.""" + with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc: + yield mock_zc.return_value + + @pytest.fixture def legacy_patchable_time(): """Allow time to be patchable by using event listeners instead of asyncio loop.""" From ae8c9d82bcf2c5b726164ece816fdae44bc4412f Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 14 Sep 2020 00:22:33 +0100 Subject: [PATCH 136/514] Mark Azure DevOps device as a service (#40044) --- homeassistant/components/azure_devops/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index e08dd4d8559..f72a4c44918 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -126,4 +126,5 @@ class AzureDevOpsDeviceEntity(AzureDevOpsEntity): }, "manufacturer": self.organization, "name": self.project, + "entry_type": "service", } From 9511103e263392bdd8c1c0fc702389576fa8f541 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 13 Sep 2020 18:52:40 -0500 Subject: [PATCH 137/514] Add unique_id to canary alarm_control_panel (#40041) * add unique_id to canary alarm_control_panel * Update test_alarm_control_panel.py * Update alarm_control_panel.py * Apply suggestions from code review Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- homeassistant/components/canary/alarm_control_panel.py | 5 +++++ tests/components/canary/test_alarm_control_panel.py | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index ea0e3078b0c..0677480815b 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -43,6 +43,11 @@ class CanaryAlarm(AlarmControlPanelEntity): location = self._data.get_location(self._location_id) return location.name + @property + def unique_id(self): + """Return the unique ID of the alarm.""" + return str(self._location_id) + @property def state(self): """Return the state of the device.""" diff --git a/tests/components/canary/test_alarm_control_panel.py b/tests/components/canary/test_alarm_control_panel.py index ebf4b0ba385..87522d6ad95 100644 --- a/tests/components/canary/test_alarm_control_panel.py +++ b/tests/components/canary/test_alarm_control_panel.py @@ -50,7 +50,8 @@ async def test_alarm_control_panel(hass, canary) -> None: entity_id = "alarm_control_panel.home" entity_entry = registry.async_get(entity_id) - assert not entity_entry + assert entity_entry + assert entity_entry.unique_id == "100" state = hass.states.get(entity_id) assert state From 9acceda0f845379f9eaa4a7a9a25fbf26e030412 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 14 Sep 2020 00:04:10 +0000 Subject: [PATCH 138/514] [ci skip] Translation update --- .../alarmdecoder/translations/en.json | 138 ++++++++--------- .../alarmdecoder/translations/nl.json | 9 ++ .../homekit_controller/translations/ca.json | 1 + .../homekit_controller/translations/en.json | 141 +++++++++--------- .../homekit_controller/translations/ru.json | 1 + .../synology_dsm/translations/ca.json | 3 +- .../synology_dsm/translations/ru.json | 3 +- .../synology_dsm/translations/zh-Hant.json | 3 +- 8 files changed, 161 insertions(+), 138 deletions(-) create mode 100644 homeassistant/components/alarmdecoder/translations/nl.json diff --git a/homeassistant/components/alarmdecoder/translations/en.json b/homeassistant/components/alarmdecoder/translations/en.json index 8592cde2065..756e1f1479e 100644 --- a/homeassistant/components/alarmdecoder/translations/en.json +++ b/homeassistant/components/alarmdecoder/translations/en.json @@ -1,72 +1,74 @@ { - "config": { - "step": { - "user": { - "title": "Choose AlarmDecoder Protocol", - "data": { - "protocol": "Protocol" + "config": { + "abort": { + "already_configured": "AlarmDecoder device is already configured." + }, + "create_entry": { + "default": "Successfully connected to AlarmDecoder." + }, + "error": { + "service_unavailable": "Failed to connect" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Device Baud Rate", + "device_path": "Device Path", + "host": "Host", + "port": "Port" + }, + "title": "Configure connection settings" + }, + "user": { + "data": { + "protocol": "Protocol" + }, + "title": "Choose AlarmDecoder Protocol" + } } - }, - "protocol": { - "title": "Configure connection settings", - "data": { - "host": "Host", - "port": "Port", - "device_baudrate": "Device Baud Rate", - "device_path": "Device Path" + }, + "options": { + "error": { + "int": "The field below must be an integer.", + "loop_range": "RF Loop must be an integer between 1 and 4.", + "loop_rfid": "RF Loop cannot be used without RF Serial.", + "relay_inclusive": "Relay Address and Relay Channel are codependent and must be included together." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternative Night Mode", + "auto_bypass": "Auto Bypass on Arm", + "code_arm_required": "Code Required for Arming" + }, + "title": "Configure AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Edit" + }, + "description": "What would you like to edit?", + "title": "Configure AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF Loop", + "zone_name": "Zone Name", + "zone_relayaddr": "Relay Address", + "zone_relaychan": "Relay Channel", + "zone_rfid": "RF Serial", + "zone_type": "Zone Type" + }, + "description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave Zone Name blank.", + "title": "Configure AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "Zone Number" + }, + "description": "Enter the zone number you'd like to to add, edit, or remove.", + "title": "Configure AlarmDecoder" + } } - } - }, - "error": { - "service_unavailable": "Failed to connect" - }, - "create_entry": { "default": "Successfully connected to AlarmDecoder." }, - "abort": { - "already_configured": "AlarmDecoder device is already configured." } - }, - "options": { - "step": { - "init": { - "title": "Configure AlarmDecoder", - "description": "What would you like to edit?", - "data": { - "edit_select": "Edit" - } - }, - "arm_settings": { - "title": "Configure AlarmDecoder", - "data": { - "auto_bypass": "Auto Bypass on Arm", - "code_arm_required": "Code Required for Arming", - "alt_night_mode": "Alternative Night Mode" - } - }, - "zone_select": { - "title": "Configure AlarmDecoder", - "description": "Enter the zone number you'd like to to add, edit, or remove.", - "data": { - "zone_number": "Zone Number" - } - }, - "zone_details": { - "title": "Configure AlarmDecoder", - "description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave Zone Name blank.", - "data": { - "zone_name": "Zone Name", - "zone_type": "Zone Type", - "zone_rfid": "RF Serial", - "zone_loop": "RF Loop", - "zone_relayaddr": "Relay Address", - "zone_relaychan": "Relay Channel" - } - } - }, - "error": { - "relay_inclusive": "Relay Address and Relay Channel are codependent and must be included together.", - "int": "The field below must be an integer.", - "loop_rfid": "RF Loop cannot be used without RF Serial.", - "loop_range": "RF Loop must be an integer between 1 and 4." - } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/nl.json b/homeassistant/components/alarmdecoder/translations/nl.json new file mode 100644 index 00000000000..643f7d39d23 --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/nl.json @@ -0,0 +1,9 @@ +{ + "options": { + "step": { + "zone_select": { + "title": "Configureer AlarmDecoder" + } + } + } +} \ 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 e13c4e25fe6..be37879841c 100644 --- a/homeassistant/components/homekit_controller/translations/ca.json +++ b/homeassistant/components/homekit_controller/translations/ca.json @@ -7,6 +7,7 @@ "already_paired": "Aquest accessori ja est\u00e0 vinculat amb un altre dispositiu. Reinicia l'accessori i torna-ho a provar.", "ignored_model": "La disponibilitat de HomeKit per aquest model est\u00e0 bloquejada ja que, de moment, no hi ha una integraci\u00f3 nativa completa.", "invalid_config_entry": "Aquest dispositiu s'est\u00e0 mostrant com a llest per a ser vinculat per\u00f2 ja hi ha una entrada de configuraci\u00f3 conflictiva a Home Assistant que s'ha d'eliminar primer.", + "invalid_properties": "Propietats anunciades pel dispositiu no v\u00e0lides.", "no_devices": "No s'han trobat dispositius desvinculats." }, "error": { diff --git a/homeassistant/components/homekit_controller/translations/en.json b/homeassistant/components/homekit_controller/translations/en.json index 62fb51709bc..ac4b3a13251 100644 --- a/homeassistant/components/homekit_controller/translations/en.json +++ b/homeassistant/components/homekit_controller/translations/en.json @@ -1,71 +1,78 @@ { - "title": "HomeKit Controller", - "config": { - "flow_title": "{name} via HomeKit Accessory Protocol", - "step": { - "user": { - "title": "Device selection", - "description": "HomeKit Controller communicates over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. 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.", + "invalid_properties": "Invalid properties announced by device.", + "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": "{name} via HomeKit Accessory Protocol", + "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": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.", + "title": "Pair with a device via HomeKit Accessory Protocol" + }, + "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": "HomeKit Controller communicates over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Select the device you want to pair with:", + "title": "Device selection" + } } - }, - "pair": { - "title": "Pair with a device via HomeKit Accessory Protocol", - "description": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.", - "data": { - "pairing_code": "Pairing Code" + }, + "device_automation": { + "trigger_subtype": { + "button1": "Button 1", + "button10": "Button 10", + "button2": "Button 2", + "button3": "Button 3", + "button4": "Button 4", + "button5": "Button 5", + "button6": "Button 6", + "button7": "Button 7", + "button8": "Button 8", + "button9": "Button 9", + "doorbell": "Doorbell" + }, + "trigger_type": { + "double_press": "\"{subtype}\" pressed twice", + "long_press": "\"{subtype}\" pressed and held", + "single_press": "\"{subtype}\" pressed" } - }, - "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.", - "invalid_properties": "Invalid properties announced by device.", - "already_in_progress": "Config flow for device is already in progress." - } - }, - "device_automation": { - "trigger_type": { - "single_press": "\"{subtype}\" pressed", - "double_press": "\"{subtype}\" pressed twice", - "long_press": "\"{subtype}\" pressed and held" - }, - "trigger_subtype": { - "doorbell": "Doorbell", - "button1": "Button 1", - "button2": "Button 2", - "button3": "Button 3", - "button4": "Button 4", - "button5": "Button 5", - "button6": "Button 6", - "button7": "Button 7", - "button8": "Button 8", - "button9": "Button 9", - "button10": "Button 10" - } - } -} + "title": "HomeKit Controller" +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/ru.json b/homeassistant/components/homekit_controller/translations/ru.json index 62120d98459..ce1f765feff 100644 --- a/homeassistant/components/homekit_controller/translations/ru.json +++ b/homeassistant/components/homekit_controller/translations/ru.json @@ -7,6 +7,7 @@ "already_paired": "\u042d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 \u0443\u0436\u0435 \u0441\u0432\u044f\u0437\u0430\u043d \u0441 \u0434\u0440\u0443\u0433\u0438\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u0431\u0440\u043e\u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", "ignored_model": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 HomeKit \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u0430, \u0442\u0430\u043a \u043a\u0430\u043a \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u043b\u043d\u0430\u044f \u043d\u0430\u0442\u0438\u0432\u043d\u0430\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f.", "invalid_config_entry": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u0433\u043e\u0442\u043e\u0432\u043e\u0435 \u043a \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044e, \u043d\u043e \u0432 Home Assistant \u0443\u0436\u0435 \u0435\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442\u0443\u044e\u0449\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f \u043d\u0435\u0433\u043e, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u0434\u0430\u043b\u0438\u0442\u044c.", + "invalid_properties": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430, \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c.", "no_devices": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0435 \u0434\u043b\u044f \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f, \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b." }, "error": { diff --git a/homeassistant/components/synology_dsm/translations/ca.json b/homeassistant/components/synology_dsm/translations/ca.json index fef0bca2ce4..e265bc31da7 100644 --- a/homeassistant/components/synology_dsm/translations/ca.json +++ b/homeassistant/components/synology_dsm/translations/ca.json @@ -44,7 +44,8 @@ "step": { "init": { "data": { - "scan_interval": "Minuts entre escanejos" + "scan_interval": "Minuts entre escanejos", + "timeout": "Temps d'espera (segons)" } } } diff --git a/homeassistant/components/synology_dsm/translations/ru.json b/homeassistant/components/synology_dsm/translations/ru.json index c0e5da7a1f1..b332afd131f 100644 --- a/homeassistant/components/synology_dsm/translations/ru.json +++ b/homeassistant/components/synology_dsm/translations/ru.json @@ -44,7 +44,8 @@ "step": { "init": { "data": { - "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043c\u0435\u0436\u0434\u0443 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u043c\u0438 (\u043c\u0438\u043d.)" + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043c\u0435\u0436\u0434\u0443 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u043c\u0438 (\u043c\u0438\u043d.)", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" } } } diff --git a/homeassistant/components/synology_dsm/translations/zh-Hant.json b/homeassistant/components/synology_dsm/translations/zh-Hant.json index eec37c812f8..96f8d5283fd 100644 --- a/homeassistant/components/synology_dsm/translations/zh-Hant.json +++ b/homeassistant/components/synology_dsm/translations/zh-Hant.json @@ -44,7 +44,8 @@ "step": { "init": { "data": { - "scan_interval": "\u6383\u63cf\u9593\u9694\u5206\u6578" + "scan_interval": "\u6383\u63cf\u9593\u9694\u5206\u6578", + "timeout": "\u903e\u6642\uff08\u79d2\uff09" } } } From 056e71266763446b7043d1bb9fdd8821d10de8c5 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 13 Sep 2020 21:29:59 -0500 Subject: [PATCH 139/514] Add device class to canary sensors (#40050) * add device class to canary sensors * Update test_sensor.py * Update sensor.py * Update sensor.py --- homeassistant/components/canary/sensor.py | 32 ++++++++++++++--------- tests/components/canary/test_sensor.py | 24 +++++++++++++---- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 5f1b1fe906b..1af2b5ad135 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -1,9 +1,15 @@ """Support for Canary sensors.""" from canary.api import SensorType -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + TEMP_CELSIUS, +) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.icon import icon_for_battery_level from . import DATA_CANARY @@ -18,13 +24,13 @@ CANARY_PRO = "Canary Pro" CANARY_FLEX = "Canary Flex" # Sensor types are defined like so: -# sensor type name, unit_of_measurement, icon +# sensor type name, unit_of_measurement, icon, device class, products supported SENSOR_TYPES = [ - ["temperature", TEMP_CELSIUS, "mdi:thermometer", [CANARY_PRO]], - ["humidity", PERCENTAGE, "mdi:water-percent", [CANARY_PRO]], - ["air_quality", None, "mdi:weather-windy", [CANARY_PRO]], - ["wifi", "dBm", "mdi:wifi", [CANARY_FLEX]], - ["battery", PERCENTAGE, "mdi:battery-50", [CANARY_FLEX]], + ["temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE, [CANARY_PRO]], + ["humidity", PERCENTAGE, None, DEVICE_CLASS_HUMIDITY, [CANARY_PRO]], + ["air_quality", None, "mdi:weather-windy", None, [CANARY_PRO]], + ["wifi", "dBm", None, DEVICE_CLASS_SIGNAL_STRENGTH, [CANARY_FLEX]], + ["battery", PERCENTAGE, None, DEVICE_CLASS_BATTERY, [CANARY_FLEX]], ] STATE_AIR_QUALITY_NORMAL = "normal" @@ -42,7 +48,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if device.is_online: device_type = device.device_type for sensor_type in SENSOR_TYPES: - if device_type.get("name") in sensor_type[3]: + if device_type.get("name") in sensor_type[4]: devices.append( CanarySensor(data, sensor_type, location, device) ) @@ -83,12 +89,14 @@ class CanarySensor(Entity): """Return the unit of measurement.""" return self._sensor_type[1] + @property + def device_class(self): + """Device class for the sensor.""" + return self._sensor_type[3] + @property def icon(self): """Icon for the sensor.""" - if self.state is not None and self._sensor_type[0] == "battery": - return icon_for_battery_level(battery_level=self.state) - return self._sensor_type[2] @property diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index 02d2dc5cc24..8d785a6ced5 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -6,7 +6,15 @@ from homeassistant.components.canary.sensor import ( STATE_AIR_QUALITY_NORMAL, STATE_AIR_QUALITY_VERY_ABNORMAL, ) -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + TEMP_CELSIUS, +) from homeassistant.setup import async_setup_component from . import mock_device, mock_location, mock_reading @@ -43,15 +51,15 @@ async def test_sensors_pro(hass, canary) -> None: "20_temperature", "21.12", TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE, None, - "mdi:thermometer", ), "home_dining_room_humidity": ( "20_humidity", "50.46", PERCENTAGE, + DEVICE_CLASS_HUMIDITY, None, - "mdi:water-percent", ), "home_dining_room_air_quality": ( "20_air_quality", @@ -156,10 +164,16 @@ async def test_sensors_flex(hass, canary) -> None: "20_battery", "70.46", PERCENTAGE, + DEVICE_CLASS_BATTERY, + None, + ), + "home_dining_room_wifi": ( + "20_wifi", + "-57.0", + "dBm", + DEVICE_CLASS_SIGNAL_STRENGTH, None, - "mdi:battery-70", ), - "home_dining_room_wifi": ("20_wifi", "-57.0", "dBm", None, "mdi:wifi"), } for (sensor_id, data) in sensors.items(): From 2ff3c74fabdf2c7f2dacea2819a5651fc627b42a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2020 21:37:52 -0500 Subject: [PATCH 140/514] Fix intermittently failing dyson test (#40051) --- tests/components/dyson/test_climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/dyson/test_climate.py b/tests/components/dyson/test_climate.py index cca589875aa..296812bb0cf 100644 --- a/tests/components/dyson/test_climate.py +++ b/tests/components/dyson/test_climate.py @@ -494,7 +494,7 @@ async def test_purehotcool_update_state(devices, login, hass): async def test_purehotcool_empty_env_attributes(devices, login, hass): """Test empty environmental state update.""" device = devices.return_value[0] - device.environmental_state.temperature = None + device.environmental_state.temperature = 0 device.environmental_state.humidity = None await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) await hass.async_block_till_done() From bda66d19299ec987c5f28a2e9ee66b5fa7f5a02b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 14 Sep 2020 08:34:18 +0200 Subject: [PATCH 141/514] Upgrade coverage to 5.3.0 (#40056) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index f6e2a2433a1..c0c0c9d9878 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ -r requirements_test_pre_commit.txt asynctest==0.13.0 codecov==2.1.9 -coverage==5.2.1 +coverage==5.3 jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.782 From aad6a24f28379abf0d431e33b612120d352a72ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 14 Sep 2020 09:35:30 +0300 Subject: [PATCH 142/514] Fix vizio async mock fixtures on Python 3.8.0 and .1 (#39926) --- tests/components/vizio/conftest.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index 08e5da5c9e5..c8a9083bb1a 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -22,7 +22,7 @@ from .const import ( MockStartPairingResponse, ) -from tests.async_mock import patch +from tests.async_mock import AsyncMock, patch class MockInput: @@ -53,7 +53,7 @@ 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, + AsyncMock(return_value=UNIQUE_ID), ): yield @@ -83,7 +83,7 @@ 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, + AsyncMock(return_value=True), ): yield @@ -156,7 +156,7 @@ def vizio_cant_connect_fixture(): """Mock vizio device can't connect with valid auth.""" with patch( "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config", - return_value=False, + AsyncMock(return_value=False), ): yield From ad1a71ebc36599c7a0777e4e3fc1e229c3977531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 14 Sep 2020 09:40:59 +0300 Subject: [PATCH 143/514] Don't try to create /test dir in camera tests (#39914) ERROR:homeassistant.components.camera: Can't write image to file: [Errno 13] Permission denied: '/test' --- tests/components/camera/test_init.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index c3db5067b1f..966adc97b67 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -14,7 +14,7 @@ from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from tests.async_mock import PropertyMock, mock_open, patch +from tests.async_mock import Mock, PropertyMock, mock_open, patch from tests.components.camera import common @@ -114,8 +114,9 @@ async def test_snapshot_service(hass, mock_camera): """Test snapshot service.""" mopen = mock_open() - with patch( - "homeassistant.components.camera.open", mopen, create=True + with patch("homeassistant.components.camera.open", mopen, create=True), patch( + "homeassistant.components.camera.os.path.exists", + Mock(spec="os.path.exists", return_value=True), ), patch.object(hass.config, "is_allowed_path", return_value=True): await hass.services.async_call( camera.DOMAIN, From c19b5c5ac3c37ea3b1006f90cb2e1fffa3a908d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 14 Sep 2020 01:48:29 -0500 Subject: [PATCH 144/514] Make recorder block_till_done reliable (#40043) --- homeassistant/components/recorder/__init__.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 9ce950fedfe..27a999e3f2c 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -196,6 +196,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: PurgeTask = namedtuple("PurgeTask", ["keep_days", "repack"]) +class WaitTask: + """An object to insert into the recorder queue to tell it set the _queue_watch event.""" + + class Recorder(threading.Thread): """A threaded recorder class.""" @@ -226,6 +230,7 @@ class Recorder(threading.Thread): self.db_retry_wait = db_retry_wait self.db_integrity_check = db_integrity_check self.async_db_ready = asyncio.Future() + self._queue_watch = threading.Event() self.engine: Any = None self.run_info: Any = None @@ -353,6 +358,9 @@ class Recorder(threading.Thread): if not purge.purge_old_data(self, event.keep_days, event.repack): self.queue.put(PurgeTask(event.keep_days, event.repack)) continue + if isinstance(event, WaitTask): + self._queue_watch.set() + continue if event.event_type == EVENT_TIME_CHANGED: self._keepalive_count += 1 if self._keepalive_count >= KEEPALIVE_TIME: @@ -506,8 +514,9 @@ class Recorder(threading.Thread): after calling this to ensure the data is in the database. """ - while not self.queue.empty(): - time.sleep(0.025) + self._queue_watch.clear() + self.queue.put(WaitTask()) + self._queue_watch.wait() def _setup_connection(self): """Ensure database is ready to fly.""" From 2e1dbe51a4627722b0125cefe72e0486f5d53df3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 14 Sep 2020 01:48:59 -0500 Subject: [PATCH 145/514] Make system_log test reliable (#40049) --- homeassistant/components/system_log/__init__.py | 3 +++ tests/components/system_log/test_init.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index b8d6b1664ac..bb255ba8bf3 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -211,6 +211,8 @@ async def async_setup(hass, config): handler = LogErrorHandler(hass, conf[CONF_MAX_ENTRIES], conf[CONF_FIRE_EVENT]) + hass.data[DOMAIN] = handler + listener = logging.handlers.QueueListener( simple_queue, handler, respect_handler_level=True ) @@ -222,6 +224,7 @@ async def async_setup(hass, config): """Cleanup handler.""" logging.root.removeHandler(queue_handler) listener.stop() + del hass.data[DOMAIN] hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_stop_queue_handler) diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index d3f7447277c..49cd2d8ea8b 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -31,6 +31,8 @@ async def _async_block_until_queue_empty(hass, sq): await hass.async_block_till_done() while not sq.empty(): await asyncio.sleep(0.01) + hass.data[system_log.DOMAIN].acquire() + hass.data[system_log.DOMAIN].release() await hass.async_block_till_done() From eb0af3752c9ca2c5c6f9dd4148d5808458b3495a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 14 Sep 2020 10:18:09 +0300 Subject: [PATCH 146/514] Add more SSDP discovery data and constants (#39984) --- homeassistant/components/ssdp/__init__.py | 18 +++++++++++++++- tests/components/ssdp/test_init.py | 26 +++++++++++++++++------ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 555d68cd5d4..af2ae21dac3 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -17,16 +17,22 @@ SCAN_INTERVAL = timedelta(seconds=60) # Attributes for accessing info from SSDP response ATTR_SSDP_LOCATION = "ssdp_location" ATTR_SSDP_ST = "ssdp_st" +ATTR_SSDP_USN = "ssdp_usn" +ATTR_SSDP_EXT = "ssdp_ext" +ATTR_SSDP_SERVER = "ssdp_server" # Attributes for accessing info from retrieved UPnP device description ATTR_UPNP_DEVICE_TYPE = "deviceType" ATTR_UPNP_FRIENDLY_NAME = "friendlyName" ATTR_UPNP_MANUFACTURER = "manufacturer" ATTR_UPNP_MANUFACTURER_URL = "manufacturerURL" +ATTR_UPNP_MODEL_DESCRIPTION = "modelDescription" ATTR_UPNP_MODEL_NAME = "modelName" ATTR_UPNP_MODEL_NUMBER = "modelNumber" -ATTR_UPNP_PRESENTATION_URL = "presentationURL" +ATTR_UPNP_MODEL_URL = "modelURL" ATTR_UPNP_SERIAL = "serialNumber" ATTR_UPNP_UDN = "UDN" +ATTR_UPNP_UPC = "UPC" +ATTR_UPNP_PRESENTATION_URL = "presentationURL" _LOGGER = logging.getLogger(__name__) @@ -107,6 +113,9 @@ class Scanner: """Process a single entry.""" info = {"st": entry.st} + for key in "usn", "ext", "server": + if key in entry.values: + info[key] = entry.values[key] if entry.location: @@ -165,5 +174,12 @@ def info_from_entry(entry, device_info): } if device_info: info.update(device_info) + info.pop("st", None) + if "usn" in info: + info[ATTR_SSDP_USN] = info.pop("usn") + if "ext" in info: + info[ATTR_SSDP_EXT] = info.pop("ext") + if "server" in info: + info[ATTR_SSDP_SERVER] = info.pop("server") return info diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 6e36778b75d..b6c8266b5da 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -15,7 +15,14 @@ async def test_scan_match_st(hass): scanner = ssdp.Scanner(hass, {"mock-domain": [{"st": "mock-st"}]}) with patch( - "netdisco.ssdp.scan", return_value=[Mock(st="mock-st", location=None)] + "netdisco.ssdp.scan", + return_value=[ + Mock( + st="mock-st", + location=None, + values={"usn": "mock-usn", "server": "mock-server", "ext": ""}, + ) + ], ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: @@ -24,6 +31,13 @@ async def test_scan_match_st(hass): assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"} + assert mock_init.mock_calls[0][2]["data"] == { + ssdp.ATTR_SSDP_ST: "mock-st", + ssdp.ATTR_SSDP_LOCATION: None, + ssdp.ATTR_SSDP_USN: "mock-usn", + ssdp.ATTR_SSDP_SERVER: "mock-server", + ssdp.ATTR_SSDP_EXT: "", + } @pytest.mark.parametrize( @@ -45,7 +59,7 @@ async def test_scan_match_upnp_devicedesc(hass, aioclient_mock, key): with patch( "netdisco.ssdp.scan", - return_value=[Mock(st="mock-st", location="http://1.1.1.1")], + return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})], ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: @@ -82,7 +96,7 @@ async def test_scan_not_all_present(hass, aioclient_mock): with patch( "netdisco.ssdp.scan", - return_value=[Mock(st="mock-st", location="http://1.1.1.1")], + return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})], ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: @@ -118,7 +132,7 @@ async def test_scan_not_all_match(hass, aioclient_mock): with patch( "netdisco.ssdp.scan", - return_value=[Mock(st="mock-st", location="http://1.1.1.1")], + return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})], ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: @@ -135,7 +149,7 @@ async def test_scan_description_fetch_fail(hass, aioclient_mock, exc): with patch( "netdisco.ssdp.scan", - return_value=[Mock(st="mock-st", location="http://1.1.1.1")], + return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})], ): await scanner.async_scan(None) @@ -152,6 +166,6 @@ async def test_scan_description_parse_fail(hass, aioclient_mock): with patch( "netdisco.ssdp.scan", - return_value=[Mock(st="mock-st", location="http://1.1.1.1")], + return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})], ): await scanner.async_scan(None) From 3881e0cb235437bb93fac81899942e7d854a3889 Mon Sep 17 00:00:00 2001 From: Thomas Germain <12560542+thomasgermain@users.noreply.github.com> Date: Mon, 14 Sep 2020 09:42:37 +0200 Subject: [PATCH 147/514] Add temperature and uptime to Synology DSM (#39419) Co-authored-by: Quentame --- .../components/synology_dsm/__init__.py | 8 +++- .../components/synology_dsm/const.py | 39 ++++++++++++++--- .../components/synology_dsm/sensor.py | 43 ++++++++++++++++++- 3 files changed, 80 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 6dcac767e5c..7f6cef46cbc 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -234,6 +234,7 @@ class SynoApi: self._with_security = True self._with_storage = True self._with_utilisation = True + self._with_information = True self._with_surveillance_station = True self._unsub_dispatcher = None @@ -304,11 +305,14 @@ class SynoApi: self._with_utilisation = bool( self._fetching_entities.get(SynoCoreUtilization.API_KEY) ) + self._with_information = bool( + self._fetching_entities.get(SynoDSMInformation.API_KEY) + ) self._with_surveillance_station = bool( self._fetching_entities.get(SynoSurveillanceStation.CAMERA_API_KEY) ) - # Reset not used API + # Reset not used API, information is not reset since it's used in device_info if not self._with_security: self.dsm.reset(self.security) self.security = None @@ -351,7 +355,7 @@ class SynoApi: async def async_update(self, now=None): """Update function for updating API information.""" self._async_setup_api_requests() - await self._hass.async_add_executor_job(self.dsm.update) + await self._hass.async_add_executor_job(self.dsm.update, self._with_information) async_dispatcher_send(self._hass, self.signal_sensor_update) diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index f993e0ece73..0941bcb916b 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -2,6 +2,7 @@ from synology_dsm.api.core.security import SynoCoreSecurity from synology_dsm.api.core.utilization import SynoCoreUtilization +from synology_dsm.api.dsm.information import SynoDSMInformation from synology_dsm.api.storage.storage import SynoStorage from homeassistant.components.binary_sensor import DEVICE_CLASS_SAFETY @@ -9,6 +10,8 @@ from homeassistant.const import ( DATA_MEGABYTES, DATA_RATE_KILOBYTES_PER_SECOND, DATA_TERABYTES, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, PERCENTAGE, ) @@ -213,15 +216,15 @@ STORAGE_VOL_SENSORS = { f"{SynoStorage.API_KEY}:volume_disk_temp_avg": { ENTITY_NAME: "Average Disk Temp", ENTITY_UNIT: None, - ENTITY_ICON: "mdi:thermometer", - ENTITY_CLASS: "temperature", + ENTITY_ICON: None, + ENTITY_CLASS: DEVICE_CLASS_TEMPERATURE, ENTITY_ENABLE: True, }, f"{SynoStorage.API_KEY}:volume_disk_temp_max": { ENTITY_NAME: "Maximum Disk Temp", ENTITY_UNIT: None, - ENTITY_ICON: "mdi:thermometer", - ENTITY_CLASS: "temperature", + ENTITY_ICON: None, + ENTITY_CLASS: DEVICE_CLASS_TEMPERATURE, ENTITY_ENABLE: False, }, } @@ -243,11 +246,33 @@ STORAGE_DISK_SENSORS = { f"{SynoStorage.API_KEY}:disk_temp": { ENTITY_NAME: "Temperature", ENTITY_UNIT: None, - ENTITY_ICON: "mdi:thermometer", - ENTITY_CLASS: "temperature", + ENTITY_ICON: None, + ENTITY_CLASS: DEVICE_CLASS_TEMPERATURE, ENTITY_ENABLE: True, }, } +INFORMATION_SENSORS = { + f"{SynoDSMInformation.API_KEY}:temperature": { + ENTITY_NAME: "temperature", + ENTITY_UNIT: None, + ENTITY_ICON: None, + ENTITY_CLASS: DEVICE_CLASS_TEMPERATURE, + ENTITY_ENABLE: True, + }, + f"{SynoDSMInformation.API_KEY}:uptime": { + ENTITY_NAME: "last boot", + ENTITY_UNIT: None, + ENTITY_ICON: None, + ENTITY_CLASS: DEVICE_CLASS_TIMESTAMP, + ENTITY_ENABLE: False, + }, +} -TEMP_SENSORS_KEYS = ["volume_disk_temp_avg", "volume_disk_temp_max", "disk_temp"] + +TEMP_SENSORS_KEYS = [ + "volume_disk_temp_avg", + "volume_disk_temp_max", + "disk_temp", + "temperature", +] diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 22171fdf2f5..31013451682 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -1,4 +1,7 @@ """Support for Synology DSM sensors.""" +from datetime import timedelta +from typing import Dict + from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DISKS, @@ -10,11 +13,13 @@ from homeassistant.const import ( ) from homeassistant.helpers.temperature import display_temp from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.dt import utcnow -from . import SynologyDSMDeviceEntity, SynologyDSMEntity +from . import SynoApi, SynologyDSMDeviceEntity, SynologyDSMEntity from .const import ( CONF_VOLUMES, DOMAIN, + INFORMATION_SENSORS, STORAGE_DISK_SENSORS, STORAGE_VOL_SENSORS, SYNO_API, @@ -55,6 +60,11 @@ async def async_setup_entry( for sensor_type in STORAGE_DISK_SENSORS ] + entities += [ + SynoDSMInfoSensor(api, sensor_type, INFORMATION_SENSORS[sensor_type]) + for sensor_type in INFORMATION_SENSORS + ] + async_add_entities(entities) @@ -105,3 +115,34 @@ class SynoDSMStorageSensor(SynologyDSMDeviceEntity): return display_temp(self.hass, attr, TEMP_CELSIUS, PRECISION_TENTHS) return attr + + +class SynoDSMInfoSensor(SynologyDSMEntity): + """Representation a Synology information sensor.""" + + def __init__(self, api: SynoApi, entity_type: str, entity_info: Dict[str, str]): + """Initialize the Synology SynoDSMInfoSensor entity.""" + super().__init__(api, entity_type, entity_info) + self._previous_uptime = None + self._last_boot = None + + @property + def state(self): + """Return the state.""" + attr = getattr(self._api.information, self.entity_type) + if attr is None: + return None + + # Temperature + if self.entity_type in TEMP_SENSORS_KEYS: + return display_temp(self.hass, attr, TEMP_CELSIUS, PRECISION_TENTHS) + + if self.entity_type == "uptime": + # reboot happened or entity creation + if self._previous_uptime is None or self._previous_uptime > attr: + last_boot = utcnow() - timedelta(seconds=attr) + self._last_boot = last_boot.replace(microsecond=0).isoformat() + + self._previous_uptime = attr + return self._last_boot + return attr From 6e6d6c65ef81ce1c67837134ee0be23685147848 Mon Sep 17 00:00:00 2001 From: rajlaud <50647620+rajlaud@users.noreply.github.com> Date: Mon, 14 Sep 2020 04:48:16 -0500 Subject: [PATCH 148/514] Improve reproduce_state for media players (#38266) --- .../media_player/reproduce_state.py | 35 +++++++++++-------- .../media_player/test_reproduce_state.py | 5 ++- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index a90e4fffdc1..64955d1913b 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -5,7 +5,6 @@ from typing import Any, Dict, Iterable, Optional from homeassistant.const import ( SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, - SERVICE_MEDIA_SEEK, SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -25,7 +24,6 @@ from .const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_ENQUEUE, - ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_SOUND_MODE, @@ -58,16 +56,18 @@ async def _async_reproduce_states( DOMAIN, service, data, blocking=True, context=context ) - if state.state == STATE_ON: - await call_service(SERVICE_TURN_ON, []) - elif state.state == STATE_OFF: + if state.state == STATE_OFF: await call_service(SERVICE_TURN_OFF, []) - elif state.state == STATE_PLAYING: - await call_service(SERVICE_MEDIA_PLAY, []) - elif state.state == STATE_IDLE: - await call_service(SERVICE_MEDIA_STOP, []) - elif state.state == STATE_PAUSED: - await call_service(SERVICE_MEDIA_PAUSE, []) + # entities that are off have no other attributes to restore + return + + if state.state in [ + STATE_ON, + STATE_PLAYING, + STATE_IDLE, + STATE_PAUSED, + ]: + await call_service(SERVICE_TURN_ON, []) if ATTR_MEDIA_VOLUME_LEVEL in state.attributes: await call_service(SERVICE_VOLUME_SET, [ATTR_MEDIA_VOLUME_LEVEL]) @@ -75,15 +75,14 @@ async def _async_reproduce_states( if ATTR_MEDIA_VOLUME_MUTED in state.attributes: await call_service(SERVICE_VOLUME_MUTE, [ATTR_MEDIA_VOLUME_MUTED]) - if ATTR_MEDIA_SEEK_POSITION in state.attributes: - await call_service(SERVICE_MEDIA_SEEK, [ATTR_MEDIA_SEEK_POSITION]) - if ATTR_INPUT_SOURCE in state.attributes: await call_service(SERVICE_SELECT_SOURCE, [ATTR_INPUT_SOURCE]) if ATTR_SOUND_MODE in state.attributes: await call_service(SERVICE_SELECT_SOUND_MODE, [ATTR_SOUND_MODE]) + already_playing = False + if (ATTR_MEDIA_CONTENT_TYPE in state.attributes) and ( ATTR_MEDIA_CONTENT_ID in state.attributes ): @@ -91,6 +90,14 @@ async def _async_reproduce_states( SERVICE_PLAY_MEDIA, [ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_ENQUEUE], ) + already_playing = True + + if state.state == STATE_PLAYING and not already_playing: + await call_service(SERVICE_MEDIA_PLAY, []) + elif state.state == STATE_IDLE: + await call_service(SERVICE_MEDIA_STOP, []) + elif state.state == STATE_PAUSED: + await call_service(SERVICE_MEDIA_PAUSE, []) async def async_reproduce_states( diff --git a/tests/components/media_player/test_reproduce_state.py b/tests/components/media_player/test_reproduce_state.py index ee2c9be377f..ba0072bc2f8 100644 --- a/tests/components/media_player/test_reproduce_state.py +++ b/tests/components/media_player/test_reproduce_state.py @@ -7,7 +7,6 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_ENQUEUE, - ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_SOUND_MODE, @@ -20,7 +19,6 @@ from homeassistant.components.media_player.reproduce_state import async_reproduc from homeassistant.const import ( SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, - SERVICE_MEDIA_SEEK, SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -53,6 +51,8 @@ ENTITY_2 = "media_player.test2" async def test_state(hass, service, state): """Test that we can turn a state into a service call.""" calls_1 = async_mock_service(hass, DOMAIN, service) + if service != SERVICE_TURN_ON: + async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) await async_reproduce_states(hass, [State(ENTITY_1, state)]) @@ -149,7 +149,6 @@ async def test_attribute_no_state(hass): [ (SERVICE_VOLUME_SET, ATTR_MEDIA_VOLUME_LEVEL), (SERVICE_VOLUME_MUTE, ATTR_MEDIA_VOLUME_MUTED), - (SERVICE_MEDIA_SEEK, ATTR_MEDIA_SEEK_POSITION), (SERVICE_SELECT_SOURCE, ATTR_INPUT_SOURCE), (SERVICE_SELECT_SOUND_MODE, ATTR_SOUND_MODE), ], From 1e5186fe94e81930ef286c32e888618c900f90c3 Mon Sep 17 00:00:00 2001 From: Xiaonan Shen Date: Mon, 14 Sep 2020 18:14:13 +0800 Subject: [PATCH 149/514] Deprecate the synology integration (#39958) * Deprecate the synology integration * Add release to remove synology --- homeassistant/components/synology/camera.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/synology/camera.py b/homeassistant/components/synology/camera.py index 5a619c821dc..4417f72918d 100644 --- a/homeassistant/components/synology/camera.py +++ b/homeassistant/components/synology/camera.py @@ -42,6 +42,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a Synology IP Camera.""" + _LOGGER.warning( + "The Synology integration is deprecated." + " Please use the Synology DSM integration" + " (https://www.home-assistant.io/integrations/synology_dsm/) instead." + " This integration will be removed in version 0.118.0." + ) + verify_ssl = config.get(CONF_VERIFY_SSL) timeout = config.get(CONF_TIMEOUT) From 224fd24898ec260fd5e8dfe52afd58405a661bcf Mon Sep 17 00:00:00 2001 From: Markus Bong <2Fake1987@gmail.com> Date: Mon, 14 Sep 2020 12:24:49 +0200 Subject: [PATCH 150/514] 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 d26160c755cf3487a5a1145549cbf397c0cc6e1d Mon Sep 17 00:00:00 2001 From: Xiaonan Shen Date: Mon, 14 Sep 2020 18:53:01 +0800 Subject: [PATCH 151/514] Add rpi_power integration (#35527) Co-authored-by: Toast Co-authored-by: Martin Hjelmare --- CODEOWNERS | 1 + .../components/rpi_power/__init__.py | 21 ++++++ .../components/rpi_power/binary_sensor.py | 73 +++++++++++++++++++ .../components/rpi_power/config_flow.py | 22 ++++++ homeassistant/components/rpi_power/const.py | 3 + .../components/rpi_power/manifest.json | 13 ++++ .../components/rpi_power/strings.json | 14 ++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/rpi_power/__init__.py | 1 + .../rpi_power/test_binary_sensor.py | 73 +++++++++++++++++++ .../components/rpi_power/test_config_flow.py | 42 +++++++++++ 13 files changed, 270 insertions(+) create mode 100644 homeassistant/components/rpi_power/__init__.py create mode 100644 homeassistant/components/rpi_power/binary_sensor.py create mode 100644 homeassistant/components/rpi_power/config_flow.py create mode 100644 homeassistant/components/rpi_power/const.py create mode 100644 homeassistant/components/rpi_power/manifest.json create mode 100644 homeassistant/components/rpi_power/strings.json create mode 100644 tests/components/rpi_power/__init__.py create mode 100644 tests/components/rpi_power/test_binary_sensor.py create mode 100644 tests/components/rpi_power/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index 203c8741821..ec55887e883 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -357,6 +357,7 @@ homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roku/* @ctalkington homeassistant/components/roomba/* @pschmitt @cyr-ius @shenxn homeassistant/components/roon/* @pavoni +homeassistant/components/rpi_power/* @shenxn @swetoast homeassistant/components/safe_mode/* @home-assistant/core homeassistant/components/saj/* @fredericvl homeassistant/components/salt/* @bjornorri diff --git a/homeassistant/components/rpi_power/__init__.py b/homeassistant/components/rpi_power/__init__.py new file mode 100644 index 00000000000..993d0b313c0 --- /dev/null +++ b/homeassistant/components/rpi_power/__init__.py @@ -0,0 +1,21 @@ +"""The Raspberry Pi Power Supply Checker integration.""" +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Raspberry Pi Power Supply Checker component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Raspberry Pi Power Supply Checker from a config entry.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "binary_sensor") + ) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + return await hass.config_entries.async_forward_entry_unload(entry, "binary_sensor") diff --git a/homeassistant/components/rpi_power/binary_sensor.py b/homeassistant/components/rpi_power/binary_sensor.py new file mode 100644 index 00000000000..79ef36e891a --- /dev/null +++ b/homeassistant/components/rpi_power/binary_sensor.py @@ -0,0 +1,73 @@ +""" +A sensor platform which detects underruns and capped status from the official Raspberry Pi Kernel. + +Minimal Kernel needed is 4.14+ +""" +import logging + +from rpi_bad_power import new_under_voltage + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_PROBLEM, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +_LOGGER = logging.getLogger(__name__) + +DESCRIPTION_NORMALIZED = "Voltage normalized. Everything is working as intended." +DESCRIPTION_UNDER_VOLTAGE = "Under-voltage was detected. Consider getting a uninterruptible power supply for your Raspberry Pi." + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +): + """Set up rpi_power binary sensor.""" + under_voltage = await hass.async_add_executor_job(new_under_voltage) + async_add_entities([RaspberryChargerBinarySensor(under_voltage)], True) + + +class RaspberryChargerBinarySensor(BinarySensorEntity): + """Binary sensor representing the rpi power status.""" + + def __init__(self, under_voltage): + """Initialize the binary sensor.""" + self._under_voltage = under_voltage + self._is_on = None + self._last_is_on = False + + def update(self): + """Update the state.""" + self._is_on = self._under_voltage.get() + if self._is_on != self._last_is_on: + if self._is_on: + _LOGGER.warning(DESCRIPTION_UNDER_VOLTAGE) + else: + _LOGGER.info(DESCRIPTION_NORMALIZED) + self._last_is_on = self._is_on + + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return "rpi_power" # only one sensor possible + + @property + def name(self): + """Return the name of the sensor.""" + return "RPi Power status" + + @property + def is_on(self): + """Return if there is a problem detected.""" + return self._is_on + + @property + def icon(self): + """Return the icon of the sensor.""" + return "mdi:raspberry-pi" + + @property + def device_class(self): + """Return the class of this device.""" + return DEVICE_CLASS_PROBLEM diff --git a/homeassistant/components/rpi_power/config_flow.py b/homeassistant/components/rpi_power/config_flow.py new file mode 100644 index 00000000000..6112bddb7d5 --- /dev/null +++ b/homeassistant/components/rpi_power/config_flow.py @@ -0,0 +1,22 @@ +"""Config flow for Raspberry Pi Power Supply Checker.""" +from rpi_bad_power import new_under_voltage + +from homeassistant import config_entries +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_flow + +from .const import DOMAIN + + +async def _async_supported(hass: HomeAssistant) -> bool: + """Return if the system supports under voltage detection.""" + under_voltage = await hass.async_add_executor_job(new_under_voltage) + return under_voltage is not None + + +config_entry_flow.register_discovery_flow( + DOMAIN, + "Raspberry Pi Power Supply Checker", + _async_supported, + config_entries.CONN_CLASS_LOCAL_POLL, +) diff --git a/homeassistant/components/rpi_power/const.py b/homeassistant/components/rpi_power/const.py new file mode 100644 index 00000000000..98cfc438903 --- /dev/null +++ b/homeassistant/components/rpi_power/const.py @@ -0,0 +1,3 @@ +"""Constants for Raspberry Pi Power Supply Checker.""" + +DOMAIN = "rpi_power" diff --git a/homeassistant/components/rpi_power/manifest.json b/homeassistant/components/rpi_power/manifest.json new file mode 100644 index 00000000000..e0d2a6424e8 --- /dev/null +++ b/homeassistant/components/rpi_power/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "rpi_power", + "name": "Raspberry Pi Power Supply Checker", + "documentation": "https://www.home-assistant.io/integrations/rpi_power", + "codeowners": [ + "@shenxn", + "@swetoast" + ], + "requirements": [ + "rpi-bad-power==0.0.3" + ], + "config_flow": true +} diff --git a/homeassistant/components/rpi_power/strings.json b/homeassistant/components/rpi_power/strings.json new file mode 100644 index 00000000000..a9cd6c2d907 --- /dev/null +++ b/homeassistant/components/rpi_power/strings.json @@ -0,0 +1,14 @@ +{ + "title": "Raspberry Pi Power Supply Checker", + "config": { + "step": { + "confirm": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "no_devices_found": "Can't find the system class needed for this component, make sure that your kernel is recent and the hardware is supported" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index bcb1b898754..21336463393 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -152,6 +152,7 @@ FLOWS = [ "roku", "roomba", "roon", + "rpi_power", "samsungtv", "sense", "sentry", diff --git a/requirements_all.txt b/requirements_all.txt index e49c2e3e6a6..49fa96192ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1923,6 +1923,9 @@ roonapi==0.0.21 # homeassistant.components.rova rova==0.1.0 +# homeassistant.components.rpi_power +rpi-bad-power==0.0.3 + # homeassistant.components.rpi_rf # rpi-rf==0.9.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b425b10cc50..2abc13f7c94 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -901,6 +901,9 @@ roombapy==1.6.1 # homeassistant.components.roon roonapi==0.0.21 +# homeassistant.components.rpi_power +rpi-bad-power==0.0.3 + # homeassistant.components.yamaha rxv==0.6.0 diff --git a/tests/components/rpi_power/__init__.py b/tests/components/rpi_power/__init__.py new file mode 100644 index 00000000000..25705bd854f --- /dev/null +++ b/tests/components/rpi_power/__init__.py @@ -0,0 +1 @@ +"""Tests for rpi_power.""" diff --git a/tests/components/rpi_power/test_binary_sensor.py b/tests/components/rpi_power/test_binary_sensor.py new file mode 100644 index 00000000000..873f654aa3b --- /dev/null +++ b/tests/components/rpi_power/test_binary_sensor.py @@ -0,0 +1,73 @@ +"""Tests for rpi_power binary sensor.""" +from datetime import timedelta +import logging + +from homeassistant.components.rpi_power.binary_sensor import ( + DESCRIPTION_NORMALIZED, + DESCRIPTION_UNDER_VOLTAGE, +) +from homeassistant.components.rpi_power.const import DOMAIN +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.async_mock import MagicMock +from tests.common import MockConfigEntry, async_fire_time_changed, patch + +ENTITY_ID = "binary_sensor.rpi_power_status" + +MODULE = "homeassistant.components.rpi_power.binary_sensor.new_under_voltage" + + +async def _async_setup_component(hass, detected): + mocked_under_voltage = MagicMock() + type(mocked_under_voltage).get = MagicMock(return_value=detected) + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + with patch(MODULE, return_value=mocked_under_voltage): + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + return mocked_under_voltage + + +async def test_new(hass, caplog): + """Test new entry.""" + await _async_setup_component(hass, False) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + assert not any(x.levelno == logging.WARNING for x in caplog.records) + + +async def test_new_detected(hass, caplog): + """Test new entry with under voltage detected.""" + mocked_under_voltage = await _async_setup_component(hass, True) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + assert ( + len( + [ + x + for x in caplog.records + if x.levelno == logging.WARNING + and x.message == DESCRIPTION_UNDER_VOLTAGE + ] + ) + == 1 + ) + + # back to normal + type(mocked_under_voltage).get = MagicMock(return_value=False) + future = dt_util.utcnow() + timedelta(minutes=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_ID) + assert ( + len( + [ + x + for x in caplog.records + if x.levelno == logging.INFO and x.message == DESCRIPTION_NORMALIZED + ] + ) + == 1 + ) diff --git a/tests/components/rpi_power/test_config_flow.py b/tests/components/rpi_power/test_config_flow.py new file mode 100644 index 00000000000..70b384d6b91 --- /dev/null +++ b/tests/components/rpi_power/test_config_flow.py @@ -0,0 +1,42 @@ +"""Tests for rpi_power config flow.""" +from homeassistant.components.rpi_power.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.async_mock import MagicMock +from tests.common import patch + +MODULE = "homeassistant.components.rpi_power.config_flow.new_under_voltage" + + +async def test_setup(hass: HomeAssistant): + """Test setting up manually.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + assert not result["errors"] + + with patch(MODULE, return_value=MagicMock()): + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + + +async def test_not_supported(hass: HomeAssistant): + """Test setting up on not supported system.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + with patch(MODULE, return_value=None): + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "no_devices_found" From f1cc5182f0fadf9ca47ea0f571296b0c3eb62ef7 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 14 Sep 2020 13:18:43 +0100 Subject: [PATCH 152/514] Bump aiohomekit version (regression fix) (#40064) --- 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 06199e9f210..1fb4c05c595 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "requirements": [ - "aiohomekit==0.2.49" + "aiohomekit==0.2.53" ], "zeroconf": [ "_hap._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 49fa96192ba..3db1c659882 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==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 2abc13f7c94..d994cfa2aa8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -106,7 +106,7 @@ aioguardian==1.0.1 aioharmony==0.2.6 # homeassistant.components.homekit_controller -aiohomekit==0.2.49 +aiohomekit==0.2.53 # homeassistant.components.emulated_hue # homeassistant.components.http From 09d437d5319cf1ec4794c3b60b7f1df3319ef88f Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 14 Sep 2020 14:36:08 +0200 Subject: [PATCH 153/514] 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 e90a9940d4619b40365c73b83350744216df6760 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 14 Sep 2020 14:50:39 +0200 Subject: [PATCH 154/514] 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 3ba18550cd59771ea786fe9c4374c403c68f40db Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Sep 2020 15:40:32 +0200 Subject: [PATCH 155/514] 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 15281f468e3abd04520f5017134de7191016f00c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 14 Sep 2020 09:35:19 -0500 Subject: [PATCH 156/514] Extract the icon and state for logbook state changed events (#40039) --- homeassistant/components/logbook/__init__.py | 16 ++++++ tests/components/logbook/test_init.py | 52 ++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 0c7786de90b..476aa62501a 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -26,6 +26,7 @@ from homeassistant.const import ( ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, + ATTR_ICON, ATTR_NAME, ATTR_SERVICE, EVENT_CALL_SERVICE, @@ -60,6 +61,7 @@ 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": "([^"]+)"') +ICON_JSON_EXTRACT = re.compile('"icon": "([^"]+)"') _LOGGER = logging.getLogger(__name__) @@ -321,9 +323,14 @@ def humanify(hass, events, entity_attr_cache, context_lookup): entity_id, domain, event, entity_attr_cache ), "domain": domain, + "state": event.state, "entity_id": entity_id, } + icon = event.attributes_icon + if icon: + data["icon"] = icon + if event.context_user_id: data["context_user_id"] = event.context_user_id @@ -724,6 +731,15 @@ class LazyEventPartialState: self.context_user_id = self._row.context_user_id self.time_fired_minute = self._row.time_fired.minute + @property + def attributes_icon(self): + """Extract the icon from the decoded attributes or json.""" + if self._attributes: + return self._attributes.get(ATTR_ICON) + + result = ICON_JSON_EXTRACT.search(self._row.attributes) + return result and result.group(1) + @property def data_entity_id(self): """Extract the entity id from the decoded data or json.""" diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 5e41f0bce89..745d809ca7f 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -2176,6 +2176,58 @@ async def test_logbook_invalid_entity(hass, hass_client): assert response.status == 500 +async def test_icon_and_state(hass, hass_client): + """Test to ensure state and custom icons are returned.""" + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "logbook", {}) + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + + hass.states.async_set("light.kitchen", STATE_OFF, {"icon": "mdi:chemical-weapon"}) + hass.states.async_set( + "light.kitchen", STATE_ON, {"brightness": 100, "icon": "mdi:security"} + ) + hass.states.async_set( + "light.kitchen", STATE_ON, {"brightness": 200, "icon": "mdi:security"} + ) + hass.states.async_set( + "light.kitchen", STATE_ON, {"brightness": 300, "icon": "mdi:security"} + ) + hass.states.async_set( + "light.kitchen", STATE_ON, {"brightness": 400, "icon": "mdi:security"} + ) + hass.states.async_set("light.kitchen", STATE_OFF, {"icon": "mdi:chemical-weapon"}) + + 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 without filters + response = await client.get(f"/api/logbook/{start_date.isoformat()}") + assert response.status == 200 + response_json = await response.json() + + assert len(response_json) == 3 + assert response_json[0]["domain"] == "homeassistant" + assert response_json[1]["message"] == "turned on" + assert response_json[1]["entity_id"] == "light.kitchen" + assert response_json[1]["icon"] == "mdi:security" + assert response_json[1]["state"] == STATE_ON + assert response_json[2]["message"] == "turned off" + assert response_json[2]["entity_id"] == "light.kitchen" + assert response_json[2]["icon"] == "mdi:chemical-weapon" + assert response_json[2]["state"] == STATE_OFF + + class MockLazyEventPartialState(ha.Event): """Minimal mock of a Lazy event.""" From cacbb2eb1237f78a2471ac9ac4f6fefb69265ee8 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Mon, 14 Sep 2020 10:41:05 -0500 Subject: [PATCH 157/514] Add unique_id to canary camera (#40054) --- homeassistant/components/canary/camera.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 3ba7f094da1..4f8370fb09c 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -72,6 +72,11 @@ class CanaryCamera(Camera): """Return the name of this device.""" return self._device.name + @property + def unique_id(self): + """Return the unique ID of this camera.""" + return str(self._device.device_id) + @property def is_recording(self): """Return true if the device is recording.""" From 7adec2d894350e82639850f7895ffc4e264f624a Mon Sep 17 00:00:00 2001 From: b3nj1 Date: Mon, 14 Sep 2020 12:29:51 -0700 Subject: [PATCH 158/514] 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 949bd8d738b959ba7d9280211fa4096d4c24dba0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 14 Sep 2020 14:43:00 -0500 Subject: [PATCH 159/514] Reduce listener cancelation code in template tracker (#40040) --- homeassistant/helpers/event.py | 57 ++++++++++++++-------------------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 733214749ef..dae93896987 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -61,6 +61,10 @@ TRACK_STATE_REMOVED_DOMAIN_LISTENER = "track_state_removed_domain_listener" TRACK_ENTITY_REGISTRY_UPDATED_CALLBACKS = "track_entity_registry_updated_callbacks" TRACK_ENTITY_REGISTRY_UPDATED_LISTENER = "track_entity_registry_updated_listener" +_TEMPLATE_ALL_LISTENER = "all" +_TEMPLATE_DOMAINS_LISTENER = "domains" +_TEMPLATE_ENTITIES_LISTENER = "entities" + _LOGGER = logging.getLogger(__name__) @@ -553,9 +557,7 @@ class _TrackTemplateResultInfo: 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._listeners: Dict[str, Callable] = {} self._last_result: Dict[Template, Union[str, TemplateError]] = {} self._last_info: Dict[Template, RenderInfo] = {} @@ -584,7 +586,7 @@ class _TrackTemplateResultInfo: def listeners(self) -> Dict: """State changes that will cause a re-render.""" return { - "all": self._all_listener is not None, + "all": _TEMPLATE_ALL_LISTENER in self._listeners, "entities": self._last_entities, "domains": self._last_domains, } @@ -630,52 +632,39 @@ class _TrackTemplateResultInfo: self._setup_entities_listener(self._last_domains, self._last_entities) @callback - def _cancel_domains_listener(self) -> None: - if self._domains_listener is None: + def _cancel_listener(self, listener_name: str) -> None: + if listener_name not in self._listeners: return - self._domains_listener() - self._domains_listener = None - @callback - def _cancel_entities_listener(self) -> None: - if self._entities_listener is None: - return - self._entities_listener() - self._entities_listener = None - - @callback - def _cancel_all_listener(self) -> None: - if self._all_listener is None: - return - self._all_listener() - self._all_listener = None + self._listeners.pop(listener_name)() @callback def _update_listeners(self) -> None: + had_all_listener = _TEMPLATE_ALL_LISTENER in self._listeners + if self._needs_all_listener: - if self._all_listener: + if had_all_listener: return self._last_domains = set() self._last_entities = set() - self._cancel_domains_listener() - self._cancel_entities_listener() + self._cancel_listener(_TEMPLATE_DOMAINS_LISTENER) + self._cancel_listener(_TEMPLATE_ENTITIES_LISTENER) self._setup_all_listener() return - had_all_listener = self._all_listener is not None if had_all_listener: - self._cancel_all_listener() + self._cancel_listener(_TEMPLATE_ALL_LISTENER) 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._cancel_listener(_TEMPLATE_DOMAINS_LISTENER) self._setup_domains_listener(domains) if had_all_listener or domains_changed or entities != self._last_entities: - self._cancel_entities_listener() + self._cancel_listener(_TEMPLATE_ENTITIES_LISTENER) self._setup_entities_listener(domains, entities) self._last_domains = domains @@ -691,7 +680,7 @@ class _TrackTemplateResultInfo: if not entities: return - self._entities_listener = async_track_state_change_event( + self._listeners[_TEMPLATE_ENTITIES_LISTENER] = async_track_state_change_event( self.hass, entities, self._refresh ) @@ -700,22 +689,22 @@ class _TrackTemplateResultInfo: if not domains: return - self._domains_listener = async_track_state_added_domain( + self._listeners[_TEMPLATE_DOMAINS_LISTENER] = async_track_state_added_domain( self.hass, domains, self._refresh ) @callback def _setup_all_listener(self) -> None: - self._all_listener = self.hass.bus.async_listen( + self._listeners[_TEMPLATE_ALL_LISTENER] = self.hass.bus.async_listen( EVENT_STATE_CHANGED, self._refresh ) @callback def async_remove(self) -> None: """Cancel the listener.""" - self._cancel_all_listener() - self._cancel_domains_listener() - self._cancel_entities_listener() + self._cancel_listener(_TEMPLATE_ALL_LISTENER) + self._cancel_listener(_TEMPLATE_DOMAINS_LISTENER) + self._cancel_listener(_TEMPLATE_ENTITIES_LISTENER) @callback def async_refresh(self) -> None: From 0f3a2f1f29d8d5f2a810ae4ce58c224f8b0e3070 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Sep 2020 22:10:30 +0200 Subject: [PATCH 160/514] 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 bdaea7879b03f1447cb5f71d3ca33e46d9ee7b75 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 14 Sep 2020 16:48:39 -0400 Subject: [PATCH 161/514] 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 3db1c659882..ad2b37880b7 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 d994cfa2aa8..f5dd74f81c2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -186,7 +186,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 cb0452d80edc7cf970982cc647f7db7dab73027f Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 15 Sep 2020 00:32:20 +0200 Subject: [PATCH 162/514] 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 d0f4b230636ed2ed1940ba44b1f8f97634f07835 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 15 Sep 2020 00:09:18 +0000 Subject: [PATCH 163/514] [ci skip] Translation update --- .../accuweather/translations/pl.json | 2 +- .../components/adguard/translations/pl.json | 2 +- .../alarmdecoder/translations/ca.json | 74 +++++++++++++++++++ .../alarmdecoder/translations/de.json | 41 ++++++++++ .../alarmdecoder/translations/es.json | 74 +++++++++++++++++++ .../alarmdecoder/translations/nl.json | 64 +++++++++++++++- .../alarmdecoder/translations/no.json | 74 +++++++++++++++++++ .../alarmdecoder/translations/pl.json | 16 ++++ .../alarmdecoder/translations/ru.json | 74 +++++++++++++++++++ .../alarmdecoder/translations/zh-Hant.json | 74 +++++++++++++++++++ .../components/almond/translations/es.json | 3 +- .../components/almond/translations/nl.json | 3 +- .../components/almond/translations/no.json | 3 +- .../components/bond/translations/pl.json | 2 +- .../components/control4/translations/pl.json | 2 +- .../components/daikin/translations/pl.json | 5 +- .../components/dexcom/translations/pl.json | 2 +- .../components/directv/translations/pl.json | 2 +- .../components/doorbird/translations/pl.json | 2 +- .../components/dsmr/translations/es.json | 7 ++ .../components/dunehd/translations/pl.json | 2 +- .../components/gogogate2/translations/pl.json | 4 +- .../home_connect/translations/es.json | 3 +- .../home_connect/translations/no.json | 3 +- .../homekit_controller/translations/ca.json | 10 +-- .../homekit_controller/translations/de.json | 20 +++++ .../homekit_controller/translations/es.json | 21 ++++++ .../homekit_controller/translations/nl.json | 21 ++++++ .../homekit_controller/translations/no.json | 31 ++++++-- .../homekit_controller/translations/pl.json | 15 ++++ .../homekit_controller/translations/ru.json | 10 +-- .../translations/zh-Hant.json | 11 +-- .../homematicip_cloud/translations/es.json | 1 + .../homematicip_cloud/translations/nl.json | 1 + .../components/hue/translations/pl.json | 4 +- .../hvv_departures/translations/pl.json | 2 +- .../components/insteon/translations/pl.json | 6 +- .../components/ipp/translations/pl.json | 4 +- .../components/isy994/translations/de.json | 11 +++ .../components/isy994/translations/pl.json | 2 +- .../components/kodi/translations/nl.json | 16 ++++ .../components/kodi/translations/pl.json | 4 +- .../components/metoffice/translations/pl.json | 2 +- .../components/mill/translations/pl.json | 2 +- .../components/netatmo/translations/es.json | 1 + .../components/netatmo/translations/no.json | 1 + .../components/nzbget/translations/de.json | 14 ++++ .../components/nzbget/translations/nl.json | 11 +++ .../openweathermap/translations/de.json | 27 +++++++ .../openweathermap/translations/es.json | 35 +++++++++ .../openweathermap/translations/nl.json | 34 +++++++++ .../ovo_energy/translations/pl.json | 2 +- .../components/pi_hole/translations/pl.json | 2 +- .../components/plugwise/translations/es.json | 10 +++ .../components/plugwise/translations/nl.json | 9 +++ .../plum_lightpad/translations/pl.json | 2 +- .../components/point/translations/es.json | 2 +- .../progettihwsw/translations/nl.json | 12 +++ .../progettihwsw/translations/pl.json | 20 +++++ .../components/remote/translations/de.json | 15 ++++ .../components/remote/translations/es.json | 15 ++++ .../components/remote/translations/nl.json | 15 ++++ .../components/risco/translations/nl.json | 39 ++++++++++ .../components/risco/translations/pl.json | 14 +++- .../components/roku/translations/pl.json | 2 +- .../components/rpi_power/translations/ca.json | 14 ++++ .../components/rpi_power/translations/en.json | 14 ++++ .../components/rpi_power/translations/es.json | 14 ++++ .../components/rpi_power/translations/nl.json | 3 + .../components/rpi_power/translations/pl.json | 12 +++ .../components/rpi_power/translations/ru.json | 14 ++++ .../components/sharkiq/translations/nl.json | 12 +++ .../components/sharkiq/translations/pl.json | 2 +- .../components/shelly/translations/de.json | 3 + .../components/shelly/translations/nl.json | 24 ++++++ .../components/shelly/translations/pl.json | 2 +- .../components/smappee/translations/es.json | 3 +- .../components/smappee/translations/no.json | 3 +- .../smart_meter_texas/translations/pl.json | 20 +++++ .../components/sms/translations/pl.json | 2 +- .../components/somfy/translations/es.json | 3 +- .../components/somfy/translations/no.json | 3 +- .../components/sonarr/translations/pl.json | 2 +- .../components/songpal/translations/pl.json | 2 +- .../components/spotify/translations/es.json | 1 + .../components/spotify/translations/nl.json | 7 +- .../components/spotify/translations/no.json | 1 + .../squeezebox/translations/pl.json | 2 +- .../synology_dsm/translations/es.json | 3 +- .../synology_dsm/translations/nl.json | 9 +++ .../synology_dsm/translations/no.json | 3 +- .../components/tile/translations/de.json | 13 +++- .../components/toon/translations/es.json | 3 +- .../components/toon/translations/nl.json | 3 +- .../components/toon/translations/no.json | 3 +- .../components/tuya/translations/pl.json | 2 +- .../twentemilieu/translations/pl.json | 2 +- .../components/unifi/translations/pl.json | 2 +- .../components/vizio/translations/pl.json | 2 +- .../components/wilight/translations/nl.json | 16 ++++ .../components/wilight/translations/pl.json | 7 ++ .../components/withings/translations/es.json | 3 +- .../components/withings/translations/nl.json | 3 +- .../components/withings/translations/no.json | 3 +- .../components/wled/translations/pl.json | 4 +- .../components/wolflink/translations/pl.json | 2 +- .../xiaomi_miio/translations/pl.json | 2 +- .../components/yeelight/translations/de.json | 20 +++++ .../components/yeelight/translations/es.json | 1 + .../components/yeelight/translations/nl.json | 31 ++++++++ .../components/yeelight/translations/pl.json | 2 +- .../components/zerproc/translations/es.json | 2 +- 112 files changed, 1231 insertions(+), 87 deletions(-) create mode 100644 homeassistant/components/alarmdecoder/translations/ca.json create mode 100644 homeassistant/components/alarmdecoder/translations/de.json create mode 100644 homeassistant/components/alarmdecoder/translations/es.json create mode 100644 homeassistant/components/alarmdecoder/translations/no.json create mode 100644 homeassistant/components/alarmdecoder/translations/pl.json create mode 100644 homeassistant/components/alarmdecoder/translations/ru.json create mode 100644 homeassistant/components/alarmdecoder/translations/zh-Hant.json create mode 100644 homeassistant/components/dsmr/translations/es.json create mode 100644 homeassistant/components/kodi/translations/nl.json create mode 100644 homeassistant/components/nzbget/translations/de.json create mode 100644 homeassistant/components/nzbget/translations/nl.json create mode 100644 homeassistant/components/openweathermap/translations/de.json create mode 100644 homeassistant/components/openweathermap/translations/es.json create mode 100644 homeassistant/components/openweathermap/translations/nl.json create mode 100644 homeassistant/components/progettihwsw/translations/nl.json create mode 100644 homeassistant/components/risco/translations/nl.json create mode 100644 homeassistant/components/rpi_power/translations/ca.json create mode 100644 homeassistant/components/rpi_power/translations/en.json create mode 100644 homeassistant/components/rpi_power/translations/es.json create mode 100644 homeassistant/components/rpi_power/translations/nl.json create mode 100644 homeassistant/components/rpi_power/translations/pl.json create mode 100644 homeassistant/components/rpi_power/translations/ru.json create mode 100644 homeassistant/components/sharkiq/translations/nl.json create mode 100644 homeassistant/components/shelly/translations/de.json create mode 100644 homeassistant/components/shelly/translations/nl.json create mode 100644 homeassistant/components/smart_meter_texas/translations/pl.json create mode 100644 homeassistant/components/wilight/translations/nl.json create mode 100644 homeassistant/components/wilight/translations/pl.json create mode 100644 homeassistant/components/yeelight/translations/de.json create mode 100644 homeassistant/components/yeelight/translations/nl.json diff --git a/homeassistant/components/accuweather/translations/pl.json b/homeassistant/components/accuweather/translations/pl.json index a518c287b11..052ed5b6236 100644 --- a/homeassistant/components/accuweather/translations/pl.json +++ b/homeassistant/components/accuweather/translations/pl.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "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." }, diff --git a/homeassistant/components/adguard/translations/pl.json b/homeassistant/components/adguard/translations/pl.json index ed034fcd1db..2e9ce8c0b3c 100644 --- a/homeassistant/components/adguard/translations/pl.json +++ b/homeassistant/components/adguard/translations/pl.json @@ -5,7 +5,7 @@ "single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja AdGuard Home." }, "error": { - "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/alarmdecoder/translations/ca.json b/homeassistant/components/alarmdecoder/translations/ca.json new file mode 100644 index 00000000000..421ebb20a21 --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/ca.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu AlarmDecoder ja est\u00e0 configurat." + }, + "create_entry": { + "default": "S'ha connectat correctament amb AlarmDecoder." + }, + "error": { + "service_unavailable": "Ha fallat la connexi\u00f3" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Velocitat, en baudis, del dispositiu", + "device_path": "Ruta del dispositiu", + "host": "Amfitri\u00f3", + "port": "Port" + }, + "title": "Configuraci\u00f3 dels par\u00e0metres de connexi\u00f3" + }, + "user": { + "data": { + "protocol": "Protocol" + }, + "title": "Selecciona el protocol d'AlarmDecoder" + } + } + }, + "options": { + "error": { + "int": "El camp seg\u00fcent ha de ser un nombre enter.", + "loop_range": "El bucle RF ha de ser un nombre enter entre 1 i 4.", + "loop_rfid": "El bucle RF no es pot utilitzar sense RF s\u00e8rie.", + "relay_inclusive": "L'adre\u00e7a i el canal de rel\u00e9 s\u00f3n codependents i s'han d'incloure junts." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Mode nocturn alternatiu", + "auto_bypass": "Bypass autom\u00e0tic en l'activaci\u00f3", + "code_arm_required": "Codi necessari per a l'activaci\u00f3" + }, + "title": "Configuraci\u00f3 d'AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Edita" + }, + "description": "Qu\u00e8 voldries editar?", + "title": "Configuraci\u00f3 d'AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "Bucle RF", + "zone_name": "Nom de la zona", + "zone_relayaddr": "Adre\u00e7a del rel\u00e9", + "zone_relaychan": "Canal del rel\u00e9", + "zone_rfid": "RF s\u00e8rie", + "zone_type": "Tipus de zona" + }, + "description": "Introdueix els detalls de la zona {zone_number}. Per suprimir la zona {zone_number}, deixa el nom de la zona en blanc.", + "title": "Configuraci\u00f3 d'AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "N\u00famero de zona" + }, + "description": "Introdueix el n\u00famero de zona que vulguis afegir, editar o eliminar.", + "title": "Configuraci\u00f3 d'AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/de.json b/homeassistant/components/alarmdecoder/translations/de.json new file mode 100644 index 00000000000..cae97cea6a2 --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/de.json @@ -0,0 +1,41 @@ +{ + "config": { + "error": { + "service_unavailable": "Verbindung konnte nicht hergestellt werden" + }, + "step": { + "user": { + "data": { + "protocol": "Protokoll" + } + } + } + }, + "options": { + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternativer Nachtmodus" + } + }, + "init": { + "data": { + "edit_select": "Bearbeiten" + }, + "description": "Was m\u00f6chtest du bearbeiten?" + }, + "zone_details": { + "data": { + "zone_name": "Zonenname", + "zone_type": "Zonentyp" + } + }, + "zone_select": { + "data": { + "zone_number": "Zonennummer" + }, + "description": "Geben Sie die Zonennummer ein, die Sie hinzuf\u00fcgen, bearbeiten oder entfernen m\u00f6chten." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/es.json b/homeassistant/components/alarmdecoder/translations/es.json new file mode 100644 index 00000000000..5b4670306da --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/es.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo AlarmDecoder ya est\u00e1 configurado." + }, + "create_entry": { + "default": "Conectado con \u00e9xito a AlarmDecoder." + }, + "error": { + "service_unavailable": "No se pudo conectar" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Velocidad en baudios del dispositivo", + "device_path": "Ruta del dispositivo", + "host": "Host", + "port": "Puerto" + }, + "title": "Configurar los ajustes de conexi\u00f3n" + }, + "user": { + "data": { + "protocol": "Protocolo" + }, + "title": "Elige el protocolo del AlarmDecoder" + } + } + }, + "options": { + "error": { + "int": "El campo siguiente debe ser un n\u00famero entero.", + "loop_range": "El bucle RF debe ser un n\u00famero entero entre 1 y 4.", + "loop_rfid": "El bucle de RF no puede utilizarse sin el serie RF.", + "relay_inclusive": "La direcci\u00f3n de retransmisi\u00f3n y el canal de retransmisi\u00f3n son codependientes y deben incluirse a la vez." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Modo noche alternativo", + "auto_bypass": "Desv\u00edo autom\u00e1tico al armar", + "code_arm_required": "C\u00f3digo requerido para el armado" + }, + "title": "Configurar AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Editar" + }, + "description": "\u00bfQu\u00e9 te gustar\u00eda editar?", + "title": "Configurar AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "Bucle RF", + "zone_name": "Nombre de zona", + "zone_relayaddr": "Direcci\u00f3n de retransmisi\u00f3n", + "zone_relaychan": "Canal de retransmisi\u00f3n", + "zone_rfid": "Serie RF", + "zone_type": "Tipo de zona" + }, + "description": "Introduce los detalles para la zona {zona_number}. Para borrar la zona {zone_number}, deja el nombre de la zona en blanco.", + "title": "Configurar AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "N\u00famero de zona" + }, + "description": "Introduce el n\u00famero de zona que deseas a\u00f1adir, editar o eliminar.", + "title": "Configurar AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/nl.json b/homeassistant/components/alarmdecoder/translations/nl.json index 643f7d39d23..970016fe8b0 100644 --- a/homeassistant/components/alarmdecoder/translations/nl.json +++ b/homeassistant/components/alarmdecoder/translations/nl.json @@ -1,7 +1,69 @@ { - "options": { + "config": { + "abort": { + "already_configured": "AlarmDecoder-apparaat is al geconfigureerd." + }, + "create_entry": { + "default": "Succesvol verbonden met AlarmDecoder." + }, + "error": { + "service_unavailable": "Kon niet verbinden" + }, "step": { + "protocol": { + "data": { + "device_baudrate": "Baudrate van apparaat", + "host": "Host", + "port": "Poort" + }, + "title": "Configureer de verbindingsinstellingen" + }, + "user": { + "data": { + "protocol": "Protocol" + } + } + } + }, + "options": { + "error": { + "int": "Het onderstaande veld moet een geheel getal zijn.", + "loop_range": "RF Lus moet een geheel getal zijn tussen 1 en 4.", + "loop_rfid": "RF Lus kan niet worden gebruikt zonder RF Serieel.", + "relay_inclusive": "Het relais-adres en het relais-kanaal zijn codeafhankelijk en moeten samen worden opgenomen." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternatieve nachtmodus", + "auto_bypass": "Automatische bypass bij inschakelen", + "code_arm_required": "Code vereist voor inschakelen" + }, + "title": "Configureer AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Bewerk" + }, + "description": "Wat wilt u bewerken?", + "title": "Configureer AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF Lus", + "zone_name": "Zone naam", + "zone_relayaddr": "Relais Adres", + "zone_relaychan": "Relais Kanaal", + "zone_rfid": "RF Serieel", + "zone_type": "Zone Type" + }, + "title": "Configureer AlarmDecoder" + }, "zone_select": { + "data": { + "zone_number": "Zone nummer" + }, + "description": "Voer het zone nummer in dat u wilt toevoegen, bewerken of verwijderen.", "title": "Configureer AlarmDecoder" } } diff --git a/homeassistant/components/alarmdecoder/translations/no.json b/homeassistant/components/alarmdecoder/translations/no.json new file mode 100644 index 00000000000..a3513ef1c18 --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/no.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "AlarmDecoder-enheten er allerede konfigurert." + }, + "create_entry": { + "default": "Vellykket koblet til AlarmDecoder." + }, + "error": { + "service_unavailable": "Tilkobling mislyktes." + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Baud-hastighet for enhet", + "device_path": "Bane til enheten", + "host": "Vert", + "port": "Port" + }, + "title": "Konfigurer tilkoblingsinnstillinger" + }, + "user": { + "data": { + "protocol": "Protokoll" + }, + "title": "Velg AlarmDecoder Protokoll" + } + } + }, + "options": { + "error": { + "int": "Feltet nedenfor m\u00e5 v\u00e6re et helt tall.", + "loop_range": "RF Loop m\u00e5 v\u00e6re et heltall mellom 1 og 4.", + "loop_rfid": "RF Loop kan ikke brukes uten RF Serial.", + "relay_inclusive": "Rel\u00e9adresse og rel\u00e9kanal er kodeavhengige og m\u00e5 inkluderes sammen." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternativ nattmodus", + "auto_bypass": "Auto bypass p\u00e5 Arm", + "code_arm_required": "Kode kreves for tilkobling" + }, + "title": "Konfigurer AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Rediger" + }, + "description": "Hva \u00f8nsker du \u00e5 redigere?", + "title": "Konfigurer AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF Loop", + "zone_name": "Sonenavn", + "zone_relayaddr": "Rel\u00e9 adresse", + "zone_relaychan": "Rel\u00e9 kanal", + "zone_rfid": "RF seriell", + "zone_type": "Sone type" + }, + "description": "Angi detaljer for sonen {zone_number}. Hvis du vil slette sonen {zone_number}, lar du Sonenavn st\u00e5 tomt.", + "title": "Konfigurer AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "Sone nummer" + }, + "description": "Angi sonenummeret du vil legge til, redigere eller fjerne.", + "title": "Konfigurer AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/pl.json b/homeassistant/components/alarmdecoder/translations/pl.json new file mode 100644 index 00000000000..e1bb88d7309 --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/pl.json @@ -0,0 +1,16 @@ +{ + "options": { + "step": { + "zone_details": { + "data": { + "zone_relaychan": "Kana\u0142 przeka\u017anika" + } + }, + "zone_select": { + "data": { + "zone_number": "Numer strefy" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/ru.json b/homeassistant/components/alarmdecoder/translations/ru.json new file mode 100644 index 00000000000..3a6e56686fd --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/ru.json @@ -0,0 +1,74 @@ +{ + "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." + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a AlarmDecoder." + }, + "error": { + "service_unavailable": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "\u0421\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "device_path": "\u041f\u0443\u0442\u044c \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443", + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + }, + "user": { + "data": { + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b AlarmDecoder" + } + } + }, + "options": { + "error": { + "int": "\u041f\u043e\u043b\u0435 \u043d\u0438\u0436\u0435 \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u0446\u0435\u043b\u044b\u043c \u0447\u0438\u0441\u043b\u043e\u043c.", + "loop_range": "RF Loop \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0446\u0435\u043b\u044b\u043c \u0447\u0438\u0441\u043b\u043e\u043c \u043e\u0442 1 \u0434\u043e 4.", + "loop_rfid": "RF Loop \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0431\u0435\u0437 RF Serial.", + "relay_inclusive": "\u0410\u0434\u0440\u0435\u0441 \u0440\u0435\u043b\u0435 \u0438 \u043a\u0430\u043d\u0430\u043b \u0440\u0435\u043b\u0435 \u0432\u0437\u0430\u0438\u043c\u043e\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u044b \u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0432\u043c\u0435\u0441\u0442\u0435." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u043d\u043e\u0447\u043d\u043e\u0439 \u0440\u0435\u0436\u0438\u043c", + "auto_bypass": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0432\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043f\u0440\u0438 \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0435 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443", + "code_arm_required": "\u041a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c" + }, + "description": "\u0427\u0442\u043e \u0431\u044b \u0412\u044b \u0445\u043e\u0442\u0435\u043b\u0438 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c?", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF Loop", + "zone_name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0437\u043e\u043d\u044b", + "zone_relayaddr": "\u0410\u0434\u0440\u0435\u0441 \u0440\u0435\u043b\u0435", + "zone_relaychan": "\u041a\u0430\u043d\u0430\u043b \u0440\u0435\u043b\u0435", + "zone_rfid": "RF Serial", + "zone_type": "\u0422\u0438\u043f \u0437\u043e\u043d\u044b" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0437\u043e\u043d\u044b {zone_number}. \u0427\u0442\u043e\u0431\u044b \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0437\u043e\u043d\u0443 {zone_number}, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \"\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0437\u043e\u043d\u044b\" \u043f\u0443\u0441\u0442\u044b\u043c.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "\u041d\u043e\u043c\u0435\u0440 \u0437\u043e\u043d\u044b" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043d\u043e\u043c\u0435\u0440 \u0437\u043e\u043d\u044b, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c, \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0438\u043b\u0438 \u0443\u0434\u0430\u043b\u0438\u0442\u044c.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/zh-Hant.json b/homeassistant/components/alarmdecoder/translations/zh-Hant.json new file mode 100644 index 00000000000..cae94eb64ef --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/zh-Hant.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "AlarmDecoder \u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "create_entry": { + "default": "\u6210\u529f\u9023\u7dda\u81f3 AlarmDecoder\u3002" + }, + "error": { + "service_unavailable": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "\u8a2d\u5099\u901a\u8a0a\u7387", + "device_path": "\u8a2d\u5099\u8def\u5f91", + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + }, + "title": "\u8a2d\u5b9a\u9023\u7dda\u8a2d\u5b9a" + }, + "user": { + "data": { + "protocol": "\u901a\u8a0a\u5354\u5b9a" + }, + "title": "\u9078\u64c7 AlarmDecoder \u901a\u8a0a\u5354\u5b9a" + } + } + }, + "options": { + "error": { + "int": "\u4e0b\u65b9\u6b04\u4f4d\u5fc5\u9808\u70ba\u6574\u6578\u3002", + "loop_range": "RF \u8ff4\u8def\u5fc5\u9808\u70ba\u4ecb\u65bc 1 \u81f3 4 \u9593\u7684\u6574\u6578\u3002", + "loop_rfid": "\u5982\u679c\u6c92\u6709 RF \u5e8f\u5217\u5247\u7121\u6cd5\u4f7f\u7528 RF \u8ff4\u8def\u3002", + "relay_inclusive": "\u4e2d\u7e7c\u5730\u5740\u8207\u4e2d\u7e7c\u983b\u9053\u70ba\u76f8\u4e92\u4f9d\u8cf4\uff0c\u4e26\u5fc5\u9808\u4e00\u8d77\u5305\u542b\u3002" + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "\u66ff\u4ee3\u591c\u9593\u6a21\u5f0f", + "auto_bypass": "\u81ea\u52d5\u5ffd\u7565\u8b66\u6212", + "code_arm_required": "\u8b66\u6212\u9700\u8981\u4ee3\u78bc" + }, + "title": "\u8a2d\u5b9a AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "\u7de8\u8f2f" + }, + "description": "\u662f\u5426\u8981\u9032\u884c\u7de8\u8f2f\uff1f", + "title": "\u8a2d\u5b9a AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF \u8ff4\u8def", + "zone_name": "\u5340\u57df\u540d\u7a31", + "zone_relayaddr": "\u4e2d\u7e7c\u4f4d\u5740", + "zone_relaychan": "\u4e2d\u7e7c\u983b\u9053", + "zone_rfid": "RF \u5e8f\u5217", + "zone_type": "\u5340\u57df\u985e\u578b" + }, + "description": "\u8f38\u5165\u5340\u57df {zone_number} \u8a73\u7d30\u8cc7\u6599\u3002\u6b32\u522a\u9664\u5340\u57df {zone_number}\uff0c\u4fdd\u6301\u5340\u57df\u540d\u7a31\u7a7a\u767d\u3002", + "title": "\u8a2d\u5b9a AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "\u5340\u57df\u78bc" + }, + "description": "\u8f38\u5165\u6240\u8981\u65b0\u589e\u3001\u7de8\u8f2f\u6216\u79fb\u9664\u7684\u5340\u57df\u78bc\u3002", + "title": "\u8a2d\u5b9a AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/es.json b/homeassistant/components/almond/translations/es.json index de9fb58eabd..94b1f4ea6ba 100644 --- a/homeassistant/components/almond/translations/es.json +++ b/homeassistant/components/almond/translations/es.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "S\u00f3lo puede configurar una cuenta de Almond.", "cannot_connect": "No se puede conectar al servidor Almond.", - "missing_configuration": "Consulte la documentaci\u00f3n sobre c\u00f3mo configurar Almond." + "missing_configuration": "Consulte la documentaci\u00f3n sobre c\u00f3mo configurar Almond.", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/almond/translations/nl.json b/homeassistant/components/almond/translations/nl.json index 7a2a60b1a69..da4671f8591 100644 --- a/homeassistant/components/almond/translations/nl.json +++ b/homeassistant/components/almond/translations/nl.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "U kunt slechts \u00e9\u00e9n Almond-account configureren.", "cannot_connect": "Kan geen verbinding maken met de Almond-server.", - "missing_configuration": "Raadpleeg de documentatie over het instellen van Almond." + "missing_configuration": "Raadpleeg de documentatie over het instellen van Almond.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/almond/translations/no.json b/homeassistant/components/almond/translations/no.json index 3a6a89a8340..c9da3b2303c 100644 --- a/homeassistant/components/almond/translations/no.json +++ b/homeassistant/components/almond/translations/no.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "Du kan bare konfigurere en Almond konto.", "cannot_connect": "Kan ikke koble til Almond-serveren.", - "missing_configuration": "Vennligst sjekk dokumentasjonen om hvordan du setter opp Almond." + "missing_configuration": "Vennligst sjekk dokumentasjonen om hvordan du setter opp Almond.", + "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk {docs_url} ] ( {docs_url} )" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/bond/translations/pl.json b/homeassistant/components/bond/translations/pl.json index 10b6433daee..7bf55561e03 100644 --- a/homeassistant/components/bond/translations/pl.json +++ b/homeassistant/components/bond/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie.", "unknown": "Nieoczekiwany b\u0142\u0105d." }, diff --git a/homeassistant/components/control4/translations/pl.json b/homeassistant/components/control4/translations/pl.json index 3064a0044b1..ae068e1b538 100644 --- a/homeassistant/components/control4/translations/pl.json +++ b/homeassistant/components/control4/translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie.", "unknown": "Nieoczekiwany b\u0142\u0105d." }, diff --git a/homeassistant/components/daikin/translations/pl.json b/homeassistant/components/daikin/translations/pl.json index fc39f78c2d0..e990e3b76a3 100644 --- a/homeassistant/components/daikin/translations/pl.json +++ b/homeassistant/components/daikin/translations/pl.json @@ -1,11 +1,12 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "error": { "device_fail": "Nieoczekiwany b\u0142\u0105d.", - "device_timeout": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "device_timeout": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "forbidden": "Niepoprawne uwierzytelnienie." }, "step": { diff --git a/homeassistant/components/dexcom/translations/pl.json b/homeassistant/components/dexcom/translations/pl.json index 24ae7a17370..f71ac6fb618 100644 --- a/homeassistant/components/dexcom/translations/pl.json +++ b/homeassistant/components/dexcom/translations/pl.json @@ -5,7 +5,7 @@ }, "error": { "account_error": "Niepoprawne uwierzytelnienie.", - "session_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "session_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { diff --git a/homeassistant/components/directv/translations/pl.json b/homeassistant/components/directv/translations/pl.json index bec0198ca70..6c7a1dda53a 100644 --- a/homeassistant/components/directv/translations/pl.json +++ b/homeassistant/components/directv/translations/pl.json @@ -5,7 +5,7 @@ "unknown": "Nieoczekiwany b\u0142\u0105d." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "flow_title": "DirecTV: {name}", "step": { diff --git a/homeassistant/components/doorbird/translations/pl.json b/homeassistant/components/doorbird/translations/pl.json index a24febcd94a..463ac347b5d 100644 --- a/homeassistant/components/doorbird/translations/pl.json +++ b/homeassistant/components/doorbird/translations/pl.json @@ -6,7 +6,7 @@ "not_doorbird_device": "To urz\u0105dzenie nie jest urz\u0105dzeniem DoorBird." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie.", "unknown": "Nieoczekiwany b\u0142\u0105d." }, diff --git a/homeassistant/components/dsmr/translations/es.json b/homeassistant/components/dsmr/translations/es.json new file mode 100644 index 00000000000..e8e23bf8343 --- /dev/null +++ b/homeassistant/components/dsmr/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/dunehd/translations/pl.json b/homeassistant/components/dunehd/translations/pl.json index 2e0e2b352ca..f6f760d6a3e 100644 --- a/homeassistant/components/dunehd/translations/pl.json +++ b/homeassistant/components/dunehd/translations/pl.json @@ -5,7 +5,7 @@ }, "error": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_host": "Nieprawid\u0142owa nazwa hosta lub adres IP." }, "step": { diff --git a/homeassistant/components/gogogate2/translations/pl.json b/homeassistant/components/gogogate2/translations/pl.json index 7a6c33be781..41df279799f 100644 --- a/homeassistant/components/gogogate2/translations/pl.json +++ b/homeassistant/components/gogogate2/translations/pl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie." }, "step": { diff --git a/homeassistant/components/home_connect/translations/es.json b/homeassistant/components/home_connect/translations/es.json index 7457f7487d4..8c60c994df0 100644 --- a/homeassistant/components/home_connect/translations/es.json +++ b/homeassistant/components/home_connect/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "missing_configuration": "El componente Home Connect no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n." + "missing_configuration": "El componente Home Connect no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" }, "create_entry": { "default": "Autenticado correctamente con Home Assistant." diff --git a/homeassistant/components/home_connect/translations/no.json b/homeassistant/components/home_connect/translations/no.json index 908f62efbc9..185ba6264e9 100644 --- a/homeassistant/components/home_connect/translations/no.json +++ b/homeassistant/components/home_connect/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "missing_configuration": "Home Connect-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." + "missing_configuration": "Home Connect-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk {docs_url} ] ( {docs_url} )" }, "create_entry": { "default": "Vellykket godkjenning med Home Connect" diff --git a/homeassistant/components/homekit_controller/translations/ca.json b/homeassistant/components/homekit_controller/translations/ca.json index be37879841c..0f05710a778 100644 --- a/homeassistant/components/homekit_controller/translations/ca.json +++ b/homeassistant/components/homekit_controller/translations/ca.json @@ -20,7 +20,7 @@ "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}", + "flow_title": "{name} a trav\u00e9s de HomeKit Accessory Protocol", "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.", @@ -34,8 +34,8 @@ "data": { "pairing_code": "Codi de vinculaci\u00f3" }, - "description": "Introdueix el codi de vinculaci\u00f3 de HomeKit per utilitzar aquest accessori (format XXX-XX-XXX)", - "title": "Vinculaci\u00f3 amb" + "description": "El controlador HomeKit es comunica amb {name} a trav\u00e9s de la xarxa d'\u00e0rea local utilitzant una connexi\u00f3 segura encriptada sense un HomeKit o iCloud separats. Introdueix el codi de vinculaci\u00f3 de HomeKit (en format XXX-XX-XXX) per utilitzar aquest accessori. Aquest codi es troba normalment en el propi dispositiu o en la seva caixa.", + "title": "Vinculaci\u00f3 amb un dispositiu a trav\u00e9s de HomeKit Accessory Protocol" }, "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.", @@ -49,8 +49,8 @@ "data": { "device": "Dispositiu" }, - "description": "Selecciona el dispositiu amb el qual et vols vincular", - "title": "Vinculaci\u00f3 amb un accessori HomeKit" + "description": "El controlador HomeKit es comunica a trav\u00e9s de la xarxa d'\u00e0rea local utilitzant una connexi\u00f3 segura encriptada sense un HomeKit o iCloud separats. Selecciona el dispositiu amb el qual et vols vincular:", + "title": "Selecci\u00f3 de dispositiu" } } }, diff --git a/homeassistant/components/homekit_controller/translations/de.json b/homeassistant/components/homekit_controller/translations/de.json index b10fb6efe45..6ee03a37f88 100644 --- a/homeassistant/components/homekit_controller/translations/de.json +++ b/homeassistant/components/homekit_controller/translations/de.json @@ -36,5 +36,25 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button1": "Knopf 1", + "button10": "Knopf 10", + "button2": "Knopf 2", + "button3": "Knopf 3", + "button4": "Knopf 4", + "button5": "Knopf 5", + "button6": "Knopf 6", + "button7": "Knopf 7", + "button8": "Knopf 8", + "button9": "Knopf 9", + "doorbell": "T\u00fcrklingel" + }, + "trigger_type": { + "double_press": "\"{subtype}\" zweimal gedr\u00fcckt", + "long_press": "\"{subtype}\" gedr\u00fcckt und gehalten", + "single_press": "\"{subtype}\" gedr\u00fcckt" + } + }, "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 8eb450e6558..b1daa4e50cc 100644 --- a/homeassistant/components/homekit_controller/translations/es.json +++ b/homeassistant/components/homekit_controller/translations/es.json @@ -7,6 +7,7 @@ "already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicia el accesorio e int\u00e9ntalo de nuevo.", "ignored_model": "El soporte de HomeKit para este modelo est\u00e1 bloqueado ya que est\u00e1 disponible una integraci\u00f3n nativa m\u00e1s completa.", "invalid_config_entry": "Este dispositivo se muestra como listo para vincular, pero ya existe una entrada que causa conflicto en Home Assistant y se debe eliminar primero.", + "invalid_properties": "Propiedades no v\u00e1lidas anunciadas por dispositivo.", "no_devices": "No se encontraron dispositivos no emparejados" }, "error": { @@ -53,5 +54,25 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button1": "Bot\u00f3n 1", + "button10": "Bot\u00f3n 10", + "button2": "Bot\u00f3n 2", + "button3": "Bot\u00f3n 3", + "button4": "Bot\u00f3n 4", + "button5": "Bot\u00f3n 5", + "button6": "Bot\u00f3n 6", + "button7": "Bot\u00f3n 7", + "button8": "Bot\u00f3n 8", + "button9": "Bot\u00f3n 9", + "doorbell": "Timbre de la puerta" + }, + "trigger_type": { + "double_press": "\"{subtype}\" pulsado dos veces", + "long_press": "\"{subtype}\" pulsado y mantenido", + "single_press": "\"{subtype}\" pulsado" + } + }, "title": "Accesorio HomeKit" } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/nl.json b/homeassistant/components/homekit_controller/translations/nl.json index 20013168c81..84f5495cca7 100644 --- a/homeassistant/components/homekit_controller/translations/nl.json +++ b/homeassistant/components/homekit_controller/translations/nl.json @@ -7,6 +7,7 @@ "already_paired": "Dit accessoire is al gekoppeld aan een ander apparaat. Reset het accessoire en probeer het opnieuw.", "ignored_model": "HomeKit-ondersteuning voor dit model is geblokkeerd omdat er een meer functie volledige native integratie beschikbaar is.", "invalid_config_entry": "Dit apparaat geeft aan dat het gereed is om te koppelen, maar er is al een conflicterend configuratie-item voor in de Home Assistant dat eerst moet worden verwijderd.", + "invalid_properties": "Ongeldige eigenschappen aangekondigd door apparaat.", "no_devices": "Er zijn geen gekoppelde apparaten gevonden" }, "error": { @@ -36,5 +37,25 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button1": "Knop 1", + "button10": "Knop 10", + "button2": "Knop 2", + "button3": "Knop 3", + "button4": "Knop 4", + "button5": "Knop 5", + "button6": "Knop 6", + "button7": "Knop 7", + "button8": "Knop 8", + "button9": "Knop 9", + "doorbell": "Deurbel" + }, + "trigger_type": { + "double_press": "\" {subtype} \" tweemaal ingedrukt", + "long_press": "\"{subtype}\" ingedrukt en vastgehouden", + "single_press": "\" {subtype} \" ingedrukt" + } + }, "title": "HomeKit Accessoires" } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/no.json b/homeassistant/components/homekit_controller/translations/no.json index 45444d3d9d0..86e247a1572 100644 --- a/homeassistant/components/homekit_controller/translations/no.json +++ b/homeassistant/components/homekit_controller/translations/no.json @@ -7,6 +7,7 @@ "already_paired": "Dette tilbeh\u00f8ret er allerede sammenkoblet med en annen enhet. Vennligst tilbakestill tilbeh\u00f8ret og pr\u00f8v igjen.", "ignored_model": "HomeKit st\u00f8tte for denne modellen er blokkert da en mer funksjonsrik standard integrasjon er tilgjengelig.", "invalid_config_entry": "Denne enheten vises som klar til sammenkobling, men det er allerede en motstridende konfigurasjonsoppf\u00f8ring for den i Hjelpeassistenten som f\u00f8rst m\u00e5 fjernes.", + "invalid_properties": "Ugyldige egenskaper kunngjort av enheten.", "no_devices": "Ingen ukoblede enheter ble funnet" }, "error": { @@ -19,7 +20,7 @@ "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}", + "flow_title": "{name} via HomeKit Accessory Protocol", "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.", @@ -33,8 +34,8 @@ "data": { "pairing_code": "Sammenkoblingskode" }, - "description": "Angi din HomeKit-sammenkoblingskode (i formatet XXX-XX-XXX) for \u00e5 bruke dette tilbeh\u00f8ret", - "title": "Koble til HomeKit tilbeh\u00f8r" + "description": "HomeKit Controller kommuniserer med {name} over lokalnettverket ved hjelp av en sikker kryptert tilkobling uten en separat HomeKit-kontroller eller iCloud. Skriv inn HomeKit-paringskoden (i formatet XXX-XX-XXX) for \u00e5 bruke dette tilbeh\u00f8ret. Denne koden finnes vanligvis p\u00e5 selve enheten eller i emballasjen.", + "title": "Par med en enhet via HomeKit Accessory Protocol" }, "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.", @@ -48,10 +49,30 @@ "data": { "device": "Enhet" }, - "description": "Velg enheten du vil koble til", - "title": "Koble til HomeKit tilbeh\u00f8r" + "description": "HomeKit Controller kommuniserer over lokalnettverket ved hjelp av en sikker kryptert tilkobling uten en separat HomeKit-kontroller eller iCloud. Velg enheten du vil pare med:", + "title": "Valg av enhet" } } }, + "device_automation": { + "trigger_subtype": { + "button1": "Knapp 1", + "button10": "Knapp 10", + "button2": "Knapp 2", + "button3": "Knapp 3", + "button4": "Knapp 4", + "button5": "Knapp 5", + "button6": "Knapp 6", + "button7": "Knapp 7", + "button8": "Knapp 8", + "button9": "Knapp 9", + "doorbell": "D\u00f8r-klokke" + }, + "trigger_type": { + "double_press": "{subtype} trykket to ganger", + "long_press": "{subtype} trykket og holdt", + "single_press": "{subtype}\u00bb trykket" + } + }, "title": "HomeKit-kontroller" } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/pl.json b/homeassistant/components/homekit_controller/translations/pl.json index bed897ea242..b8dc1b190ec 100644 --- a/homeassistant/components/homekit_controller/translations/pl.json +++ b/homeassistant/components/homekit_controller/translations/pl.json @@ -39,5 +39,20 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button1": "Przycisk 1", + "button10": "Przycisk 10", + "button2": "Przycisk 2", + "button3": "Przycisk 3", + "button4": "Przycisk 4", + "button5": "Przycisk 5", + "button6": "Przycisk 6", + "button7": "Przycisk 7", + "button8": "Przycisk 8", + "button9": "Przycisk 9", + "doorbell": "Dzwonek do drzwi" + } + }, "title": "Akcesorium HomeKit" } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/ru.json b/homeassistant/components/homekit_controller/translations/ru.json index ce1f765feff..2c631b99c6b 100644 --- a/homeassistant/components/homekit_controller/translations/ru.json +++ b/homeassistant/components/homekit_controller/translations/ru.json @@ -20,7 +20,7 @@ "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}", + "flow_title": "{name} \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u0432 HomeKit", "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.", @@ -34,8 +34,8 @@ "data": { "pairing_code": "\u041a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f" }, - "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" + "description": "HomeKit Controller \u043e\u0431\u043c\u0435\u043d\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u0441 {name} \u043f\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0431\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 HomeKit \u0438\u043b\u0438 iCloud. \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. \u042d\u0442\u043e\u0442 \u043a\u043e\u0434 \u043e\u0431\u044b\u0447\u043d\u043e \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u043d\u0430 \u0441\u0430\u043c\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u0438\u043b\u0438 \u043d\u0430 \u0443\u043f\u0430\u043a\u043e\u0432\u043a\u0435.", + "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u0432 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.", @@ -49,8 +49,8 @@ "data": { "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u043d\u0443\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435.", - "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u043c HomeKit" + "description": "HomeKit Controller \u043e\u0431\u043c\u0435\u043d\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u043f\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0433\u043e \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0431\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 HomeKit \u0438\u043b\u0438 iCloud. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u0445\u043e\u0442\u0438\u0442\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435:", + "title": "\u0412\u044b\u0431\u043e\u0440 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" } } }, diff --git a/homeassistant/components/homekit_controller/translations/zh-Hant.json b/homeassistant/components/homekit_controller/translations/zh-Hant.json index d70aa1b79b5..69c14bc8b00 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hant.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hant.json @@ -7,6 +7,7 @@ "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\u5be6\u9ad4\u5fc5\u9808\u5148\u884c\u79fb\u9664\u3002", + "invalid_properties": "\u8a2d\u5099\u5ba3\u544a\u5c6c\u6027\u7121\u6548\u3002", "no_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u914d\u5c0d\u8a2d\u5099" }, "error": { @@ -19,7 +20,7 @@ "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}", + "flow_title": "{name} \u4f7f\u7528 HomeKit \u914d\u4ef6\u901a\u8a0a\u5354\u5b9a", "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", @@ -33,8 +34,8 @@ "data": { "pairing_code": "\u8a2d\u5b9a\u4ee3\u78bc" }, - "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" + "description": "\u4f7f\u7528 {name} \u4e4b HomeKit \u63a7\u5236\u5668\u901a\u8a0a\u4f7f\u7528\u52a0\u5bc6\u9023\u7dda\uff0c\u4e26\u4e0d\u9700\u8981\u984d\u5916\u7684 HomeKit \u63a7\u5236\u5668\u6216 iCloud \u9023\u7dda\u3002\u8f38\u5165\u914d\u4ef6 Homekit \u8a2d\u5b9a\u914d\u5c0d\u4ee3\u78bc\uff08\u683c\u5f0f\uff1aXXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6\u3002\u4ee3\u78bc\u901a\u5e38\u53ef\u4ee5\u65bc\u8a2d\u5099\u6216\u8005\u5305\u88dd\u4e0a\u627e\u5230\u3002", + "title": "\u900f\u904e HomeKit \u914d\u4ef6\u901a\u8a0a\u5354\u5b9a\u6240\u914d\u5c0d\u8a2d\u5099" }, "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", @@ -48,8 +49,8 @@ "data": { "device": "\u8a2d\u5099" }, - "description": "\u9078\u64c7\u6240\u8981\u65b0\u589e\u7684\u8a2d\u5099", - "title": "HomeKit \u914d\u4ef6\u914d\u5c0d" + "description": "\u4f7f\u7528\u5340\u57df\u7db2\u8def\u4e4b HomeKit \u63a7\u5236\u5668\u901a\u8a0a\u4f7f\u7528\u52a0\u5bc6\u9023\u7dda\uff0c\u4e26\u4e0d\u9700\u8981\u984d\u5916\u7684 HomeKit \u63a7\u5236\u5668\u6216 iCloud \u9023\u7dda\u3002\u9078\u64c7\u6240\u8981\u65b0\u589e\u914d\u5c0d\u7684\u8a2d\u5099\uff1a", + "title": "\u8a2d\u5099\u9078\u64c7" } } }, diff --git a/homeassistant/components/homematicip_cloud/translations/es.json b/homeassistant/components/homematicip_cloud/translations/es.json index b5877fe09ce..cd300d4e4b3 100644 --- a/homeassistant/components/homematicip_cloud/translations/es.json +++ b/homeassistant/components/homematicip_cloud/translations/es.json @@ -6,6 +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.", diff --git a/homeassistant/components/homematicip_cloud/translations/nl.json b/homeassistant/components/homematicip_cloud/translations/nl.json index 7127b5c5aae..f16e385c3a0 100644 --- a/homeassistant/components/homematicip_cloud/translations/nl.json +++ b/homeassistant/components/homematicip_cloud/translations/nl.json @@ -6,6 +6,7 @@ "unknown": "Er is een onbekende fout opgetreden." }, "error": { + "invalid_pin": "Ongeldige pincode, probeer het opnieuw.", "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.", diff --git a/homeassistant/components/hue/translations/pl.json b/homeassistant/components/hue/translations/pl.json index 02dad0c3e52..32242752051 100644 --- a/homeassistant/components/hue/translations/pl.json +++ b/homeassistant/components/hue/translations/pl.json @@ -6,13 +6,13 @@ "already_in_progress": "Konfiguracja mostka jest ju\u017c w toku.", "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z mostkiem", "discover_timeout": "Nie mo\u017cna wykry\u0107 \u017cadnych mostk\u00f3w Hue", - "no_bridges": "Nie wykryto mostk\u00f3w Hue.", + "no_bridges": "Nie wykryto mostk\u00f3w Hue", "not_hue_bridge": "To nie jest mostek Hue", "unknown": "Nieoczekiwany b\u0142\u0105d." }, "error": { "linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.", - "register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107. Spr\u00f3buj ponownie." + "register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107. Spr\u00f3buj ponownie" }, "step": { "init": { diff --git a/homeassistant/components/hvv_departures/translations/pl.json b/homeassistant/components/hvv_departures/translations/pl.json index 5bf87fc08a8..e4914cbebc0 100644 --- a/homeassistant/components/hvv_departures/translations/pl.json +++ b/homeassistant/components/hvv_departures/translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie.", "no_results": "Brak wynik\u00f3w. Spr\u00f3buj z inn\u0105 stacj\u0105/adresem." }, diff --git a/homeassistant/components/insteon/translations/pl.json b/homeassistant/components/insteon/translations/pl.json index cb697462ca9..f04f2298732 100644 --- a/homeassistant/components/insteon/translations/pl.json +++ b/homeassistant/components/insteon/translations/pl.json @@ -1,11 +1,11 @@ { "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." + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { "hubv1": { @@ -34,7 +34,7 @@ }, "options": { "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { "change_hub_config": { diff --git a/homeassistant/components/ipp/translations/pl.json b/homeassistant/components/ipp/translations/pl.json index 4e5af33041c..3d0e846707e 100644 --- a/homeassistant/components/ipp/translations/pl.json +++ b/homeassistant/components/ipp/translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", - "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "connection_upgrade": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z drukark\u0105 z powodu konieczno\u015bci uaktualnienia po\u0142\u0105czenia.", "ipp_error": "Wyst\u0105pi\u0142 b\u0142\u0105d IPP.", "ipp_version_error": "Wersja IPP nieobs\u0142ugiwana przez drukark\u0119.", @@ -10,7 +10,7 @@ "unique_id_required": "Urz\u0105dzenie nie posiada unikalnej identyfikacji wymaganej do wykrycia." }, "error": { - "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "connection_upgrade": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z drukark\u0105. Spr\u00f3buj ponownie z zaznaczon\u0105 opcj\u0105 SSL/TLS." }, "flow_title": "Drukarka: {name}", diff --git a/homeassistant/components/isy994/translations/de.json b/homeassistant/components/isy994/translations/de.json index b9c3362c488..d14dfa6c65a 100644 --- a/homeassistant/components/isy994/translations/de.json +++ b/homeassistant/components/isy994/translations/de.json @@ -10,9 +10,20 @@ "step": { "user": { "data": { + "host": "URL", + "password": "Passwort", "username": "Benutzername" } } } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "Zeichenfolge ignorieren" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/pl.json b/homeassistant/components/isy994/translations/pl.json index 27f79ef2801..200a0355462 100644 --- a/homeassistant/components/isy994/translations/pl.json +++ b/homeassistant/components/isy994/translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie.", "invalid_host": "Wpis hosta nie by\u0142 w pe\u0142nym formacie URL, np. http://192.168.10.100:80.", "unknown": "Nieoczekiwany b\u0142\u0105d." diff --git a/homeassistant/components/kodi/translations/nl.json b/homeassistant/components/kodi/translations/nl.json new file mode 100644 index 00000000000..f6b02d0f84c --- /dev/null +++ b/homeassistant/components/kodi/translations/nl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Poort", + "ssl": "Maak verbinding via SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/pl.json b/homeassistant/components/kodi/translations/pl.json index 3e71fd0df8b..85bc5cc8e05 100644 --- a/homeassistant/components/kodi/translations/pl.json +++ b/homeassistant/components/kodi/translations/pl.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "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.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie.", "unknown": "Nieoczekiwany b\u0142\u0105d." }, diff --git a/homeassistant/components/metoffice/translations/pl.json b/homeassistant/components/metoffice/translations/pl.json index 7167faf5494..095a0ff6e29 100644 --- a/homeassistant/components/metoffice/translations/pl.json +++ b/homeassistant/components/metoffice/translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { diff --git a/homeassistant/components/mill/translations/pl.json b/homeassistant/components/mill/translations/pl.json index c9bef09227c..a7d0fb0618e 100644 --- a/homeassistant/components/mill/translations/pl.json +++ b/homeassistant/components/mill/translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "Konto jest ju\u017c skonfigurowane." }, "error": { - "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { "user": { diff --git a/homeassistant/components/netatmo/translations/es.json b/homeassistant/components/netatmo/translations/es.json index a72728e8438..1ad29ab080d 100644 --- a/homeassistant/components/netatmo/translations/es.json +++ b/homeassistant/components/netatmo/translations/es.json @@ -3,6 +3,7 @@ "abort": { "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.", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." }, "create_entry": { diff --git a/homeassistant/components/netatmo/translations/no.json b/homeassistant/components/netatmo/translations/no.json index 5cc1d400719..98e3206f46a 100644 --- a/homeassistant/components/netatmo/translations/no.json +++ b/homeassistant/components/netatmo/translations/no.json @@ -3,6 +3,7 @@ "abort": { "authorize_url_timeout": "Tidsavbrutt ved oppretting av godkjennings url.", "missing_configuration": "Komponeneten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk {docs_url} ] ( {docs_url} )", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "create_entry": { diff --git a/homeassistant/components/nzbget/translations/de.json b/homeassistant/components/nzbget/translations/de.json new file mode 100644 index 00000000000..b89b2d83550 --- /dev/null +++ b/homeassistant/components/nzbget/translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "flow_title": "NZBGet: {name}" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Aktualisierungsh\u00e4ufigkeit (Sekunden)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/nl.json b/homeassistant/components/nzbget/translations/nl.json new file mode 100644 index 00000000000..900bac61bc5 --- /dev/null +++ b/homeassistant/components/nzbget/translations/nl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Naam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/de.json b/homeassistant/components/openweathermap/translations/de.json new file mode 100644 index 00000000000..7103abba03c --- /dev/null +++ b/homeassistant/components/openweathermap/translations/de.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "auth": "Der API-Schl\u00fcssel ist nicht korrekt." + }, + "step": { + "user": { + "data": { + "language": "Sprache", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "mode": "Modus" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Sprache", + "mode": "Modus" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/es.json b/homeassistant/components/openweathermap/translations/es.json new file mode 100644 index 00000000000..7dbbd400360 --- /dev/null +++ b/homeassistant/components/openweathermap/translations/es.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "La integraci\u00f3n de OpenWeatherMap para estas coordenadas ya est\u00e1 configurada." + }, + "error": { + "auth": "La clave de API no es correcta.", + "connection": "No se puede conectar a la API de OWM" + }, + "step": { + "user": { + "data": { + "api_key": "Clave de API de OpenWeatherMap", + "language": "Idioma", + "latitude": "Latitud", + "longitude": "Longitud", + "mode": "Modo", + "name": "Nombre de la integraci\u00f3n" + }, + "description": "Configurar la integraci\u00f3n de OpenWeatherMap. Para generar la clave API, ve a https://openweathermap.org/appid", + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Idioma", + "mode": "Modo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/nl.json b/homeassistant/components/openweathermap/translations/nl.json new file mode 100644 index 00000000000..797bb2af0c6 --- /dev/null +++ b/homeassistant/components/openweathermap/translations/nl.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "OpenWeatherMap-integratie voor deze co\u00f6rdinaten is al geconfigureerd." + }, + "error": { + "auth": "API-sleutel is niet correct.", + "connection": "Kan geen verbinding maken met OWM API" + }, + "step": { + "user": { + "data": { + "api_key": "OpenWeatherMap API-sleutel", + "language": "Taal", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "mode": "Mode", + "name": "Naam van de integratie" + }, + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Taal", + "mode": "Mode" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/pl.json b/homeassistant/components/ovo_energy/translations/pl.json index 42afe86d48a..a82c7f23ab7 100644 --- a/homeassistant/components/ovo_energy/translations/pl.json +++ b/homeassistant/components/ovo_energy/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + "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 b974e6c04c9..394a24a5050 100644 --- a/homeassistant/components/pi_hole/translations/pl.json +++ b/homeassistant/components/pi_hole/translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { "user": { diff --git a/homeassistant/components/plugwise/translations/es.json b/homeassistant/components/plugwise/translations/es.json index 31e876cfe3a..9236556b64d 100644 --- a/homeassistant/components/plugwise/translations/es.json +++ b/homeassistant/components/plugwise/translations/es.json @@ -19,5 +19,15 @@ "title": "Conectarse a Smile" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalo de escaneo (segundos)" + }, + "description": "Ajustar las opciones de Plugwise" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/nl.json b/homeassistant/components/plugwise/translations/nl.json index 964675e0c63..5d0bd789957 100644 --- a/homeassistant/components/plugwise/translations/nl.json +++ b/homeassistant/components/plugwise/translations/nl.json @@ -18,5 +18,14 @@ "title": "Maak verbinding met de Smile" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Scaninterval (seconden)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/pl.json b/homeassistant/components/plum_lightpad/translations/pl.json index 121744d0f0d..83d814d65dc 100644 --- a/homeassistant/components/plum_lightpad/translations/pl.json +++ b/homeassistant/components/plum_lightpad/translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { "user": { diff --git a/homeassistant/components/point/translations/es.json b/homeassistant/components/point/translations/es.json index a7247b3d9b3..5374d2808d9 100644 --- a/homeassistant/components/point/translations/es.json +++ b/homeassistant/components/point/translations/es.json @@ -23,7 +23,7 @@ "data": { "flow_impl": "Proveedor" }, - "description": "\u00bfQuieres comenzar a configurar?", + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?", "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" } } diff --git a/homeassistant/components/progettihwsw/translations/nl.json b/homeassistant/components/progettihwsw/translations/nl.json new file mode 100644 index 00000000000..2b30a4f1caa --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/nl.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "step": { + "relay_modes": { + "title": "Stel relais in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/pl.json b/homeassistant/components/progettihwsw/translations/pl.json index ee25c598ecd..b18a46d9b3b 100644 --- a/homeassistant/components/progettihwsw/translations/pl.json +++ b/homeassistant/components/progettihwsw/translations/pl.json @@ -4,6 +4,26 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "step": { + "relay_modes": { + "data": { + "relay_1": "Przeka\u017anik 1", + "relay_10": "Przeka\u017anik 10", + "relay_11": "Przeka\u017anik 11", + "relay_12": "Przeka\u017anik 12", + "relay_13": "Przeka\u017anik 13", + "relay_14": "Przeka\u017anik 14", + "relay_15": "Przeka\u017anik 15", + "relay_16": "Przeka\u017anik 16", + "relay_2": "Przeka\u017anik 2", + "relay_3": "Przeka\u017anik 3", + "relay_4": "Przeka\u017anik 4", + "relay_5": "Przeka\u017anik 5", + "relay_6": "Przeka\u017anik 6", + "relay_7": "Przeka\u017anik 7", + "relay_8": "Przeka\u017anik 8", + "relay_9": "Przeka\u017anik 9" + } + }, "user": { "data": { "host": "Nazwa hosta lub adres IP", diff --git a/homeassistant/components/remote/translations/de.json b/homeassistant/components/remote/translations/de.json index d1ec188e2b8..ffd542f27d9 100644 --- a/homeassistant/components/remote/translations/de.json +++ b/homeassistant/components/remote/translations/de.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "{entity_name} umschalten", + "turn_off": "Schalte {entity_name} aus", + "turn_on": "Schalte {entity_name} an" + }, + "condition_type": { + "is_off": "{entity_name} ist ausgeschaltet", + "is_on": "{entity_name} ist eingeschaltet" + }, + "trigger_type": { + "turned_off": "{entity_name} ausgeschaltet", + "turned_on": "{entity_name} eingeschaltet" + } + }, "state": { "_": { "off": "Aus", diff --git a/homeassistant/components/remote/translations/es.json b/homeassistant/components/remote/translations/es.json index bf8b6d3a3ec..e2452b80e89 100644 --- a/homeassistant/components/remote/translations/es.json +++ b/homeassistant/components/remote/translations/es.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "Alternar {entity_name}", + "turn_off": "Apagar {entity_name}", + "turn_on": "Encender {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 activado" + }, + "trigger_type": { + "turned_off": "{entity_name} desactivado", + "turned_on": "{entity_name} activado" + } + }, "state": { "_": { "off": "Apagado", diff --git a/homeassistant/components/remote/translations/nl.json b/homeassistant/components/remote/translations/nl.json index b3ccad9ae2b..18d984f5c68 100644 --- a/homeassistant/components/remote/translations/nl.json +++ b/homeassistant/components/remote/translations/nl.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "Schakel {entity_name}", + "turn_off": "{entity_name} uitschakelen", + "turn_on": "{entity_name} inschakelen" + }, + "condition_type": { + "is_off": "{entity_name} staat uit", + "is_on": "{entity_name} staat aan" + }, + "trigger_type": { + "turned_off": "{entity_name} uitgeschakeld", + "turned_on": "{entity_name} ingeschakeld" + } + }, "state": { "_": { "off": "Uit", diff --git a/homeassistant/components/risco/translations/nl.json b/homeassistant/components/risco/translations/nl.json new file mode 100644 index 00000000000..614a896c3f8 --- /dev/null +++ b/homeassistant/components/risco/translations/nl.json @@ -0,0 +1,39 @@ +{ + "config": { + "step": { + "user": { + "data": { + "pin": "Pincode", + "username": "Gebruikersnaam" + } + } + } + }, + "options": { + "step": { + "ha_to_risco": { + "data": { + "armed_away": "Ingeschakeld weg", + "armed_custom_bypass": "Ingeschakeld met overbrugging(en)", + "armed_home": "Ingeschakeld thuis", + "armed_night": "Ingeschakeld nacht" + } + }, + "init": { + "data": { + "code_arm_required": "Pincode vereist om in te schakelen", + "code_disarm_required": "Pincode vereist om uit te schakelen" + }, + "title": "Configureer opties" + }, + "risco_to_ha": { + "data": { + "A": "Groep A", + "B": "Groep B", + "C": "Groep C", + "D": "Groep D" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/pl.json b/homeassistant/components/risco/translations/pl.json index 9859923d31e..45add23dc8b 100644 --- a/homeassistant/components/risco/translations/pl.json +++ b/homeassistant/components/risco/translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie.", "unknown": "Nieoczekiwany b\u0142\u0105d." }, @@ -16,5 +16,17 @@ } } } + }, + "options": { + "step": { + "risco_to_ha": { + "data": { + "A": "Grupa A", + "B": "Grupa B", + "C": "Grupa C", + "D": "Grupa D" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/roku/translations/pl.json b/homeassistant/components/roku/translations/pl.json index 7ca0148ce35..98881a1522d 100644 --- a/homeassistant/components/roku/translations/pl.json +++ b/homeassistant/components/roku/translations/pl.json @@ -5,7 +5,7 @@ "unknown": "Nieoczekiwany b\u0142\u0105d." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/rpi_power/translations/ca.json b/homeassistant/components/rpi_power/translations/ca.json new file mode 100644 index 00000000000..c53fa570b7e --- /dev/null +++ b/homeassistant/components/rpi_power/translations/ca.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'ha trobat la classe de sistema necess\u00e0ria per a aquest component, assegura't que el nucli sigui recent (versi\u00f3 del kernel) i que el maquinari sigui compatible", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "confirm": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + } + } + }, + "title": "Comprovador de font d'alimentaci\u00f3 de Raspberry Pi" +} \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/en.json b/homeassistant/components/rpi_power/translations/en.json new file mode 100644 index 00000000000..6995190979a --- /dev/null +++ b/homeassistant/components/rpi_power/translations/en.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Can't find the system class needed for this component, make sure that your kernel is recent and the hardware is supported", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "confirm": { + "description": "Do you want to start set up?" + } + } + }, + "title": "Raspberry Pi Power Supply Checker" +} \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/es.json b/homeassistant/components/rpi_power/translations/es.json new file mode 100644 index 00000000000..215b15014ab --- /dev/null +++ b/homeassistant/components/rpi_power/translations/es.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se puede encontrar la clase de sistema necesaria para este componente, aseg\u00farate de que tu kernel es reciente y el hardware es compatible", + "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." + }, + "step": { + "confirm": { + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" + } + } + }, + "title": "Comprobador de fuente de alimentaci\u00f3n de Raspberry Pi" +} \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/nl.json b/homeassistant/components/rpi_power/translations/nl.json new file mode 100644 index 00000000000..a18ff63733e --- /dev/null +++ b/homeassistant/components/rpi_power/translations/nl.json @@ -0,0 +1,3 @@ +{ + "title": "Raspberry Pi Voeding Checker" +} \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/pl.json b/homeassistant/components/rpi_power/translations/pl.json new file mode 100644 index 00000000000..4f466e3455f --- /dev/null +++ b/homeassistant/components/rpi_power/translations/pl.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "confirm": { + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/ru.json b/homeassistant/components/rpi_power/translations/ru.json new file mode 100644 index 00000000000..f91df15e1b3 --- /dev/null +++ b/homeassistant/components/rpi_power/translations/ru.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0439 \u043a\u043b\u0430\u0441\u0441, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u044d\u0442\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443 \u0412\u0430\u0441 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u043d\u043e\u0432\u0435\u0439\u0448\u0435\u0435 \u044f\u0434\u0440\u043e \u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0435 \u043e\u0431\u043e\u0440\u0443\u0434\u043e\u0432\u0430\u043d\u0438\u0435.", + "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." + }, + "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + } + } + }, + "title": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 \u043f\u0438\u0442\u0430\u043d\u0438\u044f Raspberry Pi" +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/nl.json b/homeassistant/components/sharkiq/translations/nl.json new file mode 100644 index 00000000000..ec17130cee2 --- /dev/null +++ b/homeassistant/components/sharkiq/translations/nl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "reauth": { + "data": { + "password": "Paswoord", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/pl.json b/homeassistant/components/sharkiq/translations/pl.json index dcb12e86906..cb691df13f1 100644 --- a/homeassistant/components/sharkiq/translations/pl.json +++ b/homeassistant/components/sharkiq/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "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." }, diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json new file mode 100644 index 00000000000..bc1354cc0c3 --- /dev/null +++ b/homeassistant/components/shelly/translations/de.json @@ -0,0 +1,3 @@ +{ + "title": "Shelly" +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/nl.json b/homeassistant/components/shelly/translations/nl.json new file mode 100644 index 00000000000..b5dc06eb140 --- /dev/null +++ b/homeassistant/components/shelly/translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "auth_not_supported": "Shelly apparaten die verificatie vereisen, worden momenteel niet ondersteund.", + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "flow_title": "Shelly: {name}", + "step": { + "confirm_discovery": { + "description": "Wilt u het {model} bij {host} opzetten?" + }, + "user": { + "data": { + "host": "Host" + } + } + } + }, + "title": "Shelly" +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json index 77a7f045671..f0d63cb340e 100644 --- a/homeassistant/components/shelly/translations/pl.json +++ b/homeassistant/components/shelly/translations/pl.json @@ -5,7 +5,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.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie.", "unknown": "Nieoczekiwany b\u0142\u0105d." }, diff --git a/homeassistant/components/smappee/translations/es.json b/homeassistant/components/smappee/translations/es.json index 543c988b356..5d65081f9f6 100644 --- a/homeassistant/components/smappee/translations/es.json +++ b/homeassistant/components/smappee/translations/es.json @@ -6,7 +6,8 @@ "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." + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, siga la documentaci\u00f3n.", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" }, "flow_title": "Smappee: {name}", "step": { diff --git a/homeassistant/components/smappee/translations/no.json b/homeassistant/components/smappee/translations/no.json index 76e96614aa6..11b8b1bdd30 100644 --- a/homeassistant/components/smappee/translations/no.json +++ b/homeassistant/components/smappee/translations/no.json @@ -6,7 +6,8 @@ "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." + "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk {docs_url} ] ( {docs_url} )" }, "flow_title": "Smappee: {navn}", "step": { diff --git a/homeassistant/components/smart_meter_texas/translations/pl.json b/homeassistant/components/smart_meter_texas/translations/pl.json new file mode 100644 index 00000000000..f1bb80d064f --- /dev/null +++ b/homeassistant/components/smart_meter_texas/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/sms/translations/pl.json b/homeassistant/components/sms/translations/pl.json index eec34cc0197..0637e5c9d79 100644 --- a/homeassistant/components/sms/translations/pl.json +++ b/homeassistant/components/sms/translations/pl.json @@ -5,7 +5,7 @@ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { diff --git a/homeassistant/components/somfy/translations/es.json b/homeassistant/components/somfy/translations/es.json index bbb1cedad98..992752d8420 100644 --- a/homeassistant/components/somfy/translations/es.json +++ b/homeassistant/components/somfy/translations/es.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "Solo puedes configurar una cuenta de Somfy.", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n", - "missing_configuration": "El componente Somfy no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n." + "missing_configuration": "El componente Somfy no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" }, "create_entry": { "default": "Autenticado correctamente con Somfy." diff --git a/homeassistant/components/somfy/translations/no.json b/homeassistant/components/somfy/translations/no.json index 6f8e3c3b993..65d2b87bd10 100644 --- a/homeassistant/components/somfy/translations/no.json +++ b/homeassistant/components/somfy/translations/no.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "Du kan kun konfigurere \u00e9n Somfy-konto.", "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse.", - "missing_configuration": "Somfy-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." + "missing_configuration": "Somfy-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk {docs_url} ] ( {docs_url} )" }, "create_entry": { "default": "Vellykket godkjenning med Somfy." diff --git a/homeassistant/components/sonarr/translations/pl.json b/homeassistant/components/sonarr/translations/pl.json index e2c60427b7e..841f132ec5e 100644 --- a/homeassistant/components/sonarr/translations/pl.json +++ b/homeassistant/components/sonarr/translations/pl.json @@ -5,7 +5,7 @@ "unknown": "Nieoczekiwany b\u0142\u0105d." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie." }, "flow_title": "Sonarr: {name}", diff --git a/homeassistant/components/songpal/translations/pl.json b/homeassistant/components/songpal/translations/pl.json index cc420f0f83a..7f811aa1621 100644 --- a/homeassistant/components/songpal/translations/pl.json +++ b/homeassistant/components/songpal/translations/pl.json @@ -5,7 +5,7 @@ "not_songpal_device": "To nie jest urz\u0105dzenie Songpal." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "flow_title": "Sony Songpal {name} ({host})", "step": { diff --git a/homeassistant/components/spotify/translations/es.json b/homeassistant/components/spotify/translations/es.json index 95c29f7a336..025777ad3f6 100644 --- a/homeassistant/components/spotify/translations/es.json +++ b/homeassistant/components/spotify/translations/es.json @@ -4,6 +4,7 @@ "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.", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "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": { diff --git a/homeassistant/components/spotify/translations/nl.json b/homeassistant/components/spotify/translations/nl.json index 82b25512001..9c44f30cd7f 100644 --- a/homeassistant/components/spotify/translations/nl.json +++ b/homeassistant/components/spotify/translations/nl.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "U kunt slechts \u00e9\u00e9n Spotify-account configureren.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Spotify integratie is niet geconfigureerd. Gelieve de documentatie te volgen." + "missing_configuration": "De Spotify integratie is niet geconfigureerd. Gelieve de documentatie te volgen.", + "reauth_account_mismatch": "Het Spotify account waarmee er is geverifieerd, komt niet overeen met het account dat opnieuw moet worden geverifieerd." }, "create_entry": { "default": "Succesvol geauthenticeerd met Spotify." @@ -11,6 +12,10 @@ "step": { "pick_implementation": { "title": "Kies Authenticatiemethode" + }, + "reauth_confirm": { + "description": "De Spotify integratie moet opnieuw worden geverifieerd met Spotify voor account: {account}", + "title": "Verifieer opnieuw met Spotify" } } } diff --git a/homeassistant/components/spotify/translations/no.json b/homeassistant/components/spotify/translations/no.json index c2e151e5eb7..6ab60ddbc0c 100644 --- a/homeassistant/components/spotify/translations/no.json +++ b/homeassistant/components/spotify/translations/no.json @@ -4,6 +4,7 @@ "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.", + "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk {docs_url} ] ( {docs_url} )", "reauth_account_mismatch": "Spotify-kontoen som er autentisert med, samsvarer ikke med den kontoen som trengs re-autentisering." }, "create_entry": { diff --git a/homeassistant/components/squeezebox/translations/pl.json b/homeassistant/components/squeezebox/translations/pl.json index a4339711918..fac6adf40a8 100644 --- a/homeassistant/components/squeezebox/translations/pl.json +++ b/homeassistant/components/squeezebox/translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie.", "unknown": "Nieoczekiwany b\u0142\u0105d." }, diff --git a/homeassistant/components/synology_dsm/translations/es.json b/homeassistant/components/synology_dsm/translations/es.json index a498333e049..ca02cf41e23 100644 --- a/homeassistant/components/synology_dsm/translations/es.json +++ b/homeassistant/components/synology_dsm/translations/es.json @@ -44,7 +44,8 @@ "step": { "init": { "data": { - "scan_interval": "Minutos entre escaneos" + "scan_interval": "Minutos entre escaneos", + "timeout": "Tiempo de espera (segundos)" } } } diff --git a/homeassistant/components/synology_dsm/translations/nl.json b/homeassistant/components/synology_dsm/translations/nl.json index 5798dce567d..ee7c89f7192 100644 --- a/homeassistant/components/synology_dsm/translations/nl.json +++ b/homeassistant/components/synology_dsm/translations/nl.json @@ -39,5 +39,14 @@ "title": "Synology DSM" } } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Time-out (seconden)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/no.json b/homeassistant/components/synology_dsm/translations/no.json index f8d7add4dc2..c9d16a9a6e8 100644 --- a/homeassistant/components/synology_dsm/translations/no.json +++ b/homeassistant/components/synology_dsm/translations/no.json @@ -44,7 +44,8 @@ "step": { "init": { "data": { - "scan_interval": "Minutter mellom skanninger" + "scan_interval": "Minutter mellom skanninger", + "timeout": "Tidsavbrudd (sekunder)" } } } diff --git a/homeassistant/components/tile/translations/de.json b/homeassistant/components/tile/translations/de.json index b76312d957f..dfc968eb066 100644 --- a/homeassistant/components/tile/translations/de.json +++ b/homeassistant/components/tile/translations/de.json @@ -5,7 +5,18 @@ "data": { "password": "Passwort", "username": "E-Mail Adresse" - } + }, + "title": "Kachel konfigurieren" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_inactive": "Inaktive Kacheln anzeigen" + }, + "title": "Kachel konfigurieren" } } } diff --git a/homeassistant/components/toon/translations/es.json b/homeassistant/components/toon/translations/es.json index 28e8a1dcb61..b6c6e7ad67d 100644 --- a/homeassistant/components/toon/translations/es.json +++ b/homeassistant/components/toon/translations/es.json @@ -5,7 +5,8 @@ "authorize_url_fail": "Error desconocido generando una url de autorizaci\u00f3n", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", - "no_agreements": "Esta cuenta no tiene pantallas Toon." + "no_agreements": "Esta cuenta no tiene pantallas Toon.", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" }, "step": { "agreement": { diff --git a/homeassistant/components/toon/translations/nl.json b/homeassistant/components/toon/translations/nl.json index 69eabaaf28b..da3ce6d84c7 100644 --- a/homeassistant/components/toon/translations/nl.json +++ b/homeassistant/components/toon/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "no_agreements": "Dit account heeft geen Toon schermen." + "no_agreements": "Dit account heeft geen Toon schermen.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout [check the help section] ( {docs_url} )" } } } \ No newline at end of file diff --git a/homeassistant/components/toon/translations/no.json b/homeassistant/components/toon/translations/no.json index 37652c4aee1..49103a77b37 100644 --- a/homeassistant/components/toon/translations/no.json +++ b/homeassistant/components/toon/translations/no.json @@ -5,7 +5,8 @@ "authorize_url_fail": "Ukjent feil ved generering av autoriseringsadresse.", "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", - "no_agreements": "Denne kontoen har ingen Toon skjermer." + "no_agreements": "Denne kontoen har ingen Toon skjermer.", + "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk {docs_url} ] ( {docs_url} )" }, "step": { "agreement": { diff --git a/homeassistant/components/tuya/translations/pl.json b/homeassistant/components/tuya/translations/pl.json index 7278806b5f6..134050394a2 100644 --- a/homeassistant/components/tuya/translations/pl.json +++ b/homeassistant/components/tuya/translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "auth_failed": "Niepoprawne uwierzytelnienie.", - "conn_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "conn_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "error": { diff --git a/homeassistant/components/twentemilieu/translations/pl.json b/homeassistant/components/twentemilieu/translations/pl.json index bfa38f9ef8a..e654bbac6a4 100644 --- a/homeassistant/components/twentemilieu/translations/pl.json +++ b/homeassistant/components/twentemilieu/translations/pl.json @@ -4,7 +4,7 @@ "address_exists": "Adres jest ju\u017c skonfigurowany." }, "error": { - "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_address": "Nie znaleziono adresu w obszarze us\u0142ugi Twente Milieu." }, "step": { diff --git a/homeassistant/components/unifi/translations/pl.json b/homeassistant/components/unifi/translations/pl.json index c062d392911..85a89e2eea9 100644 --- a/homeassistant/components/unifi/translations/pl.json +++ b/homeassistant/components/unifi/translations/pl.json @@ -5,7 +5,7 @@ }, "error": { "faulty_credentials": "Niepoprawne uwierzytelnienie.", - "service_unavailable": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "service_unavailable": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown_client_mac": "Brak klienta z tym adresem MAC" }, "step": { diff --git a/homeassistant/components/vizio/translations/pl.json b/homeassistant/components/vizio/translations/pl.json index 9d22796ea44..07b094b8d01 100644 --- a/homeassistant/components/vizio/translations/pl.json +++ b/homeassistant/components/vizio/translations/pl.json @@ -5,7 +5,7 @@ "updated_entry": "Ten wpis zosta\u0142 ju\u017c skonfigurowany, ale nazwa i/lub opcje zdefiniowane w konfiguracji nie pasuj\u0105 do wcze\u015bniej zaimportowanych warto\u015bci, wi\u0119c wpis konfiguracji zosta\u0142 odpowiednio zaktualizowany." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "complete_pairing_failed": "Nie mo\u017cna uko\u0144czy\u0107 parowania. Upewnij si\u0119, \u017ce podany kod PIN jest prawid\u0142owy, a telewizor jest zasilany i pod\u0142\u0105czony do sieci przed ponownym przes\u0142aniem.", "host_exists": "Urz\u0105dzenie Vizio z okre\u015blonym hostem jest ju\u017c skonfigurowane.", "name_exists": "Urz\u0105dzenie Vizio o okre\u015blonej nazwie jest ju\u017c skonfigurowane." diff --git a/homeassistant/components/wilight/translations/nl.json b/homeassistant/components/wilight/translations/nl.json new file mode 100644 index 00000000000..c04105e0878 --- /dev/null +++ b/homeassistant/components/wilight/translations/nl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "not_supported_device": "Deze WiLight wordt momenteel niet ondersteund", + "not_wilight_device": "Dit apparaat is geen WiLight" + }, + "flow_title": "WiLight: {name}", + "step": { + "confirm": { + "description": "Wil je WiLight {name} ? \n\n Het ondersteunt: {components}", + "title": "WiLight" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/pl.json b/homeassistant/components/wilight/translations/pl.json new file mode 100644 index 00000000000..815a6f19706 --- /dev/null +++ b/homeassistant/components/wilight/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/withings/translations/es.json b/homeassistant/components/withings/translations/es.json index e59b6e96775..d8b69013bbd 100644 --- a/homeassistant/components/withings/translations/es.json +++ b/homeassistant/components/withings/translations/es.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Configuraci\u00f3n actualizada para el perfil.", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", - "missing_configuration": "La integraci\u00f3n de Withings no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n." + "missing_configuration": "La integraci\u00f3n de Withings no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n.", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" }, "create_entry": { "default": "Autenticado correctamente con Withings." diff --git a/homeassistant/components/withings/translations/nl.json b/homeassistant/components/withings/translations/nl.json index 4f382f02a57..a54333ab8f2 100644 --- a/homeassistant/components/withings/translations/nl.json +++ b/homeassistant/components/withings/translations/nl.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Withings integratie is niet geconfigureerd. Gelieve de documentatie te volgen." + "missing_configuration": "De Withings integratie is niet geconfigureerd. Gelieve de documentatie te volgen.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})" }, "create_entry": { "default": "Succesvol geverifieerd met Withings voor het geselecteerde profiel." diff --git a/homeassistant/components/withings/translations/no.json b/homeassistant/components/withings/translations/no.json index 2b39f8fceab..bdec62a7160 100644 --- a/homeassistant/components/withings/translations/no.json +++ b/homeassistant/components/withings/translations/no.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Konfigurasjon oppdatert for profil.", "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse.", - "missing_configuration": "Withings-integrasjonen er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." + "missing_configuration": "Withings-integrasjonen er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, sjekk {docs_url} ] ( {docs_url} )" }, "create_entry": { "default": "Vellykket godkjenning med Withings." diff --git a/homeassistant/components/wled/translations/pl.json b/homeassistant/components/wled/translations/pl.json index 6f68055d385..ad33cc1ca40 100644 --- a/homeassistant/components/wled/translations/pl.json +++ b/homeassistant/components/wled/translations/pl.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", - "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "error": { - "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "flow_title": "WLED: {name}", "step": { diff --git a/homeassistant/components/wolflink/translations/pl.json b/homeassistant/components/wolflink/translations/pl.json index 483c73aac3d..73d8caf4a02 100644 --- a/homeassistant/components/wolflink/translations/pl.json +++ b/homeassistant/components/wolflink/translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie.", "unknown": "[%key::common::config_flow::error::unknown%]" }, diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json index 90f191a58c4..8fedd7e2b74 100644 --- a/homeassistant/components/xiaomi_miio/translations/pl.json +++ b/homeassistant/components/xiaomi_miio/translations/pl.json @@ -5,7 +5,7 @@ "already_in_progress": "Konfiguracja tego urz\u0105dzenia Xiaomi Miio jest ju\u017c w toku." }, "error": { - "connect_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "connect_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "no_device_selected": "Nie wybrano \u017cadnego urz\u0105dzenia, wybierz jedno urz\u0105dzenie." }, "flow_title": "Xiaomi Miio: {name}", diff --git a/homeassistant/components/yeelight/translations/de.json b/homeassistant/components/yeelight/translations/de.json new file mode 100644 index 00000000000..29fbc0dfe33 --- /dev/null +++ b/homeassistant/components/yeelight/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "pick_device": { + "data": { + "device": "Ger\u00e4te" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "use_music_mode": "Musik-Modus aktivieren" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/es.json b/homeassistant/components/yeelight/translations/es.json index 08c2ca92dea..fdfbdfc5634 100644 --- a/homeassistant/components/yeelight/translations/es.json +++ b/homeassistant/components/yeelight/translations/es.json @@ -15,6 +15,7 @@ }, "user": { "data": { + "host": "Host", "ip_address": "Direcci\u00f3n IP" }, "description": "Si dejas la direcci\u00f3n IP vac\u00eda, se usar\u00e1 descubrimiento para encontrar dispositivos." diff --git a/homeassistant/components/yeelight/translations/nl.json b/homeassistant/components/yeelight/translations/nl.json new file mode 100644 index 00000000000..804e9a9e545 --- /dev/null +++ b/homeassistant/components/yeelight/translations/nl.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "pick_device": { + "data": { + "device": "Apparaat" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Als u host leeg laat, wordt detectie gebruikt om apparaten te vinden." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "Model (optioneel)", + "nightlight_switch": "Gebruik Nachtlichtschakelaar", + "transition": "Overgangstijd (ms)", + "use_music_mode": "Schakel de muziekmodus in" + }, + "description": "Als u model leeg laat, wordt het automatisch gedetecteerd." + } + } + }, + "title": "Yeelight" +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/pl.json b/homeassistant/components/yeelight/translations/pl.json index 4a2636457aa..77d69d3fcf9 100644 --- a/homeassistant/components/yeelight/translations/pl.json +++ b/homeassistant/components/yeelight/translations/pl.json @@ -5,7 +5,7 @@ "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { "pick_device": { diff --git a/homeassistant/components/zerproc/translations/es.json b/homeassistant/components/zerproc/translations/es.json index 192afd87e65..a0bb855fc5d 100644 --- a/homeassistant/components/zerproc/translations/es.json +++ b/homeassistant/components/zerproc/translations/es.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "\u00bfQuieres comenzar a configurar?" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" } } }, From 938e06c00ec539020050ff54d5391e218b062afa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 14 Sep 2020 19:39:44 -0500 Subject: [PATCH 164/514] Fix homekit error when the bridge has been ignored. (#40082) --- homeassistant/components/homekit/config_flow.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 6a3206ac41b..3d35b685271 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -118,7 +118,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.entry_title = title return await self.async_step_pairing() - default_domains = [] if self._async_current_entries() else DEFAULT_DOMAINS + default_domains = [] if self._async_current_names() else DEFAULT_DOMAINS setup_schema = vol.Schema( { vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): bool, @@ -146,17 +146,27 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): find_next_available_port, DEFAULT_CONFIG_FLOW_PORT ) + @callback + def _async_current_names(self): + """Return a set of bridge names.""" + current_entries = self._async_current_entries() + + return { + entry.data[CONF_NAME] + for entry in current_entries + if CONF_NAME in entry.data + } + @callback def _async_available_name(self): """Return an available for the bridge.""" - current_entries = self._async_current_entries() # We always pick a RANDOM name to avoid Zeroconf # name collisions. If the name has been seen before # pairing will probably fail. acceptable_chars = string.ascii_uppercase + string.digits trailer = "".join(random.choices(acceptable_chars, k=4)) - all_names = {entry.data[CONF_NAME] for entry in current_entries} + all_names = self._async_current_names() suggested_name = f"{SHORT_BRIDGE_NAME} {trailer}" while suggested_name in all_names: trailer = "".join(random.choices(acceptable_chars, k=4)) From 903afb62d09a64deb1413f843ca8b39a0e61d461 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Mon, 14 Sep 2020 20:06:52 -0700 Subject: [PATCH 165/514] Add support for multiple vera controller hubs (#33613) --- homeassistant/components/vera/__init__.py | 89 ++++++++++++------- .../components/vera/binary_sensor.py | 10 +-- homeassistant/components/vera/climate.py | 10 +-- homeassistant/components/vera/common.py | 18 ++++ homeassistant/components/vera/config_flow.py | 34 +++++-- homeassistant/components/vera/const.py | 1 + homeassistant/components/vera/cover.py | 10 +-- homeassistant/components/vera/light.py | 10 +-- homeassistant/components/vera/lock.py | 10 +-- homeassistant/components/vera/scene.py | 14 ++- homeassistant/components/vera/sensor.py | 10 +-- homeassistant/components/vera/strings.json | 1 - homeassistant/components/vera/switch.py | 10 +-- tests/components/vera/common.py | 78 ++++++++++++---- tests/components/vera/test_binary_sensor.py | 4 +- tests/components/vera/test_climate.py | 6 +- tests/components/vera/test_config_flow.py | 51 +++++------ tests/components/vera/test_cover.py | 3 +- tests/components/vera/test_init.py | 83 ++++++++++++++--- tests/components/vera/test_light.py | 3 +- tests/components/vera/test_lock.py | 3 +- tests/components/vera/test_scene.py | 1 + tests/components/vera/test_sensor.py | 6 +- tests/components/vera/test_switch.py | 7 +- 24 files changed, 323 insertions(+), 149 deletions(-) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index b45716b33d6..0d34a521276 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -2,6 +2,7 @@ import asyncio from collections import defaultdict import logging +from typing import Type import pyvera as veraApi from requests.exceptions import RequestException @@ -19,17 +20,25 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import convert, slugify from homeassistant.util.dt import utc_from_timestamp -from .common import ControllerData, SubscriptionRegistry, get_configured_platforms +from .common import ( + ControllerData, + SubscriptionRegistry, + get_configured_platforms, + get_controller_data, + set_controller_data, +) from .config_flow import fix_device_id_list, new_options from .const import ( ATTR_CURRENT_ENERGY_KWH, ATTR_CURRENT_POWER_W, CONF_CONTROLLER, + CONF_LEGACY_UNIQUE_ID, DOMAIN, VERA_ID_FORMAT, ) @@ -54,6 +63,8 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: """Set up for Vera controllers.""" + hass.data[DOMAIN] = {} + config = base_config.get(DOMAIN) if not config: @@ -107,10 +118,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b all_devices = await hass.async_add_executor_job(controller.get_devices) all_scenes = await hass.async_add_executor_job(controller.get_scenes) - except RequestException: + except RequestException as exception: # There was a network related error connecting to the Vera controller. _LOGGER.exception("Error communicating with Vera API") - return False + raise ConfigEntryNotReady from exception # Exclude devices unwanted by user. devices = [device for device in all_devices if device.device_id not in exclude_ids] @@ -118,20 +129,21 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b vera_devices = defaultdict(list) for device in devices: device_type = map_vera_device(device, light_ids) - if device_type is None: - continue - - vera_devices[device_type].append(device) + if device_type is not None: + vera_devices[device_type].append(device) vera_scenes = [] for scene in all_scenes: vera_scenes.append(scene) controller_data = ControllerData( - controller=controller, devices=vera_devices, scenes=vera_scenes + controller=controller, + devices=vera_devices, + scenes=vera_scenes, + config_entry=config_entry, ) - hass.data[DOMAIN] = controller_data + set_controller_data(hass, config_entry, controller_data) # Forward the config data to the necessary platforms. for platform in get_configured_platforms(controller_data): @@ -144,7 +156,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload Withings config entry.""" - controller_data: ControllerData = hass.data[DOMAIN] + controller_data: ControllerData = get_controller_data(hass, config_entry) tasks = [ hass.config_entries.async_forward_entry_unload(config_entry, platform) @@ -159,43 +171,52 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> def map_vera_device(vera_device, remap): """Map vera classes to Home Assistant types.""" - if isinstance(vera_device, veraApi.VeraDimmer): - return "light" - if isinstance(vera_device, veraApi.VeraBinarySensor): - return "binary_sensor" - if isinstance(vera_device, veraApi.VeraSensor): - return "sensor" - if isinstance(vera_device, veraApi.VeraArmableDevice): - return "switch" - if isinstance(vera_device, veraApi.VeraLock): - return "lock" - if isinstance(vera_device, veraApi.VeraThermostat): - return "climate" - if isinstance(vera_device, veraApi.VeraCurtain): - return "cover" - if isinstance(vera_device, veraApi.VeraSceneController): - return "sensor" - if isinstance(vera_device, veraApi.VeraSwitch): - if vera_device.device_id in remap: + type_map = { + veraApi.VeraDimmer: "light", + veraApi.VeraBinarySensor: "binary_sensor", + veraApi.VeraSensor: "sensor", + veraApi.VeraArmableDevice: "switch", + veraApi.VeraLock: "lock", + veraApi.VeraThermostat: "climate", + veraApi.VeraCurtain: "cover", + veraApi.VeraSceneController: "sensor", + veraApi.VeraSwitch: "switch", + } + + def map_special_case(instance_class: Type, entity_type: str) -> str: + if instance_class is veraApi.VeraSwitch and vera_device.device_id in remap: return "light" - return "switch" - return None + return entity_type + + return next( + iter( + map_special_case(instance_class, entity_type) + for instance_class, entity_type in type_map.items() + if isinstance(vera_device, instance_class) + ), + None, + ) class VeraDevice(Entity): """Representation of a Vera device entity.""" - def __init__(self, vera_device, controller): + def __init__(self, vera_device, controller_data: ControllerData): """Initialize the device.""" self.vera_device = vera_device - self.controller = controller + self.controller = controller_data.controller self._name = self.vera_device.name # Append device id to prevent name clashes in HA. self.vera_id = VERA_ID_FORMAT.format( - slugify(vera_device.name), vera_device.device_id + slugify(vera_device.name), vera_device.vera_device_id ) + if controller_data.config_entry.data.get(CONF_LEGACY_UNIQUE_ID): + self._unique_id = str(self.vera_device.vera_device_id) + else: + self._unique_id = f"vera_{controller_data.config_entry.unique_id}_{self.vera_device.vera_device_id}" + async def async_added_to_hass(self): """Subscribe to updates.""" self.controller.register(self.vera_device, self._update_callback) @@ -254,4 +275,4 @@ class VeraDevice(Entity): The Vera assigns a unique and immutable ID number to each device. """ - return str(self.vera_device.vera_device_id) + return self._unique_id diff --git a/homeassistant/components/vera/binary_sensor.py b/homeassistant/components/vera/binary_sensor.py index 557874f846a..7ab24e9544f 100644 --- a/homeassistant/components/vera/binary_sensor.py +++ b/homeassistant/components/vera/binary_sensor.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from . import VeraDevice -from .const import DOMAIN +from .common import ControllerData, get_controller_data _LOGGER = logging.getLogger(__name__) @@ -23,10 +23,10 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - controller_data = hass.data[DOMAIN] + controller_data = get_controller_data(hass, entry) async_add_entities( [ - VeraBinarySensor(device, controller_data.controller) + VeraBinarySensor(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) ] ) @@ -35,10 +35,10 @@ async def async_setup_entry( class VeraBinarySensor(VeraDevice, BinarySensorEntity): """Representation of a Vera Binary Sensor.""" - def __init__(self, vera_device, controller): + def __init__(self, vera_device, controller_data: ControllerData): """Initialize the binary_sensor.""" self._state = False - VeraDevice.__init__(self, vera_device, controller) + VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 9b8601e45d1..a8ba647c1d6 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -24,7 +24,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import convert from . import VeraDevice -from .const import DOMAIN +from .common import ControllerData, get_controller_data _LOGGER = logging.getLogger(__name__) @@ -40,10 +40,10 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - controller_data = hass.data[DOMAIN] + controller_data = get_controller_data(hass, entry) async_add_entities( [ - VeraThermostat(device, controller_data.controller) + VeraThermostat(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) ] ) @@ -52,9 +52,9 @@ async def async_setup_entry( class VeraThermostat(VeraDevice, ClimateEntity): """Representation of a Vera Thermostat.""" - def __init__(self, vera_device, controller): + def __init__(self, vera_device, controller_data: ControllerData): """Initialize the Vera device.""" - VeraDevice.__init__(self, vera_device, controller) + VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property diff --git a/homeassistant/components/vera/common.py b/homeassistant/components/vera/common.py index 17536bcae69..66a2d6879dd 100644 --- a/homeassistant/components/vera/common.py +++ b/homeassistant/components/vera/common.py @@ -5,9 +5,12 @@ from typing import DefaultDict, List, NamedTuple, Set import pyvera as pv from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.event import call_later +from .const import DOMAIN + _LOGGER = logging.getLogger(__name__) @@ -17,6 +20,7 @@ class ControllerData(NamedTuple): controller: pv.VeraController devices: DefaultDict[str, List[pv.VeraDevice]] scenes: List[pv.VeraScene] + config_entry: ConfigEntry def get_configured_platforms(controller_data: ControllerData) -> Set[str]: @@ -31,6 +35,20 @@ def get_configured_platforms(controller_data: ControllerData) -> Set[str]: return set(platforms) +def get_controller_data( + hass: HomeAssistant, config_entry: ConfigEntry +) -> ControllerData: + """Get controller data from hass data.""" + return hass.data[DOMAIN][config_entry.entry_id] + + +def set_controller_data( + hass: HomeAssistant, config_entry: ConfigEntry, data: ControllerData +) -> None: + """Set controller data in hass data.""" + hass.data[DOMAIN][config_entry.entry_id] = data + + class SubscriptionRegistry(pv.AbstractSubscriptionRegistry): """Manages polling for data from vera.""" diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index a040e4b96b5..26ae509337b 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -10,8 +10,13 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import callback +from homeassistant.helpers.entity_registry import EntityRegistry -from .const import CONF_CONTROLLER, DOMAIN +from .const import ( # pylint: disable=unused-import + CONF_CONTROLLER, + CONF_LEGACY_UNIQUE_ID, + DOMAIN, +) LIST_REGEX = re.compile("[^0-9]+") _LOGGER = logging.getLogger(__name__) @@ -92,15 +97,13 @@ class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input: dict = None): """Handle user initiated flow.""" - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="already_configured") - if user_input is not None: return await self.async_step_finish( { **user_input, **options_data(user_input), **{CONF_SOURCE: config_entries.SOURCE_USER}, + **{CONF_LEGACY_UNIQUE_ID: False}, } ) @@ -113,8 +116,29 @@ class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, config: dict): """Handle a flow initialized by import.""" + + # If there are entities with the legacy unique_id, then this imported config + # should also use the legacy unique_id for entity creation. + entity_registry: EntityRegistry = ( + await self.hass.helpers.entity_registry.async_get_registry() + ) + use_legacy_unique_id = ( + len( + [ + entry + for entry in entity_registry.entities.values() + if entry.platform == DOMAIN and entry.unique_id.isdigit() + ] + ) + > 0 + ) + return await self.async_step_finish( - {**config, **{CONF_SOURCE: config_entries.SOURCE_IMPORT}} + { + **config, + **{CONF_SOURCE: config_entries.SOURCE_IMPORT}, + **{CONF_LEGACY_UNIQUE_ID: use_legacy_unique_id}, + } ) async def async_step_finish(self, config: dict): diff --git a/homeassistant/components/vera/const.py b/homeassistant/components/vera/const.py index c4f1d0efa3a..34ac7faa669 100644 --- a/homeassistant/components/vera/const.py +++ b/homeassistant/components/vera/const.py @@ -2,6 +2,7 @@ DOMAIN = "vera" CONF_CONTROLLER = "vera_controller_url" +CONF_LEGACY_UNIQUE_ID = "legacy_unique_id" VERA_ID_FORMAT = "{}_{}" diff --git a/homeassistant/components/vera/cover.py b/homeassistant/components/vera/cover.py index a1f536d9cc1..bad36727c15 100644 --- a/homeassistant/components/vera/cover.py +++ b/homeassistant/components/vera/cover.py @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from . import VeraDevice -from .const import DOMAIN +from .common import ControllerData, get_controller_data _LOGGER = logging.getLogger(__name__) @@ -24,10 +24,10 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - controller_data = hass.data[DOMAIN] + controller_data = get_controller_data(hass, entry) async_add_entities( [ - VeraCover(device, controller_data.controller) + VeraCover(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) ] ) @@ -36,9 +36,9 @@ async def async_setup_entry( class VeraCover(VeraDevice, CoverEntity): """Representation a Vera Cover.""" - def __init__(self, vera_device, controller): + def __init__(self, vera_device, controller_data: ControllerData): """Initialize the Vera device.""" - VeraDevice.__init__(self, vera_device, controller) + VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property diff --git a/homeassistant/components/vera/light.py b/homeassistant/components/vera/light.py index 250842f1687..84f36fe3877 100644 --- a/homeassistant/components/vera/light.py +++ b/homeassistant/components/vera/light.py @@ -17,7 +17,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.util.color as color_util from . import VeraDevice -from .const import DOMAIN +from .common import ControllerData, get_controller_data _LOGGER = logging.getLogger(__name__) @@ -28,10 +28,10 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - controller_data = hass.data[DOMAIN] + controller_data = get_controller_data(hass, entry) async_add_entities( [ - VeraLight(device, controller_data.controller) + VeraLight(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) ] ) @@ -40,12 +40,12 @@ async def async_setup_entry( class VeraLight(VeraDevice, LightEntity): """Representation of a Vera Light, including dimmable.""" - def __init__(self, vera_device, controller): + def __init__(self, vera_device, controller_data: ControllerData): """Initialize the light.""" self._state = False self._color = None self._brightness = None - VeraDevice.__init__(self, vera_device, controller) + VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property diff --git a/homeassistant/components/vera/lock.py b/homeassistant/components/vera/lock.py index f85beb5ba69..6a1158d18c4 100644 --- a/homeassistant/components/vera/lock.py +++ b/homeassistant/components/vera/lock.py @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from . import VeraDevice -from .const import DOMAIN +from .common import ControllerData, get_controller_data _LOGGER = logging.getLogger(__name__) @@ -27,10 +27,10 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - controller_data = hass.data[DOMAIN] + controller_data = get_controller_data(hass, entry) async_add_entities( [ - VeraLock(device, controller_data.controller) + VeraLock(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) ] ) @@ -39,10 +39,10 @@ async def async_setup_entry( class VeraLock(VeraDevice, LockEntity): """Representation of a Vera lock.""" - def __init__(self, vera_device, controller): + def __init__(self, vera_device, controller_data: ControllerData): """Initialize the Vera device.""" self._state = None - VeraDevice.__init__(self, vera_device, controller) + VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) def lock(self, **kwargs): diff --git a/homeassistant/components/vera/scene.py b/homeassistant/components/vera/scene.py index 2f3069f5332..c12f07c15af 100644 --- a/homeassistant/components/vera/scene.py +++ b/homeassistant/components/vera/scene.py @@ -8,7 +8,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -from .const import DOMAIN, VERA_ID_FORMAT +from .common import ControllerData, get_controller_data +from .const import VERA_ID_FORMAT _LOGGER = logging.getLogger(__name__) @@ -19,22 +20,19 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - controller_data = hass.data[DOMAIN] + controller_data = get_controller_data(hass, entry) async_add_entities( - [ - VeraScene(device, controller_data.controller) - for device in controller_data.scenes - ] + [VeraScene(device, controller_data) for device in controller_data.scenes] ) class VeraScene(Scene): """Representation of a Vera scene entity.""" - def __init__(self, vera_scene, controller): + def __init__(self, vera_scene, controller_data: ControllerData): """Initialize the scene.""" self.vera_scene = vera_scene - self.controller = controller + self.controller = controller_data.controller self._name = self.vera_scene.name # Append device id to prevent name clashes in HA. diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index 3c4e0974b85..697af6f4562 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -13,7 +13,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import convert from . import VeraDevice -from .const import DOMAIN +from .common import ControllerData, get_controller_data _LOGGER = logging.getLogger(__name__) @@ -26,10 +26,10 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - controller_data = hass.data[DOMAIN] + controller_data = get_controller_data(hass, entry) async_add_entities( [ - VeraSensor(device, controller_data.controller) + VeraSensor(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) ] ) @@ -38,12 +38,12 @@ async def async_setup_entry( class VeraSensor(VeraDevice, Entity): """Representation of a Vera Sensor.""" - def __init__(self, vera_device, controller): + def __init__(self, vera_device, controller_data: ControllerData): """Initialize the sensor.""" self.current_value = None self._temperature_units = None self.last_changed_time = None - VeraDevice.__init__(self, vera_device, controller) + VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json index 7b294eddbb9..844d1777f5d 100644 --- a/homeassistant/components/vera/strings.json +++ b/homeassistant/components/vera/strings.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured": "A controller is already configured.", "cannot_connect": "Could not connect to controller with url {base_url}" }, "step": { diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py index 0a9a94d6372..9e5af432ce8 100644 --- a/homeassistant/components/vera/switch.py +++ b/homeassistant/components/vera/switch.py @@ -13,7 +13,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import convert from . import VeraDevice -from .const import DOMAIN +from .common import ControllerData, get_controller_data _LOGGER = logging.getLogger(__name__) @@ -24,10 +24,10 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" - controller_data = hass.data[DOMAIN] + controller_data = get_controller_data(hass, entry) async_add_entities( [ - VeraSwitch(device, controller_data.controller) + VeraSwitch(device, controller_data) for device in controller_data.devices.get(PLATFORM_DOMAIN) ] ) @@ -36,10 +36,10 @@ async def async_setup_entry( class VeraSwitch(VeraDevice, SwitchEntity): """Representation of a Vera Switch.""" - def __init__(self, vera_device, controller): + def __init__(self, vera_device, controller_data: ControllerData): """Initialize the Vera device.""" self._state = False - VeraDevice.__init__(self, vera_device, controller) + VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) def turn_on(self, **kwargs): diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index 31e7c706ec9..29c6a0e8683 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -1,10 +1,15 @@ """Common code for tests.""" - +from enum import Enum from typing import Callable, Dict, NamedTuple, Tuple import pyvera as pv -from homeassistant.components.vera.const import CONF_CONTROLLER, DOMAIN +from homeassistant import config_entries +from homeassistant.components.vera.const import ( + CONF_CONTROLLER, + CONF_LEGACY_UNIQUE_ID, + DOMAIN, +) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -24,7 +29,15 @@ class ControllerData(NamedTuple): class ComponentData(NamedTuple): """Test data about the vera component.""" - controller_data: ControllerData + controller_data: Tuple[ControllerData] + + +class ConfigSource(Enum): + """Source of configuration.""" + + FILE = "file" + CONFIG_FLOW = "config_flow" + CONFIG_ENTRY = "config_entry" class ControllerConfig(NamedTuple): @@ -32,31 +45,34 @@ class ControllerConfig(NamedTuple): config: Dict options: Dict - config_from_file: bool + config_source: ConfigSource serial_number: str devices: Tuple[pv.VeraDevice, ...] scenes: Tuple[pv.VeraScene, ...] setup_callback: SetupCallback + legacy_entity_unique_id: bool def new_simple_controller_config( config: dict = None, options: dict = None, - config_from_file=False, + config_source=ConfigSource.CONFIG_FLOW, serial_number="1111", devices: Tuple[pv.VeraDevice, ...] = (), scenes: Tuple[pv.VeraScene, ...] = (), setup_callback: SetupCallback = None, + legacy_entity_unique_id=False, ) -> ControllerConfig: """Create simple contorller config.""" return ControllerConfig( config=config or {CONF_CONTROLLER: "http://127.0.0.1:123"}, options=options, - config_from_file=config_from_file, + config_source=config_source, serial_number=serial_number, devices=devices, scenes=scenes, setup_callback=setup_callback, + legacy_entity_unique_id=legacy_entity_unique_id, ) @@ -68,14 +84,38 @@ class ComponentFactory: self.vera_controller_class_mock = vera_controller_class_mock async def configure_component( - self, hass: HomeAssistant, controller_config: ControllerConfig + self, + hass: HomeAssistant, + controller_config: ControllerConfig = None, + controller_configs: Tuple[ControllerConfig] = (), ) -> ComponentData: + """Configure the component with multiple specific mock data.""" + configs = list(controller_configs) + + if controller_config: + configs.append(controller_config) + + return ComponentData( + controller_data=tuple( + [ + await self._configure_component(hass, controller_config) + for controller_config in configs + ] + ) + ) + + async def _configure_component( + self, hass: HomeAssistant, controller_config: ControllerConfig + ) -> ControllerData: """Configure the component with specific mock data.""" component_config = { **(controller_config.config or {}), **(controller_config.options or {}), } + if controller_config.legacy_entity_unique_id: + component_config[CONF_LEGACY_UNIQUE_ID] = True + controller = MagicMock(spec=pv.VeraController) # type: pv.VeraController controller.base_url = component_config.get(CONF_CONTROLLER) controller.register = MagicMock() @@ -101,7 +141,7 @@ class ComponentFactory: hass_config = {} # Setup component through config file import. - if controller_config.config_from_file: + if controller_config.config_source == ConfigSource.FILE: hass_config[DOMAIN] = component_config # Setup Home Assistant. @@ -109,9 +149,21 @@ class ComponentFactory: await hass.async_block_till_done() # Setup component through config flow. - if not controller_config.config_from_file: + if controller_config.config_source == ConfigSource.CONFIG_FLOW: + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=component_config, + ) + await hass.async_block_till_done() + + # Setup component directly from config entry. + if controller_config.config_source == ConfigSource.CONFIG_ENTRY: entry = MockConfigEntry( - domain=DOMAIN, data=component_config, options={}, unique_id="12345" + domain=DOMAIN, + data=controller_config.config, + options=controller_config.options, + unique_id="12345", ) entry.add_to_hass(hass) @@ -124,8 +176,4 @@ class ComponentFactory: else None ) - return ComponentData( - controller_data=ControllerData( - controller=controller, update_callback=update_callback - ) - ) + return ControllerData(controller=controller, update_callback=update_callback) diff --git a/tests/components/vera/test_binary_sensor.py b/tests/components/vera/test_binary_sensor.py index 4b0d41d9a1e..a02c2ef1635 100644 --- a/tests/components/vera/test_binary_sensor.py +++ b/tests/components/vera/test_binary_sensor.py @@ -14,7 +14,7 @@ async def test_binary_sensor( """Test function.""" vera_device = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor vera_device.device_id = 1 - vera_device.vera_device_id = 1 + vera_device.vera_device_id = vera_device.device_id vera_device.name = "dev1" vera_device.is_tripped = False entity_id = "binary_sensor.dev1_1" @@ -23,7 +23,7 @@ async def test_binary_sensor( hass=hass, controller_config=new_simple_controller_config(devices=(vera_device,)), ) - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback vera_device.is_tripped = False update_callback(vera_device) diff --git a/tests/components/vera/test_climate.py b/tests/components/vera/test_climate.py index f11f3ea5a3b..370ecc18dcd 100644 --- a/tests/components/vera/test_climate.py +++ b/tests/components/vera/test_climate.py @@ -22,6 +22,7 @@ async def test_climate( """Test function.""" vera_device = MagicMock(spec=pv.VeraThermostat) # type: pv.VeraThermostat vera_device.device_id = 1 + vera_device.vera_device_id = vera_device.device_id vera_device.name = "dev1" vera_device.category = pv.CATEGORY_THERMOSTAT vera_device.power = 10 @@ -34,7 +35,7 @@ async def test_climate( hass=hass, controller_config=new_simple_controller_config(devices=(vera_device,)), ) - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback assert hass.states.get(entity_id).state == HVAC_MODE_OFF @@ -131,6 +132,7 @@ async def test_climate_f( """Test function.""" vera_device = MagicMock(spec=pv.VeraThermostat) # type: pv.VeraThermostat vera_device.device_id = 1 + vera_device.vera_device_id = vera_device.device_id vera_device.name = "dev1" vera_device.category = pv.CATEGORY_THERMOSTAT vera_device.power = 10 @@ -148,7 +150,7 @@ async def test_climate_f( devices=(vera_device,), setup_callback=setup_callback ), ) - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback await hass.services.async_call( "climate", diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index 793e313125c..dceac728e4d 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -2,17 +2,13 @@ from requests.exceptions import RequestException from homeassistant import config_entries, data_entry_flow -from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN +from homeassistant.components.vera import CONF_CONTROLLER, CONF_LEGACY_UNIQUE_ID, DOMAIN from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM from tests.async_mock import MagicMock, patch -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_registry async def test_async_step_user_success(hass: HomeAssistant) -> None: @@ -44,6 +40,7 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: CONF_SOURCE: config_entries.SOURCE_USER, CONF_LIGHTS: [12, 13], CONF_EXCLUDE: [14, 15], + CONF_LEGACY_UNIQUE_ID: False, } assert result["result"].unique_id == controller.serial_number @@ -51,18 +48,6 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: assert entries -async def test_async_step_user_already_configured(hass: HomeAssistant) -> None: - """Test user step with entry already configured.""" - entry = MockConfigEntry(domain=DOMAIN, data={}, options={}, unique_id="12345") - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - async def test_async_step_import_success(hass: HomeAssistant) -> None: """Test import step success.""" with patch("pyvera.VeraController") as vera_controller_class_mock: @@ -82,28 +67,40 @@ async def test_async_step_import_success(hass: HomeAssistant) -> None: assert result["data"] == { CONF_CONTROLLER: "http://127.0.0.1:123", CONF_SOURCE: config_entries.SOURCE_IMPORT, + CONF_LEGACY_UNIQUE_ID: False, } assert result["result"].unique_id == controller.serial_number -async def test_async_step_import_alredy_setup(hass: HomeAssistant) -> None: - """Test import step with entry already setup.""" - entry = MockConfigEntry(domain=DOMAIN, data={}, options={}, unique_id="12345") - entry.add_to_hass(hass) +async def test_async_step_import_success_with_legacy_unique_id( + hass: HomeAssistant, +) -> None: + """Test import step success with legacy unique id.""" + entity_registry = mock_registry(hass) + entity_registry.async_get_or_create( + domain="switch", platform=DOMAIN, unique_id="12" + ) with patch("pyvera.VeraController") as vera_controller_class_mock: controller = MagicMock() controller.refresh_data = MagicMock() - controller.serial_number = "12345" + controller.serial_number = "serial_number_1" vera_controller_class_mock.return_value = controller result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_CONTROLLER: "http://localhost:445"}, + data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, ) - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "http://127.0.0.1:123" + assert result["data"] == { + CONF_CONTROLLER: "http://127.0.0.1:123", + CONF_SOURCE: config_entries.SOURCE_IMPORT, + CONF_LEGACY_UNIQUE_ID: True, + } + assert result["result"].unique_id == controller.serial_number async def test_async_step_finish_error(hass: HomeAssistant) -> None: diff --git a/tests/components/vera/test_cover.py b/tests/components/vera/test_cover.py index 311c8013d86..f3dc2263749 100644 --- a/tests/components/vera/test_cover.py +++ b/tests/components/vera/test_cover.py @@ -14,6 +14,7 @@ async def test_cover( """Test function.""" vera_device = MagicMock(spec=pv.VeraCurtain) # type: pv.VeraCurtain vera_device.device_id = 1 + vera_device.vera_device_id = vera_device.device_id vera_device.name = "dev1" vera_device.category = pv.CATEGORY_CURTAIN vera_device.is_closed = False @@ -24,7 +25,7 @@ async def test_cover( hass=hass, controller_config=new_simple_controller_config(devices=(vera_device,)), ) - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback assert hass.states.get(entity_id).state == "closed" assert hass.states.get(entity_id).attributes["current_position"] == 0 diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py index 210037a2ca3..b3f7b3249ef 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -12,10 +12,10 @@ from homeassistant.components.vera import ( from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED from homeassistant.core import HomeAssistant -from .common import ComponentFactory, new_simple_controller_config +from .common import ComponentFactory, ConfigSource, new_simple_controller_config from tests.async_mock import MagicMock -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_registry async def test_init( @@ -24,7 +24,7 @@ async def test_init( """Test function.""" vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor vera_device1.device_id = 1 - vera_device1.vera_device_id = 1 + vera_device1.vera_device_id = vera_device1.device_id vera_device1.name = "first_dev" vera_device1.is_tripped = False entity1_id = "binary_sensor.first_dev_1" @@ -33,7 +33,7 @@ async def test_init( hass=hass, controller_config=new_simple_controller_config( config={CONF_CONTROLLER: "http://127.0.0.1:111"}, - config_from_file=False, + config_source=ConfigSource.CONFIG_FLOW, serial_number="first_serial", devices=(vera_device1,), ), @@ -41,8 +41,8 @@ async def test_init( entity_registry = await hass.helpers.entity_registry.async_get_registry() entry1 = entity_registry.async_get(entity1_id) - assert entry1 + assert entry1.unique_id == "vera_first_serial_1" async def test_init_from_file( @@ -51,7 +51,7 @@ async def test_init_from_file( """Test function.""" vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor vera_device1.device_id = 1 - vera_device1.vera_device_id = 1 + vera_device1.vera_device_id = vera_device1.device_id vera_device1.name = "first_dev" vera_device1.is_tripped = False entity1_id = "binary_sensor.first_dev_1" @@ -60,7 +60,7 @@ async def test_init_from_file( hass=hass, controller_config=new_simple_controller_config( config={CONF_CONTROLLER: "http://127.0.0.1:111"}, - config_from_file=True, + config_source=ConfigSource.FILE, serial_number="first_serial", devices=(vera_device1,), ), @@ -69,6 +69,62 @@ async def test_init_from_file( entity_registry = await hass.helpers.entity_registry.async_get_registry() entry1 = entity_registry.async_get(entity1_id) assert entry1 + assert entry1.unique_id == "vera_first_serial_1" + + +async def test_multiple_controllers_with_legacy_one( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test multiple controllers with one legacy controller.""" + vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor + vera_device1.device_id = 1 + vera_device1.vera_device_id = vera_device1.device_id + vera_device1.name = "first_dev" + vera_device1.is_tripped = False + entity1_id = "binary_sensor.first_dev_1" + + vera_device2 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor + vera_device2.device_id = 2 + vera_device2.vera_device_id = vera_device2.device_id + vera_device2.name = "second_dev" + vera_device2.is_tripped = False + entity2_id = "binary_sensor.second_dev_2" + + # Add existing entity registry entry from previous setup. + entity_registry = mock_registry(hass) + entity_registry.async_get_or_create( + domain="switch", platform=DOMAIN, unique_id="12" + ) + + await vera_component_factory.configure_component( + hass=hass, + controller_config=new_simple_controller_config( + config={CONF_CONTROLLER: "http://127.0.0.1:111"}, + config_source=ConfigSource.FILE, + serial_number="first_serial", + devices=(vera_device1,), + ), + ) + + await vera_component_factory.configure_component( + hass=hass, + controller_config=new_simple_controller_config( + config={CONF_CONTROLLER: "http://127.0.0.1:222"}, + config_source=ConfigSource.CONFIG_FLOW, + serial_number="second_serial", + devices=(vera_device2,), + ), + ) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entry1 = entity_registry.async_get(entity1_id) + assert entry1 + assert entry1.unique_id == "1" + + entry2 = entity_registry.async_get(entity2_id) + assert entry2 + assert entry2.unique_id == "vera_second_serial_2" async def test_unload( @@ -77,7 +133,7 @@ async def test_unload( """Test function.""" vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor vera_device1.device_id = 1 - vera_device1.vera_device_id = 1 + vera_device1.vera_device_id = vera_device1.device_id vera_device1.name = "first_dev" vera_device1.is_tripped = False @@ -145,6 +201,7 @@ async def test_exclude_and_light_ids( vera_device3 = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch vera_device3.device_id = 3 + vera_device3.vera_device_id = 3 vera_device3.name = "dev3" vera_device3.category = pv.CATEGORY_SWITCH vera_device3.is_switched_on = MagicMock(return_value=False) @@ -152,6 +209,7 @@ async def test_exclude_and_light_ids( vera_device4 = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch vera_device4.device_id = 4 + vera_device4.vera_device_id = 4 vera_device4.name = "dev4" vera_device4.category = pv.CATEGORY_SWITCH vera_device4.is_switched_on = MagicMock(return_value=False) @@ -160,6 +218,7 @@ async def test_exclude_and_light_ids( component_data = await vera_component_factory.configure_component( hass=hass, controller_config=new_simple_controller_config( + config_source=ConfigSource.CONFIG_ENTRY, devices=(vera_device1, vera_device2, vera_device3, vera_device4), config={**{CONF_CONTROLLER: "http://127.0.0.1:123"}, **options}, ), @@ -167,12 +226,10 @@ async def test_exclude_and_light_ids( # Assert the entries were setup correctly. config_entry = next(iter(hass.config_entries.async_entries(DOMAIN))) - assert config_entry.options == { - CONF_LIGHTS: [4, 10, 12], - CONF_EXCLUDE: [1], - } + assert config_entry.options[CONF_LIGHTS] == [4, 10, 12] + assert config_entry.options[CONF_EXCLUDE] == [1] - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback update_callback(vera_device1) update_callback(vera_device2) diff --git a/tests/components/vera/test_light.py b/tests/components/vera/test_light.py index 99391d8d82a..72118e33a31 100644 --- a/tests/components/vera/test_light.py +++ b/tests/components/vera/test_light.py @@ -15,6 +15,7 @@ async def test_light( """Test function.""" vera_device = MagicMock(spec=pv.VeraDimmer) # type: pv.VeraDimmer vera_device.device_id = 1 + vera_device.vera_device_id = vera_device.device_id vera_device.name = "dev1" vera_device.category = pv.CATEGORY_DIMMER vera_device.is_switched_on = MagicMock(return_value=False) @@ -27,7 +28,7 @@ async def test_light( hass=hass, controller_config=new_simple_controller_config(devices=(vera_device,)), ) - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback assert hass.states.get(entity_id).state == "off" diff --git a/tests/components/vera/test_lock.py b/tests/components/vera/test_lock.py index 11af1f5a7b7..b3433b2bafb 100644 --- a/tests/components/vera/test_lock.py +++ b/tests/components/vera/test_lock.py @@ -15,6 +15,7 @@ async def test_lock( """Test function.""" vera_device = MagicMock(spec=pv.VeraLock) # type: pv.VeraLock vera_device.device_id = 1 + vera_device.vera_device_id = vera_device.device_id vera_device.name = "dev1" vera_device.category = pv.CATEGORY_LOCK vera_device.is_locked = MagicMock(return_value=False) @@ -24,7 +25,7 @@ async def test_lock( hass=hass, controller_config=new_simple_controller_config(devices=(vera_device,)), ) - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback assert hass.states.get(entity_id).state == STATE_UNLOCKED diff --git a/tests/components/vera/test_scene.py b/tests/components/vera/test_scene.py index 29ef338b9f1..6c80f27d8c8 100644 --- a/tests/components/vera/test_scene.py +++ b/tests/components/vera/test_scene.py @@ -14,6 +14,7 @@ async def test_scene( """Test function.""" vera_scene = MagicMock(spec=pv.VeraScene) # type: pv.VeraScene vera_scene.scene_id = 1 + vera_scene.vera_scene_id = vera_scene.scene_id vera_scene.name = "dev1" entity_id = "scene.dev1_1" diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py index 36730e8d6d2..58e91a5581b 100644 --- a/tests/components/vera/test_sensor.py +++ b/tests/components/vera/test_sensor.py @@ -23,6 +23,7 @@ async def run_sensor_test( """Test generic sensor.""" vera_device = MagicMock(spec=pv.VeraSensor) # type: pv.VeraSensor vera_device.device_id = 1 + vera_device.vera_device_id = vera_device.device_id vera_device.name = "dev1" vera_device.category = category setattr(vera_device, class_property, "33") @@ -34,7 +35,7 @@ async def run_sensor_test( devices=(vera_device,), setup_callback=setup_callback ), ) - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback for (initial_value, state_value) in assert_states: setattr(vera_device, class_property, initial_value) @@ -175,6 +176,7 @@ async def test_scene_controller_sensor( """Test function.""" vera_device = MagicMock(spec=pv.VeraSensor) # type: pv.VeraSensor vera_device.device_id = 1 + vera_device.vera_device_id = vera_device.device_id vera_device.name = "dev1" vera_device.category = pv.CATEGORY_SCENE_CONTROLLER vera_device.get_last_scene_id = MagicMock(return_value="id0") @@ -185,7 +187,7 @@ async def test_scene_controller_sensor( hass=hass, controller_config=new_simple_controller_config(devices=(vera_device,)), ) - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback vera_device.get_last_scene_time.return_value = "1111" update_callback(vera_device) diff --git a/tests/components/vera/test_switch.py b/tests/components/vera/test_switch.py index 60e31add4bd..42c74e4e843 100644 --- a/tests/components/vera/test_switch.py +++ b/tests/components/vera/test_switch.py @@ -14,6 +14,7 @@ async def test_switch( """Test function.""" vera_device = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch vera_device.device_id = 1 + vera_device.vera_device_id = vera_device.device_id vera_device.name = "dev1" vera_device.category = pv.CATEGORY_SWITCH vera_device.is_switched_on = MagicMock(return_value=False) @@ -21,9 +22,11 @@ async def test_switch( component_data = await vera_component_factory.configure_component( hass=hass, - controller_config=new_simple_controller_config(devices=(vera_device,)), + controller_config=new_simple_controller_config( + devices=(vera_device,), legacy_entity_unique_id=False + ), ) - update_callback = component_data.controller_data.update_callback + update_callback = component_data.controller_data[0].update_callback assert hass.states.get(entity_id).state == "off" From 11319ac479e496eb894c8962639ae67118cc3b3b 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 166/514] 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 8d687c951a32ff4da1cb06c534f24bf40709b33d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Sep 2020 01:48:30 -0500 Subject: [PATCH 167/514] 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 ad2b37880b7..abd7816a861 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 f5dd74f81c2..dca932bb031 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,7 +337,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 2ea604cc2a73bfdd19bfe0b0c1ca71f1a370ad3a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Sep 2020 02:27:30 -0500 Subject: [PATCH 168/514] 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 8236394b8538dc59ae2b6e371e5fb86dd2ac213d Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 15 Sep 2020 09:30:00 +0200 Subject: [PATCH 169/514] 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 5d0fa1417ec6f9a40a529813eb1b23bf18e3ed4f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 15 Sep 2020 10:09:56 +0200 Subject: [PATCH 170/514] Upgrade pytest to 6.0.2 (#39959) Co-authored-by: Paulus Schoutsen --- requirements_test.txt | 2 +- tests/components/dyson/test_climate.py | 4 ++++ tests/components/dyson/test_fan.py | 4 ++++ tests/components/pvpc_hourly_pricing/test_sensor.py | 8 ++------ tests/mock/zwave.py | 2 +- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index c0c0c9d9878..9b4590b613c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -21,7 +21,7 @@ pytest-test-groups==1.0.3 pytest-sugar==0.9.4 pytest-timeout==1.4.2 pytest-xdist==1.32.0 -pytest==5.4.3 +pytest==6.0.2 requests_mock==1.8.0 responses==0.12.0 stdlib-list==0.7.0 diff --git a/tests/components/dyson/test_climate.py b/tests/components/dyson/test_climate.py index 296812bb0cf..124fc46f325 100644 --- a/tests/components/dyson/test_climate.py +++ b/tests/components/dyson/test_climate.py @@ -56,6 +56,10 @@ class MockDysonState(DysonPureHotCoolState): def __init__(self): """Create new Mock Dyson State.""" + def __repr__(self): + """Mock repr because original one fails since constructor not called.""" + return "" + def _get_config(): """Return a config dictionary.""" diff --git a/tests/components/dyson/test_fan.py b/tests/components/dyson/test_fan.py index 807cf3565ed..11770e1f133 100644 --- a/tests/components/dyson/test_fan.py +++ b/tests/components/dyson/test_fan.py @@ -38,6 +38,10 @@ class MockDysonState(DysonPureCoolState): """Create new Mock Dyson State.""" pass + def __repr__(self): + """Mock repr because original one fails since constructor not called.""" + return "" + def _get_dyson_purecool_device(): """Return a valid device as provided by the Dyson web services.""" diff --git a/tests/components/pvpc_hourly_pricing/test_sensor.py b/tests/components/pvpc_hourly_pricing/test_sensor.py index 57861b8b72b..6dae784a0cc 100644 --- a/tests/components/pvpc_hourly_pricing/test_sensor.py +++ b/tests/components/pvpc_hourly_pricing/test_sensor.py @@ -54,12 +54,8 @@ async def test_sensor_availability( # sensor has no more prices, state is "unavailable" from now on await _process_time_step(hass, mock_data, value="unavailable") await _process_time_step(hass, mock_data, value="unavailable") - num_errors = sum( - 1 for x in caplog.get_records("call") if x.levelno == logging.ERROR - ) - num_warnings = sum( - 1 for x in caplog.get_records("call") if x.levelno == logging.WARNING - ) + num_errors = sum(1 for x in caplog.records if x.levelno == logging.ERROR) + num_warnings = sum(1 for x in caplog.records if x.levelno == logging.WARNING) assert num_warnings == 1 assert num_errors == 0 assert pvpc_aioclient_mock.call_count == 9 diff --git a/tests/mock/zwave.py b/tests/mock/zwave.py index 1049108f9de..41c0aa5727b 100644 --- a/tests/mock/zwave.py +++ b/tests/mock/zwave.py @@ -106,7 +106,7 @@ class MockNode(MagicMock): def __init__( self, *, - node_id="567", + node_id=567, name="Mock Node", manufacturer_id="ABCD", product_id="123", From fffc7e2e8e79692e58056f9cc4a4ddd97425f803 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 15 Sep 2020 12:10:22 +0200 Subject: [PATCH 171/514] Upgrade sentry-sdk to 0.17.5 (#40092) --- 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 1a9bb74a8ee..999981c6c27 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.4"], + "requirements": ["sentry-sdk==0.17.5"], "codeowners": ["@dcramer", "@frenck"] } diff --git a/requirements_all.txt b/requirements_all.txt index abd7816a861..567e5b961fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1967,7 +1967,7 @@ sense-hat==2.2.0 sense_energy==0.8.0 # homeassistant.components.sentry -sentry-sdk==0.17.4 +sentry-sdk==0.17.5 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dca932bb031..1538b16ec7d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -918,7 +918,7 @@ samsungtvws==1.4.0 sense_energy==0.8.0 # homeassistant.components.sentry -sentry-sdk==0.17.4 +sentry-sdk==0.17.5 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From 487b56ab69fa0c71bb3498043a6f861af34547a8 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 15 Sep 2020 12:37:31 +0200 Subject: [PATCH 172/514] Fix hvv_departures config flow patches (#40095) --- .../hvv_departures/test_config_flow.py | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index 1e37ff2e021..1646ee73dbd 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -32,8 +32,11 @@ async def test_user_flow(hass): with patch( "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", + ), patch( + "homeassistant.components.hvv_departures.hub.GTI.checkName", + return_value=FIXTURE_CHECK_NAME, + ), patch( + "homeassistant.components.hvv_departures.hub.GTI.stationInformation", return_value=FIXTURE_STATION_INFORMATION, ), patch( "homeassistant.components.hvv_departures.async_setup", return_value=True @@ -96,7 +99,7 @@ async def test_user_flow_no_results(hass): "homeassistant.components.hvv_departures.hub.GTI.init", return_value=FIXTURE_INIT, ), patch( - "pygti.gti.GTI.checkName", + "homeassistant.components.hvv_departures.hub.GTI.checkName", return_value={"returnCode": "OK", "results": []}, ), patch( "homeassistant.components.hvv_departures.async_setup", return_value=True @@ -186,7 +189,7 @@ async def test_user_flow_station(hass): "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True, ), patch( - "pygti.gti.GTI.checkName", + "homeassistant.components.hvv_departures.hub.GTI.checkName", return_value={"returnCode": "OK", "results": []}, ): @@ -220,7 +223,7 @@ async def test_user_flow_station_select(hass): "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True, ), patch( - "pygti.gti.GTI.checkName", + "homeassistant.components.hvv_departures.hub.GTI.checkName", return_value=FIXTURE_CHECK_NAME, ): result_user = await hass.config_entries.flow.async_init( @@ -268,10 +271,11 @@ async def test_options_flow(hass): "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True, ), patch( - "pygti.gti.GTI.departureList", + "homeassistant.components.hvv_departures.hub.GTI.departureList", return_value=FIXTURE_DEPARTURE_LIST, ): 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) @@ -315,14 +319,22 @@ async def test_options_flow_invalid_auth(hass): config_entry.add_to_hass(hass) with patch( - "homeassistant.components.hvv_departures.hub.GTI.init", + "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True + ), patch( + "homeassistant.components.hvv_departures.hub.GTI.departureList", + return_value=FIXTURE_DEPARTURE_LIST, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.hvv_departures.hub.GTI.departureList", side_effect=InvalidAuth( "ERROR_TEXT", "Bei der Verarbeitung der Anfrage ist ein technisches Problem aufgetreten.", "Authentication failed!", ), ): - assert await hass.config_entries.async_setup(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 @@ -348,11 +360,18 @@ async def test_options_flow_cannot_connect(hass): config_entry.add_to_hass(hass) with patch( - "pygti.gti.GTI.departureList", - side_effect=CannotConnect(), + "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True + ), patch( + "homeassistant.components.hvv_departures.hub.GTI.departureList", + return_value=FIXTURE_DEPARTURE_LIST, ): assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + with patch( + "homeassistant.components.hvv_departures.hub.GTI.departureList", + side_effect=CannotConnect(), + ): result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM From 272d36bc9301ad73bec345d106f6ffee2ef6203d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 15 Sep 2020 15:57:10 +0200 Subject: [PATCH 173/514] 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 567e5b961fa..e6d865090f0 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 1538b16ec7d..ce3a61562ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -373,7 +373,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 b116e5862032e3dff8c23feb222a3491ebb6adc9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 15 Sep 2020 16:28:51 +0200 Subject: [PATCH 174/514] Upgrade pytest-xdist to 2.1.0 (#40057) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 9b4590b613c..2674c780b34 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -20,7 +20,7 @@ pytest-cov==2.10.1 pytest-test-groups==1.0.3 pytest-sugar==0.9.4 pytest-timeout==1.4.2 -pytest-xdist==1.32.0 +pytest-xdist==2.1.0 pytest==6.0.2 requests_mock==1.8.0 responses==0.12.0 From 4fbd4957bdbcca9def1060c5c2bc459dd4fac1e0 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 15 Sep 2020 17:29:24 +0300 Subject: [PATCH 175/514] 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 56ba4907e1de212db74e77a813c44919a73fd719 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 15 Sep 2020 16:30:33 +0200 Subject: [PATCH 176/514] 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 c8d550044121ebda46bed76b3edb1eb92623f58b Mon Sep 17 00:00:00 2001 From: Hareesh M U Date: Tue, 15 Sep 2020 21:22:19 +0530 Subject: [PATCH 177/514] Add bimonthly period feature for utility_meter component (#39931) --- homeassistant/components/utility_meter/const.py | 3 ++- .../components/utility_meter/sensor.py | 9 ++++++++- tests/components/utility_meter/test_sensor.py | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/utility_meter/const.py b/homeassistant/components/utility_meter/const.py index 5be7dcf9b69..7b55ec4dcd0 100644 --- a/homeassistant/components/utility_meter/const.py +++ b/homeassistant/components/utility_meter/const.py @@ -5,10 +5,11 @@ HOURLY = "hourly" DAILY = "daily" WEEKLY = "weekly" MONTHLY = "monthly" +BIMONTHLY = "bimonthly" QUARTERLY = "quarterly" YEARLY = "yearly" -METER_TYPES = [HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY] +METER_TYPES = [HOURLY, DAILY, WEEKLY, MONTHLY, BIMONTHLY, QUARTERLY, YEARLY] DATA_UTILITY = "utility_meter_data" diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 8372d8e6b22..54f93422abd 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -24,6 +24,7 @@ import homeassistant.util.dt as dt_util from .const import ( ATTR_VALUE, + BIMONTHLY, CONF_METER, CONF_METER_NET_CONSUMPTION, CONF_METER_OFFSET, @@ -204,6 +205,12 @@ class UtilityMeterSensor(RestoreEntity): and now != date(now.year, now.month, 1) + self._period_offset ): return + if ( + self._period == BIMONTHLY + and now + != date(now.year, (((now.month - 1) // 2) * 2 + 1), 1) + self._period_offset + ): + return if ( self._period == QUARTERLY and now @@ -241,7 +248,7 @@ class UtilityMeterSensor(RestoreEntity): minute=self._period_offset.seconds // 60, second=self._period_offset.seconds % 60, ) - elif self._period in [DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY]: + elif self._period in [DAILY, WEEKLY, MONTHLY, BIMONTHLY, QUARTERLY, YEARLY]: async_track_time_change( self.hass, self._async_reset_meter, diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index c1613c53a20..af2bbdbf6a2 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -288,6 +288,23 @@ async def test_self_reset_monthly(hass, legacy_patchable_time): ) +async def test_self_reset_bimonthly(hass, legacy_patchable_time): + """Test bimonthly reset of meter occurs on even months.""" + await _test_self_reset( + hass, gen_config("bimonthly"), "2017-12-31T23:59:00.000000+00:00" + ) + + +async def test_self_no_reset_bimonthly(hass, legacy_patchable_time): + """Test bimonthly reset of meter does not occur on odd months.""" + await _test_self_reset( + hass, + gen_config("bimonthly"), + "2018-01-01T23:59:00.000000+00:00", + expect_reset=False, + ) + + async def test_self_reset_quarterly(hass, legacy_patchable_time): """Test quarterly reset of meter.""" await _test_self_reset( From 59d610af172ab3685882e187f96b1f970952d361 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Tue, 15 Sep 2020 18:52:42 +0200 Subject: [PATCH 178/514] Upgrade youtube_dl to version 2020.09.14 (#40104) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 5ec4b3ac166..afb0838de37 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -2,7 +2,7 @@ "domain": "media_extractor", "name": "Media Extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", - "requirements": ["youtube_dl==2020.09.06"], + "requirements": ["youtube_dl==2020.09.14"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/requirements_all.txt b/requirements_all.txt index e6d865090f0..1c9f2309c62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2284,7 +2284,7 @@ yeelight==0.5.3 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2020.09.06 +youtube_dl==2020.09.14 # homeassistant.components.zengge zengge==0.2 From db582bdc1b86b605b1d44cf828fca81bd38a6078 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 15 Sep 2020 19:01:07 +0200 Subject: [PATCH 179/514] Use http status constants more, add HTTP_ACCEPTED and HTTP_BAD_GATEWAY (#39993) * Use http status codes and add HTTP_BAD_GATEWAY constant * Address review comments: - using constants in tado integration - using constant in media_player init.py * Add and use HTTP_ACCEPTED constant --- homeassistant/components/alexa/state_report.py | 6 +++--- homeassistant/components/auth/login_flow.py | 8 ++++++-- homeassistant/components/bloomsky/__init__.py | 11 ++++++++--- homeassistant/components/bond/config_flow.py | 9 +++++++-- homeassistant/components/clickatell/notify.py | 4 ++-- homeassistant/components/cloud/http_api.py | 17 +++++++++++++---- homeassistant/components/config/zwave.py | 6 ++++-- .../components/ddwrt/device_tracker.py | 3 ++- homeassistant/components/doorbird/__init__.py | 7 +++++-- .../components/doorbird/config_flow.py | 10 ++++++++-- homeassistant/components/foursquare/__init__.py | 9 +++++++-- .../components/google_assistant/http.py | 8 ++++++-- homeassistant/components/hassio/http.py | 3 ++- homeassistant/components/ios/notify.py | 5 +++-- homeassistant/components/lifx_cloud/scene.py | 10 ++++++++-- .../components/media_player/__init__.py | 5 +++-- .../components/melcloud/config_flow.py | 10 ++++++++-- homeassistant/components/mobile_app/notify.py | 11 ++++++++--- homeassistant/components/nest/local_auth.py | 3 ++- homeassistant/components/onvif/__init__.py | 3 ++- homeassistant/components/sendgrid/notify.py | 3 ++- homeassistant/components/shelly/config_flow.py | 9 +++++++-- homeassistant/components/sigfox/sensor.py | 4 ++-- .../components/smartthings/__init__.py | 3 ++- .../components/synology_chat/notify.py | 4 ++-- homeassistant/components/tesla/config_flow.py | 3 ++- .../components/thethingsnetwork/sensor.py | 4 ++-- .../components/tomato/device_tracker.py | 3 ++- homeassistant/components/verisure/__init__.py | 3 ++- homeassistant/components/withings/common.py | 3 ++- homeassistant/const.py | 2 ++ homeassistant/helpers/data_entry_flow.py | 6 +++--- 32 files changed, 137 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index a61dfc02d10..1d06422056d 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -6,7 +6,7 @@ import logging import aiohttp import async_timeout -from homeassistant.const import MATCH_ALL, STATE_ON +from homeassistant.const import HTTP_ACCEPTED, MATCH_ALL, STATE_ON import homeassistant.util.dt as dt_util from .const import API_CHANGE, Cause @@ -109,7 +109,7 @@ async def async_send_changereport_message( _LOGGER.debug("Sent: %s", json.dumps(message_serialized)) _LOGGER.debug("Received (%s): %s", response.status, response_text) - if response.status == 202: + if response.status == HTTP_ACCEPTED: return response_json = json.loads(response_text) @@ -240,7 +240,7 @@ async def async_send_doorbell_event_message(hass, config, alexa_entity): _LOGGER.debug("Sent: %s", json.dumps(message_serialized)) _LOGGER.debug("Received (%s): %s", response.status, response_text) - if response.status == 202: + if response.status == HTTP_ACCEPTED: return response_json = json.loads(response_text) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index 31e3b7ea648..725450a0a12 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -80,7 +80,11 @@ from homeassistant.components.http.ban import ( ) from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView -from homeassistant.const import HTTP_BAD_REQUEST, HTTP_NOT_FOUND +from homeassistant.const import ( + HTTP_BAD_REQUEST, + HTTP_METHOD_NOT_ALLOWED, + HTTP_NOT_FOUND, +) from . import indieauth @@ -153,7 +157,7 @@ class LoginFlowIndexView(HomeAssistantView): async def get(self, request): """Do not allow index of flows in progress.""" - return web.Response(status=405) + return web.Response(status=HTTP_METHOD_NOT_ALLOWED) @RequestDataValidator( vol.Schema( diff --git a/homeassistant/components/bloomsky/__init__.py b/homeassistant/components/bloomsky/__init__.py index 929f8218144..cd993e0332a 100644 --- a/homeassistant/components/bloomsky/__init__.py +++ b/homeassistant/components/bloomsky/__init__.py @@ -6,7 +6,12 @@ from aiohttp.hdrs import AUTHORIZATION import requests import voluptuous as vol -from homeassistant.const import CONF_API_KEY, HTTP_OK +from homeassistant.const import ( + CONF_API_KEY, + HTTP_METHOD_NOT_ALLOWED, + HTTP_OK, + HTTP_UNAUTHORIZED, +) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -67,9 +72,9 @@ class BloomSky: headers={AUTHORIZATION: self._api_key}, timeout=10, ) - if response.status_code == 401: + if response.status_code == HTTP_UNAUTHORIZED: raise RuntimeError("Invalid API_KEY") - if response.status_code == 405: + if response.status_code == HTTP_METHOD_NOT_ALLOWED: _LOGGER.error("You have no bloomsky devices configured") return if response.status_code != HTTP_OK: diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 49ca559685c..6666cd57ca3 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -7,7 +7,12 @@ from bond_api import Bond import voluptuous as vol from homeassistant import config_entries, exceptions -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_HOST, + CONF_NAME, + HTTP_UNAUTHORIZED, +) from .const import CONF_BOND_ID from .const import DOMAIN # pylint:disable=unused-import @@ -31,7 +36,7 @@ async def _validate_input(data: Dict[str, Any]) -> str: except ClientConnectionError as error: raise InputValidationError("cannot_connect") from error except ClientResponseError as error: - if error.status == 401: + if error.status == HTTP_UNAUTHORIZED: raise InputValidationError("invalid_auth") from error raise InputValidationError("unknown") from error except Exception as error: diff --git a/homeassistant/components/clickatell/notify.py b/homeassistant/components/clickatell/notify.py index 966dbdee6e2..09096f44b74 100644 --- a/homeassistant/components/clickatell/notify.py +++ b/homeassistant/components/clickatell/notify.py @@ -5,7 +5,7 @@ import requests import voluptuous as vol from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService -from homeassistant.const import CONF_API_KEY, CONF_RECIPIENT, HTTP_OK +from homeassistant.const import CONF_API_KEY, CONF_RECIPIENT, HTTP_ACCEPTED, HTTP_OK import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -37,5 +37,5 @@ class ClickatellNotificationService(BaseNotificationService): data = {"apiKey": self.api_key, "to": self.recipient, "content": message} resp = requests.get(BASE_API_URL, params=data, timeout=5) - if (resp.status_code != HTTP_OK) or (resp.status_code != 202): + if (resp.status_code != HTTP_OK) or (resp.status_code != HTTP_ACCEPTED): _LOGGER.error("Error %s : %s", resp.status_code, resp.text) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 00a2ddb4663..3075f6a3f9d 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -19,7 +19,13 @@ from homeassistant.components.google_assistant import helpers as google_helpers from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.websocket_api import const as ws_const -from homeassistant.const import HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, HTTP_OK +from homeassistant.const import ( + HTTP_BAD_GATEWAY, + HTTP_BAD_REQUEST, + HTTP_INTERNAL_SERVER_ERROR, + HTTP_OK, + HTTP_UNAUTHORIZED, +) from homeassistant.core import callback from .const import ( @@ -73,7 +79,10 @@ _CLOUD_ERRORS = { HTTP_INTERNAL_SERVER_ERROR, "Remote UI not compatible with 127.0.0.1/::1 as trusted proxies.", ), - asyncio.TimeoutError: (502, "Unable to reach the Home Assistant cloud."), + asyncio.TimeoutError: ( + HTTP_BAD_GATEWAY, + "Unable to reach the Home Assistant cloud.", + ), aiohttp.ClientError: ( HTTP_INTERNAL_SERVER_ERROR, "Error making internal request", @@ -122,7 +131,7 @@ async def async_setup(hass): HTTP_BAD_REQUEST, "An account with the given email already exists.", ), - auth.Unauthenticated: (401, "Authentication failed."), + auth.Unauthenticated: (HTTP_UNAUTHORIZED, "Authentication failed."), auth.PasswordChangeRequired: ( HTTP_BAD_REQUEST, "Password change required.", @@ -177,7 +186,7 @@ def _process_cloud_exception(exc, where): if err_info is None: _LOGGER.exception("Unexpected error processing request for %s", where) - err_info = (502, f"Unexpected error: {exc}") + err_info = (HTTP_BAD_GATEWAY, f"Unexpected error: {exc}") return err_info diff --git a/homeassistant/components/config/zwave.py b/homeassistant/components/config/zwave.py index edc2e9af42c..7a99d29c774 100644 --- a/homeassistant/components/config/zwave.py +++ b/homeassistant/components/config/zwave.py @@ -6,7 +6,7 @@ from aiohttp.web import Response from homeassistant.components.http import HomeAssistantView from homeassistant.components.zwave import DEVICE_CONFIG_SCHEMA_ENTRY, const -from homeassistant.const import HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_OK +from homeassistant.const import HTTP_ACCEPTED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_OK import homeassistant.core as ha import homeassistant.helpers.config_validation as cv @@ -254,7 +254,9 @@ class ZWaveProtectionView(HomeAssistantView): ) state = node.set_protection(value_id, selection) if not state: - return self.json_message("Protection setting did not complete", 202) + return self.json_message( + "Protection setting did not complete", HTTP_ACCEPTED + ) return self.json_message("Protection setting succsessfully set", HTTP_OK) return await hass.async_add_executor_job(_set_protection) diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py index 9b6fd1bdb64..c324d6a5b64 100644 --- a/homeassistant/components/ddwrt/device_tracker.py +++ b/homeassistant/components/ddwrt/device_tracker.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, HTTP_OK, + HTTP_UNAUTHORIZED, ) import homeassistant.helpers.config_validation as cv @@ -155,7 +156,7 @@ class DdWrtDeviceScanner(DeviceScanner): return if response.status_code == HTTP_OK: return _parse_ddwrt_response(response.text) - if response.status_code == 401: + if response.status_code == HTTP_UNAUTHORIZED: # Authentication error _LOGGER.exception( "Failed to authenticate, check your username and password" diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 43ab0c96153..1f7e02e8569 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_TOKEN, CONF_USERNAME, HTTP_OK, + HTTP_UNAUTHORIZED, ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -127,7 +128,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): status = await hass.async_add_executor_job(device.ready) info = await hass.async_add_executor_job(device.info) except urllib.error.HTTPError as err: - if err.code == 401: + if err.code == HTTP_UNAUTHORIZED: _LOGGER.error( "Authorization rejected by DoorBird for %s@%s", username, device_ip ) @@ -357,7 +358,9 @@ class DoorBirdRequestView(HomeAssistantView): device = get_doorstation_by_token(hass, token) if device is None: - return web.Response(status=401, text="Invalid token provided.") + return web.Response( + status=HTTP_UNAUTHORIZED, text="Invalid token provided." + ) if device: event_data = device.get_event_data() diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 07b753da6ee..8e3f661254d 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -7,7 +7,13 @@ from doorbirdpy import DoorBird import voluptuous as vol from homeassistant import config_entries, core, exceptions -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + HTTP_UNAUTHORIZED, +) from homeassistant.core import callback from homeassistant.util.network import is_link_local @@ -39,7 +45,7 @@ async def validate_input(hass: core.HomeAssistant, data): status = await hass.async_add_executor_job(device.ready) info = await hass.async_add_executor_job(device.info) except urllib.error.HTTPError as err: - if err.code == 401: + if err.code == HTTP_UNAUTHORIZED: raise InvalidAuth from err raise CannotConnect from err except OSError as err: diff --git a/homeassistant/components/foursquare/__init__.py b/homeassistant/components/foursquare/__init__.py index bae0336a63e..6f33c9ff591 100644 --- a/homeassistant/components/foursquare/__init__.py +++ b/homeassistant/components/foursquare/__init__.py @@ -5,7 +5,12 @@ import requests import voluptuous as vol from homeassistant.components.http import HomeAssistantView -from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_BAD_REQUEST, HTTP_OK +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + HTTP_BAD_REQUEST, + HTTP_CREATED, + HTTP_OK, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -55,7 +60,7 @@ def setup(hass, config): url = f"https://api.foursquare.com/v2/checkins/add?oauth_token={config[CONF_ACCESS_TOKEN]}&v=20160802&m=swarm" response = requests.post(url, data=call.data, timeout=10) - if response.status_code not in (HTTP_OK, 201): + if response.status_code not in (HTTP_OK, HTTP_CREATED): _LOGGER.exception( "Error checking in user. Response %d: %s:", response.status_code, diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 7b75a36f8bb..4bf0df8b933 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -10,7 +10,11 @@ import jwt # Typing imports from homeassistant.components.http import HomeAssistantView -from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_INTERNAL_SERVER_ERROR +from homeassistant.const import ( + CLOUD_NEVER_EXPOSED_ENTITIES, + HTTP_INTERNAL_SERVER_ERROR, + HTTP_UNAUTHORIZED, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util @@ -200,7 +204,7 @@ class GoogleConfig(AbstractConfig): try: return await _call() except ClientResponseError as error: - if error.status == 401: + if error.status == HTTP_UNAUTHORIZED: _LOGGER.warning( "Request for %s unauthorized, renewing token and retrying", url ) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index be2cec5ae9c..6dccdeb2101 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -12,6 +12,7 @@ from aiohttp.web_exceptions import HTTPBadGateway import async_timeout from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView +from homeassistant.const import HTTP_UNAUTHORIZED from .const import X_HASS_IS_ADMIN, X_HASS_USER_ID, X_HASSIO @@ -53,7 +54,7 @@ class HassIOView(HomeAssistantView): ) -> Union[web.Response, web.StreamResponse]: """Route data to Hass.io.""" if _need_auth(path) and not request[KEY_AUTHENTICATED]: - return web.Response(status=401) + return web.Response(status=HTTP_UNAUTHORIZED) return await self._command_proxy(path, request) diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py index 62dd72973da..f9c682bf527 100644 --- a/homeassistant/components/ios/notify.py +++ b/homeassistant/components/ios/notify.py @@ -12,6 +12,7 @@ from homeassistant.components.notify import ( ATTR_TITLE_DEFAULT, BaseNotificationService, ) +from homeassistant.const import HTTP_CREATED, HTTP_TOO_MANY_REQUESTS import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -90,13 +91,13 @@ class iOSNotificationService(BaseNotificationService): req = requests.post(PUSH_URL, json=data, timeout=10) - if req.status_code != 201: + if req.status_code != HTTP_CREATED: fallback_error = req.json().get("errorMessage", "Unknown error") fallback_message = ( f"Internal server error, please try again later: {fallback_error}" ) message = req.json().get("message", fallback_message) - if req.status_code == 429: + if req.status_code == HTTP_TOO_MANY_REQUESTS: _LOGGER.warning(message) log_rate_limits(self.hass, target, req.json(), 30) else: diff --git a/homeassistant/components/lifx_cloud/scene.py b/homeassistant/components/lifx_cloud/scene.py index 75ab4656794..cf39d70d89a 100644 --- a/homeassistant/components/lifx_cloud/scene.py +++ b/homeassistant/components/lifx_cloud/scene.py @@ -9,7 +9,13 @@ import async_timeout import voluptuous as vol from homeassistant.components.scene import Scene -from homeassistant.const import CONF_PLATFORM, CONF_TIMEOUT, CONF_TOKEN, HTTP_OK +from homeassistant.const import ( + CONF_PLATFORM, + CONF_TIMEOUT, + CONF_TOKEN, + HTTP_OK, + HTTP_UNAUTHORIZED, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -50,7 +56,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= devices = [LifxCloudScene(hass, headers, timeout, scene) for scene in data] async_add_entities(devices) return True - if status == 401: + if status == HTTP_UNAUTHORIZED: _LOGGER.error("Unauthorized (bad token?) on %s", url) return False diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 348bc521a5a..47804fcc1cd 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -27,6 +27,7 @@ from homeassistant.const import ( HTTP_INTERNAL_SERVER_ERROR, HTTP_NOT_FOUND, HTTP_OK, + HTTP_UNAUTHORIZED, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, @@ -880,7 +881,7 @@ class MediaPlayerImageView(HomeAssistantView): """Start a get request.""" player = self.component.get_entity(entity_id) if player is None: - status = HTTP_NOT_FOUND if request[KEY_AUTHENTICATED] else 401 + status = HTTP_NOT_FOUND if request[KEY_AUTHENTICATED] else HTTP_UNAUTHORIZED return web.Response(status=status) authenticated = ( @@ -889,7 +890,7 @@ class MediaPlayerImageView(HomeAssistantView): ) if not authenticated: - return web.Response(status=401) + return web.Response(status=HTTP_UNAUTHORIZED) data, content_type = await player.async_get_media_image() diff --git a/homeassistant/components/melcloud/config_flow.py b/homeassistant/components/melcloud/config_flow.py index ed6fc31c414..8813883c151 100644 --- a/homeassistant/components/melcloud/config_flow.py +++ b/homeassistant/components/melcloud/config_flow.py @@ -9,7 +9,13 @@ import pymelcloud import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME, HTTP_FORBIDDEN +from homeassistant.const import ( + CONF_PASSWORD, + CONF_TOKEN, + CONF_USERNAME, + HTTP_FORBIDDEN, + HTTP_UNAUTHORIZED, +) from .const import DOMAIN # pylint: disable=unused-import @@ -57,7 +63,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.hass.helpers.aiohttp_client.async_get_clientsession(), ) except ClientResponseError as err: - if err.status == 401 or err.status == HTTP_FORBIDDEN: + if err.status == HTTP_UNAUTHORIZED or err.status == HTTP_FORBIDDEN: return self.async_abort(reason="invalid_auth") return self.async_abort(reason="cannot_connect") except (asyncio.TimeoutError, ClientError): diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 62bb5fdf08d..04d308a5a05 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -12,7 +12,12 @@ from homeassistant.components.notify import ( ATTR_TITLE_DEFAULT, BaseNotificationService, ) -from homeassistant.const import HTTP_OK +from homeassistant.const import ( + HTTP_ACCEPTED, + HTTP_CREATED, + HTTP_OK, + HTTP_TOO_MANY_REQUESTS, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.util.dt as dt_util @@ -135,7 +140,7 @@ class MobileAppNotificationService(BaseNotificationService): response = await self._session.post(push_url, json=data) result = await response.json() - if response.status in [HTTP_OK, 201, 202]: + if response.status in [HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED]: log_rate_limits(self.hass, entry_data[ATTR_DEVICE_NAME], result) continue @@ -152,7 +157,7 @@ class MobileAppNotificationService(BaseNotificationService): " This message is generated externally to Home Assistant." ) - if response.status == 429: + if response.status == HTTP_TOO_MANY_REQUESTS: _LOGGER.warning(message) log_rate_limits( self.hass, entry_data[ATTR_DEVICE_NAME], result, logging.WARNING diff --git a/homeassistant/components/nest/local_auth.py b/homeassistant/components/nest/local_auth.py index 8b0af5011ec..df03c054398 100644 --- a/homeassistant/components/nest/local_auth.py +++ b/homeassistant/components/nest/local_auth.py @@ -4,6 +4,7 @@ from functools import partial from nest.nest import AUTHORIZE_URL, AuthorizationError, NestAuth +from homeassistant.const import HTTP_UNAUTHORIZED from homeassistant.core import callback from . import config_flow @@ -42,7 +43,7 @@ async def resolve_auth_code(hass, client_id, client_secret, code): await hass.async_add_job(auth.login) return await result except AuthorizationError as err: - if err.response.status_code == 401: + if err.response.status_code == HTTP_UNAUTHORIZED: raise config_flow.CodeInvalid() raise config_flow.NestAuthError( f"Unknown error: {err} ({err.response.status_code})" diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 964c7a70a6d..cf92f3df3ba 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -17,6 +17,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, + HTTP_UNAUTHORIZED, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -138,7 +139,7 @@ async def _get_snapshot_auth(hass, device, entry): try: response = await hass.async_add_executor_job(_get) - if response.status_code == 401: + if response.status_code == HTTP_UNAUTHORIZED: return HTTP_BASIC_AUTHENTICATION return HTTP_DIGEST_AUTHENTICATION diff --git a/homeassistant/components/sendgrid/notify.py b/homeassistant/components/sendgrid/notify.py index 6dbf4d5c2b7..8d8907f2dcf 100644 --- a/homeassistant/components/sendgrid/notify.py +++ b/homeassistant/components/sendgrid/notify.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_RECIPIENT, CONF_SENDER, CONTENT_TYPE_TEXT_PLAIN, + HTTP_ACCEPTED, ) import homeassistant.helpers.config_validation as cv @@ -65,5 +66,5 @@ class SendgridNotificationService(BaseNotificationService): } response = self._sg.client.mail.send.post(request_body=data) - if response.status_code != 202: + if response.status_code != HTTP_ACCEPTED: _LOGGER.error("Unable to send notification") diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 6446d2dd2d2..0ebf70d2f00 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -8,7 +8,12 @@ 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.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + HTTP_UNAUTHORIZED, +) from homeassistant.helpers import aiohttp_client from .const import DOMAIN # pylint:disable=unused-import @@ -91,7 +96,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: device_info = await validate_input(self.hass, self.host, user_input) except aiohttp.ClientResponseError as error: - if error.status == 401: + if error.status == HTTP_UNAUTHORIZED: errors["base"] = "invalid_auth" else: errors["base"] = "cannot_connect" diff --git a/homeassistant/components/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py index 6df6e1d0c82..1de3cfeb8a0 100644 --- a/homeassistant/components/sigfox/sensor.py +++ b/homeassistant/components/sigfox/sensor.py @@ -8,7 +8,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, HTTP_OK +from homeassistant.const import CONF_NAME, HTTP_OK, HTTP_UNAUTHORIZED import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -67,7 +67,7 @@ class SigfoxAPI: url = urljoin(API_URL, "devicetypes") response = requests.get(url, auth=self._auth, timeout=10) if response.status_code != HTTP_OK: - if response.status_code == 401: + if response.status_code == HTTP_UNAUTHORIZED: _LOGGER.error("Invalid credentials for Sigfox API") else: _LOGGER.error( diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 974cde35faf..d184a3ca6ce 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -14,6 +14,7 @@ from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, HTTP_FORBIDDEN, + HTTP_UNAUTHORIZED, ) from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -158,7 +159,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): hass.data[DOMAIN][DATA_BROKERS][entry.entry_id] = broker except ClientResponseError as ex: - if ex.status in (401, HTTP_FORBIDDEN): + if ex.status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN): _LOGGER.exception( "Unable to setup configuration entry '%s' - please reconfigure the integration", entry.title, diff --git a/homeassistant/components/synology_chat/notify.py b/homeassistant/components/synology_chat/notify.py index c8f665cb408..df43c5668f3 100644 --- a/homeassistant/components/synology_chat/notify.py +++ b/homeassistant/components/synology_chat/notify.py @@ -10,7 +10,7 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import CONF_RESOURCE, CONF_VERIFY_SSL, HTTP_OK +from homeassistant.const import CONF_RESOURCE, CONF_VERIFY_SSL, HTTP_CREATED, HTTP_OK import homeassistant.helpers.config_validation as cv ATTR_FILE_URL = "file_url" @@ -57,7 +57,7 @@ class SynologyChatNotificationService(BaseNotificationService): self._resource, data=to_send, timeout=10, verify=self._verify_ssl ) - if response.status_code not in (HTTP_OK, 201): + if response.status_code not in (HTTP_OK, HTTP_CREATED): _LOGGER.exception( "Error sending message. Response %d: %s:", response.status_code, diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py index 9e46a30972f..f21c8c76e23 100644 --- a/homeassistant/components/tesla/config_flow.py +++ b/homeassistant/components/tesla/config_flow.py @@ -11,6 +11,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_USERNAME, + HTTP_UNAUTHORIZED, ) from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv @@ -140,7 +141,7 @@ async def validate_input(hass: core.HomeAssistant, data): test_login=True ) except TeslaException as ex: - if ex.code == 401: + if ex.code == HTTP_UNAUTHORIZED: _LOGGER.error("Invalid credentials: %s", ex) raise InvalidAuth() from ex _LOGGER.error("Unable to communicate with Tesla API: %s", ex) diff --git a/homeassistant/components/thethingsnetwork/sensor.py b/homeassistant/components/thethingsnetwork/sensor.py index 35a16d30f32..3555816213a 100644 --- a/homeassistant/components/thethingsnetwork/sensor.py +++ b/homeassistant/components/thethingsnetwork/sensor.py @@ -8,7 +8,7 @@ import async_timeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONTENT_TYPE_JSON, HTTP_NOT_FOUND +from homeassistant.const import CONTENT_TYPE_JSON, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -135,7 +135,7 @@ class TtnDataStorage: _LOGGER.error("The device is not available: %s", self._device_id) return None - if status == 401: + if status == HTTP_UNAUTHORIZED: _LOGGER.error("Not authorized for Application ID: %s", self._app_id) return None diff --git a/homeassistant/components/tomato/device_tracker.py b/homeassistant/components/tomato/device_tracker.py index 873da5a7864..ce660a60280 100644 --- a/homeassistant/components/tomato/device_tracker.py +++ b/homeassistant/components/tomato/device_tracker.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, HTTP_OK, + HTTP_UNAUTHORIZED, ) import homeassistant.helpers.config_validation as cv @@ -111,7 +112,7 @@ class TomatoDeviceScanner(DeviceScanner): self.last_results[param] = json.loads(value.replace("'", '"')) return True - if response.status_code == 401: + if response.status_code == HTTP_UNAUTHORIZED: # Authentication error _LOGGER.exception( "Failed to authenticate, please check your username and password" diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 757e299792a..8cd8b0672cf 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, + HTTP_SERVICE_UNAVAILABLE, ) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv @@ -189,7 +190,7 @@ class VerisureHub: self.overview = self.session.get_overview() except verisure.ResponseError as ex: _LOGGER.error("Could not read overview, %s", ex) - if ex.status_code == 503: # Service unavailable + if ex.status_code == HTTP_SERVICE_UNAVAILABLE: # Service unavailable _LOGGER.info("Trying to log in again") self.login() else: diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index af81c0e68d3..067133f331f 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -29,6 +29,7 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_WEBHOOK_ID, + HTTP_UNAUTHORIZED, MASS_KILOGRAMS, PERCENTAGE, SPEED_METERS_PER_SECOND, @@ -54,7 +55,7 @@ from .const import Measurement _LOGGER = logging.getLogger(const.LOG_NAMESPACE) NOT_AUTHENTICATED_ERROR = re.compile( - "^401,.*", + f"^{HTTP_UNAUTHORIZED},.*", re.IGNORECASE, ) DATA_UPDATED_SIGNAL = "withings_entity_state_updated" diff --git a/homeassistant/const.py b/homeassistant/const.py index 81f2243bca3..fc44f6a8712 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -564,6 +564,7 @@ URL_API_TEMPLATE = "/api/template" HTTP_OK = 200 HTTP_CREATED = 201 +HTTP_ACCEPTED = 202 HTTP_MOVED_PERMANENTLY = 301 HTTP_BAD_REQUEST = 400 HTTP_UNAUTHORIZED = 401 @@ -573,6 +574,7 @@ HTTP_METHOD_NOT_ALLOWED = 405 HTTP_UNPROCESSABLE_ENTITY = 422 HTTP_TOO_MANY_REQUESTS = 429 HTTP_INTERNAL_SERVER_ERROR = 500 +HTTP_BAD_GATEWAY = 502 HTTP_SERVICE_UNAVAILABLE = 503 HTTP_BASIC_AUTHENTICATION = "basic" diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 1cf1fa4545c..e686dd2ae4b 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.const import HTTP_NOT_FOUND +from homeassistant.const import HTTP_BAD_REQUEST, HTTP_NOT_FOUND import homeassistant.helpers.config_validation as cv @@ -76,7 +76,7 @@ class FlowManagerIndexView(_BaseFlowManagerView): except data_entry_flow.UnknownHandler: return self.json_message("Invalid handler specified", HTTP_NOT_FOUND) except data_entry_flow.UnknownStep: - return self.json_message("Handler does not support user", 400) + return self.json_message("Handler does not support user", HTTP_BAD_REQUEST) result = self._prepare_result_json(result) @@ -107,7 +107,7 @@ class FlowManagerResourceView(_BaseFlowManagerView): except data_entry_flow.UnknownFlow: return self.json_message("Invalid flow specified", HTTP_NOT_FOUND) except vol.Invalid: - return self.json_message("User input malformed", 400) + return self.json_message("User input malformed", HTTP_BAD_REQUEST) result = self._prepare_result_json(result) From 5a12056e59001afd2414c62e9442e37970d0d080 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 15 Sep 2020 19:59:26 +0200 Subject: [PATCH 180/514] Add and use volume cubic constants (#40106) --- homeassistant/components/airvisual/sensor.py | 4 ---- homeassistant/components/comfoconnect/sensor.py | 5 +++-- homeassistant/components/isy994/const.py | 12 +++++++----- homeassistant/components/mysensors/sensor.py | 3 ++- homeassistant/components/rainmachine/sensor.py | 4 ++-- .../components/zha/core/channels/smartenergy.py | 12 +++++++++--- homeassistant/const.py | 1 + 7 files changed, 24 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 895ffa494a4..c67f31d0c3f 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -38,10 +38,6 @@ ATTR_POLLUTANT_SYMBOL = "pollutant_symbol" ATTR_POLLUTANT_UNIT = "pollutant_unit" ATTR_REGION = "region" -MASS_PARTS_PER_MILLION = "ppm" -MASS_PARTS_PER_BILLION = "ppb" -VOLUME_MICROGRAMS_PER_CUBIC_METER = "µg/m3" - SENSOR_KIND_LEVEL = "air_pollution_level" SENSOR_KIND_AQI = "air_quality_index" SENSOR_KIND_POLLUTANT = "main_pollutant" diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 6ec92477555..5f04ba134a8 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -34,6 +34,7 @@ from homeassistant.const import ( TEMP_CELSIUS, TIME_DAYS, TIME_HOURS, + VOLUME_CUBIC_METERS, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -159,14 +160,14 @@ SENSOR_TYPES = { ATTR_AIR_FLOW_SUPPLY: { ATTR_DEVICE_CLASS: None, ATTR_LABEL: "Supply airflow", - ATTR_UNIT: f"m³/{TIME_HOURS}", + ATTR_UNIT: f"{VOLUME_CUBIC_METERS}/{TIME_HOURS}", ATTR_ICON: "mdi:fan", ATTR_ID: SENSOR_FAN_SUPPLY_FLOW, }, ATTR_AIR_FLOW_EXHAUST: { ATTR_DEVICE_CLASS: None, ATTR_LABEL: "Exhaust airflow", - ATTR_UNIT: f"m³/{TIME_HOURS}", + ATTR_UNIT: f"{VOLUME_CUBIC_METERS}/{TIME_HOURS}", ATTR_ICON: "mdi:fan", ATTR_ID: SENSOR_FAN_EXHAUST_FLOW, }, diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index d911fae2c82..4fb32d3843d 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -86,6 +86,8 @@ from homeassistant.const import ( TIME_YEARS, UV_INDEX, VOLT, + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, VOLUME_GALLONS, VOLUME_LITERS, ) @@ -316,9 +318,9 @@ UOM_FRIENDLY_NAME = { "3": f"btu/{TIME_HOURS}", "4": TEMP_CELSIUS, "5": LENGTH_CENTIMETERS, - "6": f"{LENGTH_FEET}³", - "7": f"{LENGTH_FEET}³/{TIME_MINUTES}", - "8": "m³", + "6": VOLUME_CUBIC_FEET, + "7": f"{VOLUME_CUBIC_FEET}/{TIME_MINUTES}", + "8": f"{VOLUME_CUBIC_METERS}", "9": TIME_DAYS, "10": TIME_DAYS, "12": "dB", @@ -347,7 +349,7 @@ UOM_FRIENDLY_NAME = { "36": "lx", "37": "mercalli", "38": LENGTH_METERS, - "39": f"{LENGTH_METERS}³/{TIME_HOURS}", + "39": f"{VOLUME_CUBIC_METERS}/{TIME_HOURS}", "40": SPEED_METERS_PER_SECOND, "41": "mA", "42": TIME_MILLISECONDS, @@ -385,7 +387,7 @@ UOM_FRIENDLY_NAME = { "83": LENGTH_KILOMETERS, "85": "Ω", "86": "kΩ", - "87": f"{LENGTH_METERS}³/{LENGTH_METERS}³", + "87": f"{VOLUME_CUBIC_METERS}/{VOLUME_CUBIC_METERS}", "88": "Water activity", "89": "RPM", "90": FREQUENCY_HERTZ, diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 8ff2139a7b4..24c1d9be09b 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -15,6 +15,7 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, VOLT, + VOLUME_CUBIC_METERS, ) SENSORS = { @@ -36,7 +37,7 @@ SENSORS = { "V_KWH": [ENERGY_KILO_WATT_HOUR, None], "V_LIGHT_LEVEL": [PERCENTAGE, "mdi:white-balance-sunny"], "V_FLOW": [LENGTH_METERS, "mdi:gauge"], - "V_VOLUME": ["m³", None], + "V_VOLUME": [f"{VOLUME_CUBIC_METERS}", None], "V_LEVEL": { "S_SOUND": ["dB", "mdi:volume-high"], "S_VIBRATION": [FREQUENCY_HERTZ, None], diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 6f87c34d607..8534748978d 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -1,7 +1,7 @@ """This platform provides support for sensor data from RainMachine.""" import logging -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import TEMP_CELSIUS, VOLUME_CUBIC_METERS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -26,7 +26,7 @@ SENSORS = { TYPE_FLOW_SENSOR_CLICK_M3: ( "Flow Sensor Clicks", "mdi:water-pump", - "clicks/m^3", + f"clicks/{VOLUME_CUBIC_METERS}", None, False, DATA_PROVISION_SETTINGS, diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 9138ea09782..a4bd2bf2d40 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -3,7 +3,13 @@ import logging import zigpy.zcl.clusters.smartenergy as smartenergy -from homeassistant.const import LENGTH_FEET, POWER_WATT, TIME_HOURS, TIME_SECONDS +from homeassistant.const import ( + POWER_WATT, + TIME_HOURS, + TIME_SECONDS, + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, +) from homeassistant.core import callback from .. import registries, typing as zha_typing @@ -61,8 +67,8 @@ class Metering(ZigbeeChannel): unit_of_measure_map = { 0x00: POWER_WATT, - 0x01: f"m³/{TIME_HOURS}", - 0x02: f"{LENGTH_FEET}³/{TIME_HOURS}", + 0x01: f"{VOLUME_CUBIC_METERS}/{TIME_HOURS}", + 0x02: f"{VOLUME_CUBIC_FEET}/{TIME_HOURS}", 0x03: f"ccf/{TIME_HOURS}", 0x04: f"US gal/{TIME_HOURS}", 0x05: f"IMP gal/{TIME_HOURS}", diff --git a/homeassistant/const.py b/homeassistant/const.py index fc44f6a8712..22fde9f21dd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -422,6 +422,7 @@ PRESSURE_PSI: str = "psi" VOLUME_LITERS: str = "L" VOLUME_MILLILITERS: str = "mL" VOLUME_CUBIC_METERS = f"{LENGTH_METERS}³" +VOLUME_CUBIC_FEET = f"{LENGTH_FEET}³" VOLUME_GALLONS: str = "gal" VOLUME_FLUID_OUNCE: str = "fl. oz." From 749d3c360a68f7874c09d09140a7e4c915c72326 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 15 Sep 2020 19:01:36 +0100 Subject: [PATCH 181/514] 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 74cef6966d6a9469566694fd1120d1123f8d183a Mon Sep 17 00:00:00 2001 From: Dan Klaffenbach Date: Tue, 15 Sep 2020 21:13:17 +0200 Subject: [PATCH 182/514] Expose angle and xy attributes in deCONZ event if present (#39822) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These attribute are used by the color wheel on the Müller Licht tint remote control. --- homeassistant/components/deconz/const.py | 2 ++ .../components/deconz/deconz_event.py | 8 ++++++- tests/components/deconz/test_deconz_event.py | 24 ++++++++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index c2190321fdf..0937339ab94 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -44,4 +44,6 @@ POWER_PLUGS = ["On/Off light", "On/Off plug-in unit", "Smart plug"] SIRENS = ["Warning device"] SWITCH_TYPES = POWER_PLUGS + SIRENS +CONF_ANGLE = "angle" CONF_GESTURE = "gesture" +CONF_XY = "xy" diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 9ad4a7f3162..d1325bcddd6 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -3,7 +3,7 @@ from homeassistant.const import CONF_EVENT, CONF_ID, CONF_UNIQUE_ID from homeassistant.core import callback from homeassistant.util import slugify -from .const import CONF_GESTURE, LOGGER +from .const import CONF_ANGLE, CONF_GESTURE, CONF_XY, LOGGER from .deconz_device import DeconzBase CONF_DECONZ_EVENT = "deconz_event" @@ -52,6 +52,12 @@ class DeconzEvent(DeconzBase): if self._device.gesture is not None: data[CONF_GESTURE] = self._device.gesture + if self._device.angle is not None: + data[CONF_ANGLE] = self._device.angle + + if self._device.xy is not None: + data[CONF_XY] = self._device.xy + self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data) async def async_update_device_registry(self): diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index fc8d4f9d1ba..525821e389f 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -40,6 +40,14 @@ SENSORS = { "config": {"battery": 100}, "uniqueid": "00:00:00:00:00:00:00:04-00", }, + "5": { + "id": "ZHA remote 1 id", + "name": "ZHA remote 1", + "type": "ZHASwitch", + "state": {"angle": 0, "buttonevent": 1000, "xy": [0.0, 0.0]}, + "config": {"group": "4,5,6", "reachable": True, "on": True}, + "uniqueid": "00:00:00:00:00:00:00:05-00", + }, } @@ -53,7 +61,7 @@ async def test_deconz_events(hass): assert "sensor.switch_2" not in gateway.deconz_ids assert "sensor.switch_2_battery_level" in gateway.deconz_ids assert len(hass.states.async_all()) == 3 - assert len(gateway.events) == 4 + assert len(gateway.events) == 5 switch_1 = hass.states.get("sensor.switch_1") assert switch_1 is None @@ -101,6 +109,20 @@ async def test_deconz_events(hass): "gesture": 0, } + gateway.api.sensors["5"].update( + {"state": {"buttonevent": 6002, "angle": 110, "xy": [0.5982, 0.3897]}} + ) + await hass.async_block_till_done() + + assert len(events) == 4 + assert events[3].data == { + "id": "zha_remote_1", + "unique_id": "00:00:00:00:00:00:00:05", + "event": 6002, + "angle": 110, + "xy": [0.5982, 0.3897], + } + await gateway.async_reset() assert len(hass.states.async_all()) == 0 From f651b1f54b16e5dd906cc4d3a0292dc579a26fe4 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 15 Sep 2020 21:49:08 +0200 Subject: [PATCH 183/514] Extract Netatmo test data (#40094) --- tests/components/netatmo/test_media_source.py | 68 ++----------------- tests/fixtures/netatmo/events.txt | 61 +++++++++++++++++ 2 files changed, 68 insertions(+), 61 deletions(-) create mode 100644 tests/fixtures/netatmo/events.txt diff --git a/tests/components/netatmo/test_media_source.py b/tests/components/netatmo/test_media_source.py index 1773c0d83da..fd36c57dfd1 100644 --- a/tests/components/netatmo/test_media_source.py +++ b/tests/components/netatmo/test_media_source.py @@ -1,4 +1,6 @@ """Test Local Media Source.""" +import ast + import pytest from homeassistant.components import media_source @@ -7,6 +9,8 @@ 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 +from tests.common import load_fixture + async def test_async_browse_media(hass): """Test browse media.""" @@ -14,67 +18,9 @@ async def test_async_browse_media(hass): # Prepare cached Netatmo event date hass.data[DOMAIN] = {} - hass.data[DOMAIN][DATA_EVENTS] = { - "12:34:56:78:90:ab": { - 1599152672: { - "id": "12345", - "type": "person", - "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", - "type": "person", - "time": 1599152673, - "camera_id": "12:34:56:78:90:ab", - "snapshot": { - "url": "https://netatmocameraimage", - }, - "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_EVENTS] = ast.literal_eval( + load_fixture("netatmo/events.txt") + ) hass.data[DOMAIN][DATA_CAMERAS] = { "12:34:56:78:90:ab": "MyCamera", diff --git a/tests/fixtures/netatmo/events.txt b/tests/fixtures/netatmo/events.txt new file mode 100644 index 00000000000..f2bc29f782c --- /dev/null +++ b/tests/fixtures/netatmo/events.txt @@ -0,0 +1,61 @@ +{ + "12:34:56:78:90:ab": { + 1599152672: { + "id": "12345", + "type": "person", + "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", + "type": "person", + "time": 1599152673, + "camera_id": "12:34:56:78:90:ab", + "snapshot": { + "url": "https://netatmocameraimage", + }, + "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", + }, + } +} \ No newline at end of file From a71a4d642bd1ddd47dbc455bb470f29061797914 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 15 Sep 2020 22:38:45 +0200 Subject: [PATCH 184/514] Clean dyson climate tests (#40110) * Move setup without devices * Remove not needed tests * Move set temp test * Move set temp cool mode test * Run black * Move test target temperature * Move test current temperature * Move test current humidity * Move test hvac mode * Move test hvac modes * Move test fan modes * Move test service fan modes * Move test fan state * Move test supported features * Move test idle state * Move test humidity invalid state * Move test humidity without state * Move test device cool * Rename test * Move test set mode service * Clean imports * Clean strings * Only load climate platform for climate tests --- tests/components/dyson/test_climate.py | 522 +++++++++++++------------ 1 file changed, 262 insertions(+), 260 deletions(-) diff --git a/tests/components/dyson/test_climate.py b/tests/components/dyson/test_climate.py index 124fc46f325..77105dc73db 100644 --- a/tests/components/dyson/test_climate.py +++ b/tests/components/dyson/test_climate.py @@ -1,6 +1,5 @@ """Test the Dyson fan component.""" import json -import unittest from libpurecool.const import ( FanPower, @@ -15,8 +14,8 @@ from libpurecool.dyson_pure_hotcool import DysonPureHotCool from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink from libpurecool.dyson_pure_state import DysonPureHotCoolState from libpurecool.dyson_pure_state_v2 import DysonPureHotCoolV2State +import pytest -from homeassistant.components import dyson as dyson_parent from homeassistant.components.climate import ( DOMAIN, SERVICE_SET_FAN_MODE, @@ -25,9 +24,16 @@ from homeassistant.components.climate import ( ) from homeassistant.components.climate.const import ( ATTR_CURRENT_HUMIDITY, + ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, + ATTR_FAN_MODES, ATTR_HVAC_ACTION, ATTR_HVAC_MODE, + ATTR_HVAC_MODES, + ATTR_MAX_TEMP, + ATTR_MIN_TEMP, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -40,19 +46,29 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, HVAC_MODE_OFF, ) -from homeassistant.components.dyson import climate as dyson -from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.components.dyson import CONF_LANGUAGE, DOMAIN as DYSON_DOMAIN +from homeassistant.components.dyson.climate import FAN_DIFFUSE, FAN_FOCUS, SUPPORT_FLAGS +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + ATTR_TEMPERATURE, + CONF_DEVICES, + CONF_PASSWORD, + CONF_USERNAME, + TEMP_CELSIUS, +) from homeassistant.setup import async_setup_component from .common import load_mock_device -from tests.async_mock import MagicMock, Mock, patch -from tests.common import get_test_home_assistant +from tests.async_mock import Mock, call, patch class MockDysonState(DysonPureHotCoolState): """Mock Dyson state.""" + # pylint: disable=super-init-not-called + def __init__(self): """Create new Mock Dyson State.""" @@ -64,11 +80,11 @@ class MockDysonState(DysonPureHotCoolState): def _get_config(): """Return a config dictionary.""" return { - dyson_parent.DOMAIN: { - dyson_parent.CONF_USERNAME: "email", - dyson_parent.CONF_PASSWORD: "password", - dyson_parent.CONF_LANGUAGE: "GB", - dyson_parent.CONF_DEVICES: [ + DYSON_DOMAIN: { + CONF_USERNAME: "email", + CONF_PASSWORD: "password", + CONF_LANGUAGE: "GB", + CONF_DEVICES: [ {"device_id": "XX-XXXXX-XX", "device_ip": "192.168.0.1"}, {"device_id": "YY-YYYYY-YY", "device_ip": "192.168.0.2"}, ], @@ -89,15 +105,6 @@ def _get_dyson_purehotcool_device(): return device -def _get_device_with_no_state(): - """Return a device with no state.""" - device = Mock(spec=DysonPureHotCoolLink) - load_mock_device(device) - device.state = None - device.environmental_state = None - return device - - def _get_device_off(): """Return a device with state off.""" device = Mock(spec=DysonPureHotCoolLink) @@ -105,22 +112,6 @@ def _get_device_off(): return device -def _get_device_focus(): - """Return a device with fan state of focus mode.""" - device = Mock(spec=DysonPureHotCoolLink) - load_mock_device(device) - device.state.focus_mode = FocusMode.FOCUS_ON.value - return device - - -def _get_device_diffuse(): - """Return a device with fan state of diffuse mode.""" - device = Mock(spec=DysonPureHotCoolLink) - load_mock_device(device) - device.state.focus_mode = FocusMode.FOCUS_OFF.value - return device - - def _get_device_cool(): """Return a device with state of cooling.""" device = Mock(spec=DysonPureHotCoolLink) @@ -132,15 +123,6 @@ def _get_device_cool(): return device -def _get_device_heat_off(): - """Return a device with state of heat reached target.""" - device = Mock(spec=DysonPureHotCoolLink) - load_mock_device(device) - device.state.heat_mode = HeatMode.HEAT_ON.value - device.state.heat_state = HeatState.HEAT_STATE_OFF.value - return device - - def _get_device_heat_on(): """Return a device with state of heating.""" device = Mock(spec=DysonPureHotCoolLink) @@ -154,236 +136,259 @@ def _get_device_heat_on(): return device -class DysonTest(unittest.TestCase): - """Dyson Climate component test class.""" +@pytest.fixture(autouse=True) +def patch_platforms_fixture(): + """Only set up the climate platform for the climate tests.""" + with patch("homeassistant.components.dyson.DYSON_PLATFORMS", new=[DOMAIN]): + yield - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.addCleanup(self.tear_down_cleanup) - def tear_down_cleanup(self): - """Stop everything that was started.""" - self.hass.stop() +@patch( + "homeassistant.components.dyson.DysonAccount.devices", + return_value=[_get_device_heat_on()], +) +@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) +async def test_pure_hot_cool_link_set_mode(mocked_login, mocked_devices, hass): + """Test set climate mode.""" + await async_setup_component(hass, DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() - def test_setup_component_without_devices(self): - """Test setup component with no devices.""" - self.hass.data[dyson.DYSON_DEVICES] = [] - add_devices = MagicMock() - dyson.setup_platform(self.hass, None, add_devices) - add_devices.assert_not_called() + device = mocked_devices.return_value[0] - def test_setup_component_with_devices(self): - """Test setup component with valid devices.""" - devices = [ - _get_device_with_no_state(), - _get_device_off(), - _get_device_heat_on(), - ] - self.hass.data[dyson.DYSON_DEVICES] = devices - add_devices = MagicMock() - dyson.setup_platform(self.hass, None, add_devices, discovery_info={}) - assert add_devices.called + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.temp_name", ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + True, + ) - def test_setup_component(self): - """Test setup component with devices.""" - device_fan = _get_device_heat_on() - device_non_fan = _get_device_off() + set_config = device.set_configuration + assert set_config.call_args == call(heat_mode=HeatMode.HEAT_ON) - def _add_device(devices): - assert len(devices) == 1 - assert devices[0].name == "Device_name" + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.temp_name", ATTR_HVAC_MODE: HVAC_MODE_COOL}, + True, + ) - self.hass.data[dyson.DYSON_DEVICES] = [device_fan, device_non_fan] - dyson.setup_platform(self.hass, None, _add_device) + set_config = device.set_configuration + assert set_config.call_args == call(heat_mode=HeatMode.HEAT_OFF) - def test_dyson_set_temperature(self): - """Test set climate temperature.""" - device = _get_device_heat_on() - device.temp_unit = TEMP_CELSIUS - entity = dyson.DysonPureHotCoolLinkEntity(device) - assert not entity.should_poll - # Without target temp. - kwargs = {} - entity.set_temperature(**kwargs) - set_config = device.set_configuration - set_config.assert_not_called() +@patch( + "homeassistant.components.dyson.DysonAccount.devices", + return_value=[_get_device_heat_on()], +) +@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) +async def test_pure_hot_cool_link_set_fan(mocked_login, mocked_devices, hass): + """Test set climate fan.""" + await async_setup_component(hass, DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() - kwargs = {ATTR_TEMPERATURE: 23} - entity.set_temperature(**kwargs) - set_config = device.set_configuration - set_config.assert_called_with( - heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(23) - ) + device = mocked_devices.return_value[0] + device.temp_unit = TEMP_CELSIUS - # Should clip the target temperature between 1 and 37 inclusive. - kwargs = {ATTR_TEMPERATURE: 50} - entity.set_temperature(**kwargs) - set_config = device.set_configuration - set_config.assert_called_with( - heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(37) - ) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: "climate.temp_name", ATTR_FAN_MODE: FAN_FOCUS}, + True, + ) - kwargs = {ATTR_TEMPERATURE: -5} - entity.set_temperature(**kwargs) - set_config = device.set_configuration - set_config.assert_called_with( - heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(1) - ) + set_config = device.set_configuration + assert set_config.call_args == call(focus_mode=FocusMode.FOCUS_ON) - def test_dyson_set_temperature_when_cooling_mode(self): - """Test set climate temperature when heating is off.""" - device = _get_device_cool() - device.temp_unit = TEMP_CELSIUS - entity = dyson.DysonPureHotCoolLinkEntity(device) - entity.schedule_update_ha_state = Mock() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: "climate.temp_name", ATTR_FAN_MODE: FAN_DIFFUSE}, + True, + ) - kwargs = {ATTR_TEMPERATURE: 23} - entity.set_temperature(**kwargs) - set_config = device.set_configuration - set_config.assert_called_with( - heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(23) - ) + set_config = device.set_configuration + assert set_config.call_args == call(focus_mode=FocusMode.FOCUS_OFF) - def test_dyson_set_fan_mode(self): - """Test set fan mode.""" - device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkEntity(device) - assert not entity.should_poll - entity.set_fan_mode(dyson.FAN_FOCUS) - set_config = device.set_configuration - set_config.assert_called_with(focus_mode=FocusMode.FOCUS_ON) +@patch( + "homeassistant.components.dyson.DysonAccount.devices", + return_value=[_get_device_heat_on()], +) +@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) +async def test_pure_hot_cool_link_state(mocked_login, mocked_devices, hass): + """Test set climate temperature.""" + await async_setup_component(hass, DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() - entity.set_fan_mode(dyson.FAN_DIFFUSE) - set_config = device.set_configuration - set_config.assert_called_with(focus_mode=FocusMode.FOCUS_OFF) + state = hass.states.get("climate.temp_name") + assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_FLAGS + assert state.attributes[ATTR_TEMPERATURE] == 23 + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 289 - 273 + assert state.attributes[ATTR_CURRENT_HUMIDITY] == 53 + assert state.state == HVAC_MODE_HEAT + assert len(state.attributes[ATTR_HVAC_MODES]) == 2 + assert HVAC_MODE_HEAT in state.attributes[ATTR_HVAC_MODES] + assert HVAC_MODE_COOL in state.attributes[ATTR_HVAC_MODES] + assert len(state.attributes[ATTR_FAN_MODES]) == 2 + assert FAN_FOCUS in state.attributes[ATTR_FAN_MODES] + assert FAN_DIFFUSE in state.attributes[ATTR_FAN_MODES] - def test_dyson_fan_modes(self): - """Test get fan list.""" - device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkEntity(device) - assert len(entity.fan_modes) == 2 - assert dyson.FAN_FOCUS in entity.fan_modes - assert dyson.FAN_DIFFUSE in entity.fan_modes + device = mocked_devices.return_value[0] + update_callback = device.add_message_listener.call_args[0][0] - def test_dyson_fan_mode_focus(self): - """Test fan focus mode.""" - device = _get_device_focus() - entity = dyson.DysonPureHotCoolLinkEntity(device) - assert entity.fan_mode == dyson.FAN_FOCUS + device.state.focus_mode = FocusMode.FOCUS_ON.value + await hass.async_add_executor_job(update_callback, MockDysonState()) + await hass.async_block_till_done() - def test_dyson_fan_mode_diffuse(self): - """Test fan diffuse mode.""" - device = _get_device_diffuse() - entity = dyson.DysonPureHotCoolLinkEntity(device) - assert entity.fan_mode == dyson.FAN_DIFFUSE + state = hass.states.get("climate.temp_name") + assert state.attributes[ATTR_FAN_MODE] == FAN_FOCUS - def test_dyson_set_hvac_mode(self): - """Test set operation mode.""" - device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkEntity(device) - assert not entity.should_poll + device.state.focus_mode = FocusMode.FOCUS_OFF.value + await hass.async_add_executor_job(update_callback, MockDysonState()) + await hass.async_block_till_done() - entity.set_hvac_mode(dyson.HVAC_MODE_HEAT) - set_config = device.set_configuration - set_config.assert_called_with(heat_mode=HeatMode.HEAT_ON) + state = hass.states.get("climate.temp_name") + assert state.attributes[ATTR_FAN_MODE] == FAN_DIFFUSE - entity.set_hvac_mode(dyson.HVAC_MODE_COOL) - set_config = device.set_configuration - set_config.assert_called_with(heat_mode=HeatMode.HEAT_OFF) + device.state.heat_mode = HeatMode.HEAT_ON.value + device.state.heat_state = HeatState.HEAT_STATE_OFF.value + await hass.async_add_executor_job(update_callback, MockDysonState()) + await hass.async_block_till_done() - def test_dyson_operation_list(self): - """Test get operation list.""" - device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkEntity(device) - assert len(entity.hvac_modes) == 2 - assert dyson.HVAC_MODE_HEAT in entity.hvac_modes - assert dyson.HVAC_MODE_COOL in entity.hvac_modes + state = hass.states.get("climate.temp_name") + assert state.state == HVAC_MODE_HEAT + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE - def test_dyson_heat_off(self): - """Test turn off heat.""" - device = _get_device_heat_off() - entity = dyson.DysonPureHotCoolLinkEntity(device) - entity.set_hvac_mode(dyson.HVAC_MODE_COOL) - set_config = device.set_configuration - set_config.assert_called_with(heat_mode=HeatMode.HEAT_OFF) + device.environmental_state.humidity = 0 + await hass.async_add_executor_job(update_callback, MockDysonState()) + await hass.async_block_till_done() - def test_dyson_heat_on(self): - """Test turn on heat.""" - device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkEntity(device) - entity.set_hvac_mode(dyson.HVAC_MODE_HEAT) - set_config = device.set_configuration - set_config.assert_called_with(heat_mode=HeatMode.HEAT_ON) + state = hass.states.get("climate.temp_name") + assert state.attributes.get(ATTR_CURRENT_HUMIDITY) is None - def test_dyson_heat_value_on(self): - """Test get heat value on.""" - device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkEntity(device) - assert entity.hvac_mode == dyson.HVAC_MODE_HEAT + device.environmental_state = None + await hass.async_add_executor_job(update_callback, MockDysonState()) + await hass.async_block_till_done() - def test_dyson_heat_value_off(self): - """Test get heat value off.""" - device = _get_device_cool() - entity = dyson.DysonPureHotCoolLinkEntity(device) - assert entity.hvac_mode == dyson.HVAC_MODE_COOL + state = hass.states.get("climate.temp_name") + assert state.attributes.get(ATTR_CURRENT_HUMIDITY) is None - def test_dyson_heat_value_idle(self): - """Test get heat value idle.""" - device = _get_device_heat_off() - entity = dyson.DysonPureHotCoolLinkEntity(device) - assert entity.hvac_mode == dyson.HVAC_MODE_HEAT - assert entity.hvac_action == dyson.CURRENT_HVAC_IDLE + device.state.heat_mode = HeatMode.HEAT_OFF.value + device.state.heat_state = HeatState.HEAT_STATE_OFF.value + await hass.async_add_executor_job(update_callback, MockDysonState()) + await hass.async_block_till_done() - def test_on_message(self): - """Test when message is received.""" - device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkEntity(device) - entity.schedule_update_ha_state = Mock() - entity.on_message(MockDysonState()) - entity.schedule_update_ha_state.assert_called_with() + state = hass.states.get("climate.temp_name") + assert state.state == HVAC_MODE_COOL + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL - def test_general_properties(self): - """Test properties of entity.""" - device = _get_device_with_no_state() - entity = dyson.DysonPureHotCoolLinkEntity(device) - assert entity.should_poll is False - assert entity.supported_features == dyson.SUPPORT_FLAGS - assert entity.temperature_unit == TEMP_CELSIUS - def test_property_current_humidity(self): - """Test properties of current humidity.""" - device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkEntity(device) - assert entity.current_humidity == 53 +@patch( + "homeassistant.components.dyson.DysonAccount.devices", + return_value=[], +) +@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) +async def test_setup_component_without_devices(mocked_login, mocked_devices, hass): + """Test setup component with no devices.""" + await async_setup_component(hass, DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() - def test_property_current_humidity_with_invalid_env_state(self): - """Test properties of current humidity with invalid env state.""" - device = _get_device_off() - device.environmental_state.humidity = 0 - entity = dyson.DysonPureHotCoolLinkEntity(device) - assert entity.current_humidity is None + entity_ids = hass.states.async_entity_ids(DOMAIN) + assert not entity_ids - def test_property_current_humidity_without_env_state(self): - """Test properties of current humidity without env state.""" - device = _get_device_with_no_state() - entity = dyson.DysonPureHotCoolLinkEntity(device) - assert entity.current_humidity is None - def test_property_current_temperature(self): - """Test properties of current temperature.""" - device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkEntity(device) - # Result should be in celsius, hence then subtraction of 273. - assert entity.current_temperature == 289 - 273 +@patch( + "homeassistant.components.dyson.DysonAccount.devices", + return_value=[_get_device_heat_on()], +) +@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) +async def test_dyson_set_temperature(mocked_login, mocked_devices, hass): + """Test set climate temperature.""" + await async_setup_component(hass, DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() - def test_property_target_temperature(self): - """Test properties of target temperature.""" - device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkEntity(device) - assert entity.target_temperature == 23 + device = mocked_devices.return_value[0] + device.temp_unit = TEMP_CELSIUS + + # Without correct target temp. + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "climate.temp_name", + ATTR_TARGET_TEMP_HIGH: 25.0, + ATTR_TARGET_TEMP_LOW: 15.0, + }, + True, + ) + + set_config = device.set_configuration + assert set_config.call_count == 0 + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "climate.temp_name", ATTR_TEMPERATURE: 23}, + True, + ) + + set_config = device.set_configuration + assert set_config.call_args == call( + heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(23) + ) + + # Should clip the target temperature between 1 and 37 inclusive. + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "climate.temp_name", ATTR_TEMPERATURE: 50}, + True, + ) + + set_config = device.set_configuration + assert set_config.call_args == call( + heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(37) + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "climate.temp_name", ATTR_TEMPERATURE: -5}, + True, + ) + + set_config = device.set_configuration + assert set_config.call_args == call( + heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(1) + ) + + +@patch( + "homeassistant.components.dyson.DysonAccount.devices", + return_value=[_get_device_cool()], +) +@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) +async def test_dyson_set_temperature_when_cooling_mode( + mocked_login, mocked_devices, hass +): + """Test set climate temperature when heating is off.""" + await async_setup_component(hass, DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() + + device = mocked_devices.return_value[0] + device.temp_unit = TEMP_CELSIUS + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "climate.temp_name", ATTR_TEMPERATURE: 23}, + True, + ) + + set_config = device.set_configuration + assert set_config.call_args == call( + heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(23) + ) @patch( @@ -395,10 +400,10 @@ async def test_setup_component_with_parent_discovery( mocked_login, mocked_devices, hass ): """Test setup_component using discovery.""" - await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await async_setup_component(hass, DYSON_DOMAIN, _get_config()) await hass.async_block_till_done() - entity_ids = hass.states.async_entity_ids("climate") + entity_ids = hass.states.async_entity_ids(DOMAIN) assert len(entity_ids) == 2 @@ -410,10 +415,10 @@ async def test_setup_component_with_parent_discovery( async def test_purehotcool_component_setup_only_once(devices, login, hass): """Test if entities are created only once.""" config = _get_config() - await async_setup_component(hass, dyson_parent.DOMAIN, config) + await async_setup_component(hass, DYSON_DOMAIN, config) await hass.async_block_till_done() - entity_ids = hass.states.async_entity_ids("climate") + entity_ids = hass.states.async_entity_ids(DOMAIN) assert len(entity_ids) == 1 state = hass.states.get(entity_ids[0]) assert state.name == "Living room" @@ -427,10 +432,10 @@ async def test_purehotcool_component_setup_only_once(devices, login, hass): async def test_purehotcoollink_component_setup_only_once(devices, login, hass): """Test if entities are created only once.""" config = _get_config() - await async_setup_component(hass, dyson_parent.DOMAIN, config) + await async_setup_component(hass, DYSON_DOMAIN, config) await hass.async_block_till_done() - entity_ids = hass.states.async_entity_ids("climate") + entity_ids = hass.states.async_entity_ids(DOMAIN) assert len(entity_ids) == 1 state = hass.states.get(entity_ids[0]) assert state.name == "Temp Name" @@ -444,7 +449,7 @@ async def test_purehotcoollink_component_setup_only_once(devices, login, hass): async def test_purehotcool_update_state(devices, login, hass): """Test state update.""" device = devices.return_value[0] - await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await async_setup_component(hass, DYSON_DOMAIN, _get_config()) await hass.async_block_till_done() event = { "msg": "CURRENT-STATE", @@ -476,12 +481,9 @@ async def test_purehotcool_update_state(devices, login, hass): }, } device.state = DysonPureHotCoolV2State(json.dumps(event)) + update_callback = device.add_message_listener.call_args[0][0] - for call in device.add_message_listener.call_args_list: - callback = call[0][0] - if type(callback.__self__) == dyson.DysonPureHotCoolEntity: - callback(device.state) - + await hass.async_add_executor_job(update_callback, device.state) await hass.async_block_till_done() state = hass.states.get("climate.living_room") attributes = state.attributes @@ -500,7 +502,7 @@ async def test_purehotcool_empty_env_attributes(devices, login, hass): device = devices.return_value[0] device.environmental_state.temperature = 0 device.environmental_state.humidity = None - await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await async_setup_component(hass, DYSON_DOMAIN, _get_config()) await hass.async_block_till_done() state = hass.states.get("climate.living_room") @@ -518,7 +520,7 @@ async def test_purehotcool_fan_state_off(devices, login, hass): """Test device fan state off.""" device = devices.return_value[0] device.state.fan_state = FanState.FAN_OFF.value - await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await async_setup_component(hass, DYSON_DOMAIN, _get_config()) await hass.async_block_till_done() state = hass.states.get("climate.living_room") @@ -536,7 +538,7 @@ async def test_purehotcool_hvac_action_cool(devices, login, hass): """Test device HVAC action cool.""" device = devices.return_value[0] device.state.fan_power = FanPower.POWER_ON.value - await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await async_setup_component(hass, DYSON_DOMAIN, _get_config()) await hass.async_block_till_done() state = hass.states.get("climate.living_room") @@ -555,7 +557,7 @@ async def test_purehotcool_hvac_action_idle(devices, login, hass): device = devices.return_value[0] device.state.fan_power = FanPower.POWER_ON.value device.state.heat_mode = HeatMode.HEAT_ON.value - await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await async_setup_component(hass, DYSON_DOMAIN, _get_config()) await hass.async_block_till_done() state = hass.states.get("climate.living_room") @@ -572,12 +574,12 @@ async def test_purehotcool_hvac_action_idle(devices, login, hass): async def test_purehotcool_set_temperature(devices, login, hass): """Test set temperature.""" device = devices.return_value[0] - await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await async_setup_component(hass, DYSON_DOMAIN, _get_config()) await hass.async_block_till_done() state = hass.states.get("climate.living_room") attributes = state.attributes - min_temp = attributes["min_temp"] - max_temp = attributes["max_temp"] + min_temp = attributes[ATTR_MIN_TEMP] + max_temp = attributes[ATTR_MAX_TEMP] await hass.services.async_call( DOMAIN, @@ -623,7 +625,7 @@ async def test_purehotcool_set_temperature(devices, login, hass): async def test_purehotcool_set_fan_mode(devices, login, hass): """Test set fan mode.""" device = devices.return_value[0] - await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await async_setup_component(hass, DYSON_DOMAIN, _get_config()) await hass.async_block_till_done() await hass.services.async_call( @@ -687,7 +689,7 @@ async def test_purehotcool_set_fan_mode(devices, login, hass): async def test_purehotcool_set_hvac_mode(devices, login, hass): """Test set HVAC mode.""" device = devices.return_value[0] - await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await async_setup_component(hass, DYSON_DOMAIN, _get_config()) await hass.async_block_till_done() await hass.services.async_call( From 90f5b178ef732bd3d27db03de9cd0c85e4df61ee Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 15 Sep 2020 23:00:26 +0200 Subject: [PATCH 185/514] Use AREA_SQUARE_METERS constant in all integrations (#40107) --- homeassistant/components/ambient_station/__init__.py | 8 +++++++- homeassistant/components/bloomsky/sensor.py | 5 +++-- homeassistant/components/isy994/const.py | 3 ++- homeassistant/components/smartthings/sensor.py | 8 +++++++- homeassistant/components/zamg/sensor.py | 8 +++++++- 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index f8b2cd7348d..ac8ae18a657 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( + AREA_SQUARE_METERS, ATTR_LOCATION, ATTR_NAME, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, @@ -206,7 +207,12 @@ SENSOR_TYPES = { TYPE_SOILTEMP7F: ("Soil Temp 7", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), TYPE_SOILTEMP8F: ("Soil Temp 8", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), TYPE_SOILTEMP9F: ("Soil Temp 9", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_SOLARRADIATION: ("Solar Rad", f"{POWER_WATT}/m^2", TYPE_SENSOR, None), + TYPE_SOLARRADIATION: ( + "Solar Rad", + f"{POWER_WATT}/{AREA_SQUARE_METERS}", + TYPE_SENSOR, + None, + ), TYPE_SOLARRADIATION_LX: ("Solar Rad (lx)", "lx", TYPE_SENSOR, "illuminance"), TYPE_TEMP10F: ("Temp 10", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), TYPE_TEMP1F: ("Temp 1", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index 0ddeec6a577..812efe7697e 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + AREA_SQUARE_METERS, CONF_MONITORED_CONDITIONS, PERCENTAGE, TEMP_CELSIUS, @@ -32,7 +33,7 @@ SENSOR_UNITS_IMPERIAL = { "Temperature": TEMP_FAHRENHEIT, "Humidity": PERCENTAGE, "Pressure": "inHg", - "Luminance": "cd/m²", + "Luminance": f"cd/{AREA_SQUARE_METERS}", "Voltage": "mV", } @@ -41,7 +42,7 @@ SENSOR_UNITS_METRIC = { "Temperature": TEMP_CELSIUS, "Humidity": PERCENTAGE, "Pressure": "mbar", - "Luminance": "cd/m²", + "Luminance": f"cd/{AREA_SQUARE_METERS}", "Voltage": "mV", } diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 4fb32d3843d..3e19dda1ea8 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -44,6 +44,7 @@ from homeassistant.components.lock import DOMAIN as LOCK from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.const import ( + AREA_SQUARE_METERS, CONCENTRATION_PARTS_PER_MILLION, DEGREE, ENERGY_KILO_WATT_HOUR, @@ -379,7 +380,7 @@ UOM_FRIENDLY_NAME = { "71": UV_INDEX, "72": VOLT, "73": POWER_WATT, - "74": f"{POWER_WATT}/{LENGTH_METERS}²", + "74": f"{POWER_WATT}/{AREA_SQUARE_METERS}", "75": "weekday", "76": DEGREE, "77": TIME_YEARS, diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 7de3b98b1da..a7a15e3cefc 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -5,6 +5,7 @@ from typing import Optional, Sequence from pysmartthings import Attribute, Capability from homeassistant.const import ( + AREA_SQUARE_METERS, CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, @@ -41,7 +42,12 @@ CAPABILITY_TO_SENSORS = { 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) + Map( + Attribute.bmi_measurement, + "Body Mass Index", + f"{MASS_KILOGRAMS}/{AREA_SQUARE_METERS}", + None, + ) ], Capability.body_weight_measurement: [ Map(Attribute.body_weight_measurement, "Body Weight", MASS_KILOGRAMS, None) diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 0aa5dea0687..9372be58493 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -12,6 +12,7 @@ import requests import voluptuous as vol from homeassistant.const import ( + AREA_SQUARE_METERS, ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, @@ -60,7 +61,12 @@ SENSOR_TYPES = { "wind_max_bearing": ("Top Wind Bearing", DEGREE, f"WSR {DEGREE}", 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), + "precipitation": ( + "Precipitation", + f"l/{AREA_SQUARE_METERS}", + f"N l/{AREA_SQUARE_METERS}", + float, + ), "dewpoint": ("Dew Point", TEMP_CELSIUS, f"TP {TEMP_CELSIUS}", float), # The following probably not useful for general consumption, # but we need them to fill in internal attributes From 000d2047fb660026c4f05d1c895b147754258798 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 15 Sep 2020 23:01:01 +0200 Subject: [PATCH 186/514] Add and use currency constants (#40113) --- .../components/dsmr_reader/definitions.py | 21 ++++++++++--------- .../components/growatt_server/sensor.py | 5 +++-- homeassistant/components/isy994/const.py | 3 ++- .../components/pvpc_hourly_pricing/sensor.py | 4 ++-- .../components/tankerkoenig/sensor.py | 9 ++++++-- homeassistant/const.py | 4 ++++ .../pvpc_hourly_pricing/conftest.py | 11 ++++++++-- 7 files changed, 38 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 0ec67bc97fd..5fda67e65a3 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -1,6 +1,7 @@ """Definitions for DSMR Reader sensors added to MQTT.""" from homeassistant.const import ( + CURRENCY_EURO, ELECTRICAL_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, VOLT, @@ -166,17 +167,17 @@ DEFINITIONS = { "dsmr/day-consumption/electricity1_cost": { "name": "Low tariff cost", "icon": "mdi:currency-eur", - "unit": "€", + "unit": CURRENCY_EURO, }, "dsmr/day-consumption/electricity2_cost": { "name": "High tariff cost", "icon": "mdi:currency-eur", - "unit": "€", + "unit": CURRENCY_EURO, }, "dsmr/day-consumption/electricity_cost_merged": { "name": "Power total cost", "icon": "mdi:currency-eur", - "unit": "€", + "unit": CURRENCY_EURO, }, "dsmr/day-consumption/gas": { "name": "Gas usage", @@ -186,37 +187,37 @@ DEFINITIONS = { "dsmr/day-consumption/gas_cost": { "name": "Gas cost", "icon": "mdi:currency-eur", - "unit": "€", + "unit": CURRENCY_EURO, }, "dsmr/day-consumption/total_cost": { "name": "Total cost", "icon": "mdi:currency-eur", - "unit": "€", + "unit": CURRENCY_EURO, }, "dsmr/day-consumption/energy_supplier_price_electricity_delivered_1": { "name": "Low tariff delivered price", "icon": "mdi:currency-eur", - "unit": "€", + "unit": CURRENCY_EURO, }, "dsmr/day-consumption/energy_supplier_price_electricity_delivered_2": { "name": "High tariff delivered price", "icon": "mdi:currency-eur", - "unit": "€", + "unit": CURRENCY_EURO, }, "dsmr/day-consumption/energy_supplier_price_electricity_returned_1": { "name": "Low tariff returned price", "icon": "mdi:currency-eur", - "unit": "€", + "unit": CURRENCY_EURO, }, "dsmr/day-consumption/energy_supplier_price_electricity_returned_2": { "name": "High tariff returned price", "icon": "mdi:currency-eur", - "unit": "€", + "unit": CURRENCY_EURO, }, "dsmr/day-consumption/energy_supplier_price_gas": { "name": "Gas price", "icon": "mdi:currency-eur", - "unit": "€", + "unit": CURRENCY_EURO, }, "dsmr/meter-stats/dsmr_version": { "name": "DSMR version", diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 366596beb0b..384b35a0aff 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_USERNAME, + CURRENCY_EURO, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, @@ -39,8 +40,8 @@ SCAN_INTERVAL = datetime.timedelta(minutes=5) # Sensor type order is: Sensor name, Unit of measurement, api data name, additional options TOTAL_SENSOR_TYPES = { - "total_money_today": ("Total money today", "€", "plantMoneyText", {}), - "total_money_total": ("Money lifetime", "€", "totalMoneyText", {}), + "total_money_today": ("Total money today", CURRENCY_EURO, "plantMoneyText", {}), + "total_money_total": ("Money lifetime", CURRENCY_EURO, "totalMoneyText", {}), "total_energy_today": ( "Energy Today", ENERGY_KILO_WATT_HOUR, diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 3e19dda1ea8..375f4e4aa73 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -46,6 +46,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.const import ( AREA_SQUARE_METERS, CONCENTRATION_PARTS_PER_MILLION, + CURRENCY_DOLLAR, DEGREE, ENERGY_KILO_WATT_HOUR, FREQUENCY_HERTZ, @@ -397,7 +398,7 @@ UOM_FRIENDLY_NAME = { UOM_8_BIT_RANGE: "", # Range 0-255, no unit. UOM_DOUBLE_TEMP: UOM_DOUBLE_TEMP, "102": "kWs", - "103": "$", + "103": CURRENCY_DOLLAR, "104": "¢", "105": LENGTH_INCHES, "106": f"mm/{TIME_DAYS}", diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index 25b4531cee0..a9b53c970bd 100644 --- a/homeassistant/components/pvpc_hourly_pricing/sensor.py +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -6,7 +6,7 @@ from typing import Optional from aiopvpc import PVPCData from homeassistant import config_entries -from homeassistant.const import CONF_NAME, ENERGY_KILO_WATT_HOUR +from homeassistant.const import CONF_NAME, CURRENCY_EURO, ENERGY_KILO_WATT_HOUR from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_call_later, async_track_time_change @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_PRICE = "price" ICON = "mdi:currency-eur" -UNIT = f"€/{ENERGY_KILO_WATT_HOUR}" +UNIT = f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" _DEFAULT_TIMEOUT = 10 diff --git a/homeassistant/components/tankerkoenig/sensor.py b/homeassistant/components/tankerkoenig/sensor.py index b0dd3368ad3..6985072b065 100644 --- a/homeassistant/components/tankerkoenig/sensor.py +++ b/homeassistant/components/tankerkoenig/sensor.py @@ -2,7 +2,12 @@ import logging -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_LATITUDE, + ATTR_LONGITUDE, + CURRENCY_EURO, +) from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -106,7 +111,7 @@ class FuelPriceSensor(CoordinatorEntity): @property def unit_of_measurement(self): """Return unit of measurement.""" - return "€" + return CURRENCY_EURO @property def state(self): diff --git a/homeassistant/const.py b/homeassistant/const.py index 22fde9f21dd..9444009cbc9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -380,6 +380,10 @@ ELECTRICAL_VOLT_AMPERE = f"{VOLT}{ELECTRICAL_CURRENT_AMPERE}" # Degree units DEGREE = "°" +# Currency units +CURRENCY_EURO = "€" +CURRENCY_DOLLAR = "$" + # Temperature units TEMP_CELSIUS = f"{DEGREE}C" TEMP_FAHRENHEIT = f"{DEGREE}F" diff --git a/tests/components/pvpc_hourly_pricing/conftest.py b/tests/components/pvpc_hourly_pricing/conftest.py index 681b3675aa6..a16923d0a73 100644 --- a/tests/components/pvpc_hourly_pricing/conftest.py +++ b/tests/components/pvpc_hourly_pricing/conftest.py @@ -2,7 +2,11 @@ import pytest from homeassistant.components.pvpc_hourly_pricing import ATTR_TARIFF, DOMAIN -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, ENERGY_KILO_WATT_HOUR +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + CURRENCY_EURO, + ENERGY_KILO_WATT_HOUR, +) from tests.common import load_fixture from tests.test_util.aiohttp import AiohttpClientMocker @@ -15,7 +19,10 @@ FIXTURE_JSON_DATA_2019_10_29 = "PVPC_CURV_DD_2019_10_29.json" def check_valid_state(state, tariff: str, value=None, key_attr=None): """Ensure that sensor has a valid state and attributes.""" assert state - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == f"€/{ENERGY_KILO_WATT_HOUR}" + assert ( + state.attributes[ATTR_UNIT_OF_MEASUREMENT] + == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" + ) try: _ = float(state.state) # safety margins for current electricity price (it shouldn't be out of [0, 0.2]) From 66bb6a6ffa2847a1f519d60203b6ca25cf26e9a5 Mon Sep 17 00:00:00 2001 From: AJ Schmidt Date: Tue, 15 Sep 2020 18:11:29 -0400 Subject: [PATCH 187/514] AlarmDecoder config flow fixes (#40037) --- .../components/alarmdecoder/__init__.py | 13 +++++------ .../alarmdecoder/alarm_control_panel.py | 23 +++++-------------- .../components/alarmdecoder/binary_sensor.py | 9 ++++---- .../components/alarmdecoder/config_flow.py | 10 +++++--- .../components/alarmdecoder/strings.json | 2 +- .../alarmdecoder/translations/en.json | 4 ++-- .../alarmdecoder/test_config_flow.py | 16 +++++++++---- 7 files changed, 37 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index b69b60b82c4..8dd704f1333 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -59,13 +59,13 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = False controller.close() - def open_connection(now=None): + async def open_connection(now=None): """Open a connection to AlarmDecoder.""" try: - controller.open(baud) + await hass.async_add_executor_job(controller.open, baud) except NoDeviceError: _LOGGER.debug("Failed to connect. Retrying in 5 seconds") - hass.helpers.event.track_point_in_time( + hass.helpers.event.async_track_point_in_time( open_connection, dt_util.utcnow() + timedelta(seconds=5) ) return @@ -100,8 +100,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool """Handle relay or zone expander message from AlarmDecoder.""" hass.helpers.dispatcher.dispatcher_send(SIGNAL_REL_MESSAGE, message) - controller = False - baud = ad_connection[CONF_DEVICE_BAUD] + baud = ad_connection.get(CONF_DEVICE_BAUD) if protocol == PROTOCOL_SOCKET: host = ad_connection[CONF_HOST] port = ad_connection[CONF_PORT] @@ -129,7 +128,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool DATA_RESTART: False, } - open_connection() + await open_connection() for component in PLATFORMS: hass.async_create_task( @@ -156,7 +155,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id][DATA_REMOVE_UPDATE_LISTENER]() hass.data[DOMAIN][entry.entry_id][DATA_REMOVE_STOP_LISTENER]() - hass.data[DOMAIN][entry.entry_id][DATA_AD].close() + await hass.async_add_executor_job(hass.data[DOMAIN][entry.entry_id][DATA_AD].close) if hass.data[DOMAIN][entry.entry_id]: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 83288c93c67..bc2d74a5042 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -15,7 +15,6 @@ from homeassistant.components.alarm_control_panel.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CODE, - ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, @@ -40,21 +39,9 @@ from .const import ( _LOGGER = logging.getLogger(__name__) SERVICE_ALARM_TOGGLE_CHIME = "alarm_toggle_chime" -ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID, default=[]): cv.entity_ids, - vol.Required(ATTR_CODE): cv.string, - } -) SERVICE_ALARM_KEYPRESS = "alarm_keypress" ATTR_KEYPRESS = "keypress" -ALARM_KEYPRESS_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID, default=[]): cv.entity_ids, - vol.Required(ATTR_KEYPRESS): cv.string, - } -) async def async_setup_entry( @@ -77,18 +64,20 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_ALARM_TOGGLE_CHIME, - ALARM_TOGGLE_CHIME_SCHEMA, + { + vol.Required(ATTR_CODE): cv.string, + }, "alarm_toggle_chime", ) platform.async_register_entity_service( SERVICE_ALARM_KEYPRESS, - ALARM_KEYPRESS_SCHEMA, + { + vol.Required(ATTR_KEYPRESS): cv.string, + }, "alarm_keypress", ) - return True - class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): """Representation of an AlarmDecoder-based alarm panel.""" diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index 417dfd6f96a..89b7d00b3a4 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -40,7 +40,7 @@ async def async_setup_entry( zones = entry.options.get(OPTIONS_ZONES, DEFAULT_ZONE_OPTIONS) - devices = [] + entities = [] for zone_num in zones: zone_info = zones[zone_num] zone_type = zone_info[CONF_ZONE_TYPE] @@ -49,13 +49,12 @@ async def async_setup_entry( zone_loop = zone_info.get(CONF_ZONE_LOOP) relay_addr = zone_info.get(CONF_RELAY_ADDR) relay_chan = zone_info.get(CONF_RELAY_CHAN) - device = AlarmDecoderBinarySensor( + entity = AlarmDecoderBinarySensor( zone_num, zone_name, zone_type, zone_rfid, zone_loop, relay_addr, relay_chan ) - devices.append(device) + entities.append(entity) - async_add_entities(devices) - return True + async_add_entities(entities) class AlarmDecoderBinarySensor(BinarySensorEntity): diff --git a/homeassistant/components/alarmdecoder/config_flow.py b/homeassistant/components/alarmdecoder/config_flow.py index 1f6f049fcb3..74b23f049a7 100644 --- a/homeassistant/components/alarmdecoder/config_flow.py +++ b/homeassistant/components/alarmdecoder/config_flow.py @@ -87,8 +87,8 @@ class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ): return self.async_abort(reason="already_configured") connection = {} + baud = None if self.protocol == PROTOCOL_SOCKET: - baud = connection[CONF_DEVICE_BAUD] = None host = connection[CONF_HOST] = user_input[CONF_HOST] port = connection[CONF_PORT] = user_input[CONF_PORT] title = f"{host}:{port}" @@ -100,9 +100,13 @@ class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): device = SerialDevice(interface=path) controller = AdExt(device) + + def test_connection(): + controller.open(baud) + controller.close() + try: - with controller: - controller.open(baudrate=baud) + await self.hass.async_add_executor_job(test_connection) return self.async_create_entry( title=title, data={CONF_PROTOCOL: self.protocol, **connection} ) diff --git a/homeassistant/components/alarmdecoder/strings.json b/homeassistant/components/alarmdecoder/strings.json index 73e4cc760f2..ed250b92b98 100644 --- a/homeassistant/components/alarmdecoder/strings.json +++ b/homeassistant/components/alarmdecoder/strings.json @@ -22,7 +22,7 @@ }, "create_entry": { "default": "Successfully connected to AlarmDecoder." }, "abort": { - "already_configured": "AlarmDecoder device is already configured." + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, "options": { diff --git a/homeassistant/components/alarmdecoder/translations/en.json b/homeassistant/components/alarmdecoder/translations/en.json index 756e1f1479e..55a4ad99d1c 100644 --- a/homeassistant/components/alarmdecoder/translations/en.json +++ b/homeassistant/components/alarmdecoder/translations/en.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "AlarmDecoder device is already configured." + "already_configured": "Device is already configured" }, "create_entry": { "default": "Successfully connected to AlarmDecoder." @@ -71,4 +71,4 @@ } } } -} \ No newline at end of file +} diff --git a/tests/components/alarmdecoder/test_config_flow.py b/tests/components/alarmdecoder/test_config_flow.py index dd7091fd0ef..64f4a604ff3 100644 --- a/tests/components/alarmdecoder/test_config_flow.py +++ b/tests/components/alarmdecoder/test_config_flow.py @@ -34,7 +34,7 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize( - "protocol,connection,baud,title", + "protocol,connection,title", [ ( PROTOCOL_SOCKET, @@ -42,7 +42,6 @@ from tests.common import MockConfigEntry CONF_HOST: "alarmdecoder123", CONF_PORT: 10001, }, - None, "alarmdecoder123:10001", ), ( @@ -51,12 +50,11 @@ from tests.common import MockConfigEntry CONF_DEVICE_PATH: "/dev/ttyUSB123", CONF_DEVICE_BAUD: 115000, }, - 115000, "/dev/ttyUSB123", ), ], ) -async def test_setups(hass: HomeAssistant, protocol, connection, baud, title): +async def test_setups(hass: HomeAssistant, protocol, connection, title): """Test flow for setting up the available AlarmDecoder protocols.""" result = await hass.config_entries.flow.async_init( @@ -90,7 +88,6 @@ async def test_setups(hass: HomeAssistant, protocol, connection, baud, title): assert result["data"] == { **connection, CONF_PROTOCOL: protocol, - CONF_DEVICE_BAUD: baud, } await hass.async_block_till_done() @@ -142,6 +139,9 @@ async def test_options_arm_flow(hass: HomeAssistant): entry = MockConfigEntry(domain=DOMAIN) entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(entry.entry_id) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -177,6 +177,9 @@ async def test_options_zone_flow(hass: HomeAssistant): entry = MockConfigEntry(domain=DOMAIN) entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(entry.entry_id) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -250,6 +253,9 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): entry = MockConfigEntry(domain=DOMAIN) entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(entry.entry_id) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM From 413263a6eb2c8671e6d1f378169def9feefe53a4 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 16 Sep 2020 00:12:59 +0200 Subject: [PATCH 188/514] 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 38ccc9e0f74..eebf53dd69c 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 1c9f2309c62..0f1304c076b 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 ce3a61562ed..193d39ee4b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -134,7 +134,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 f28b7f21871c840f3c512853b676d75f724a5076 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 16 Sep 2020 00:09:01 +0000 Subject: [PATCH 189/514] [ci skip] Translation update --- .../components/agent_dvr/translations/pl.json | 2 +- .../components/airvisual/translations/pl.json | 2 +- .../alarmdecoder/translations/cs.json | 35 +++++++++++++++++++ .../alarmdecoder/translations/en.json | 2 +- .../alarmdecoder/translations/pl.json | 13 +++++++ .../components/arcam_fmj/translations/pl.json | 2 +- .../components/august/translations/pl.json | 6 ++-- .../components/awair/translations/pl.json | 4 +-- .../components/axis/translations/pl.json | 4 +-- .../azure_devops/translations/pl.json | 8 +++++ .../components/blink/translations/pl.json | 8 +++-- .../components/bond/translations/pl.json | 12 +++++-- .../components/bsblan/translations/pl.json | 2 +- .../components/control4/translations/pl.json | 6 ++-- .../components/daikin/translations/pl.json | 6 ++-- .../components/deconz/translations/pl.json | 2 +- .../components/denonavr/translations/pl.json | 2 +- .../components/dexcom/translations/pl.json | 6 ++-- .../components/directv/translations/pl.json | 4 +-- .../components/doorbird/translations/pl.json | 4 +-- .../components/dsmr/translations/pl.json | 2 +- .../components/dunehd/translations/pl.json | 4 +-- .../components/eafm/translations/pl.json | 7 ++++ .../components/elgato/translations/pl.json | 2 +- .../components/elkm1/translations/pl.json | 4 +-- .../flick_electric/translations/pl.json | 6 ++-- .../components/flo/translations/pl.json | 21 +++++++++++ .../components/flume/translations/pl.json | 6 ++-- .../flunearyou/translations/pl.json | 2 +- .../garmin_connect/translations/pl.json | 6 ++-- .../components/gogogate2/translations/pl.json | 2 +- .../components/griddy/translations/pl.json | 2 +- .../components/guardian/translations/pl.json | 2 +- .../components/hangouts/translations/pl.json | 2 +- .../components/harmony/translations/pl.json | 4 +-- .../components/hlk_sw16/translations/pl.json | 18 ++++++++++ .../homematicip_cloud/translations/pl.json | 2 +- .../components/hue/translations/pl.json | 6 ++-- .../translations/pl.json | 4 +-- .../hvv_departures/translations/pl.json | 4 +-- .../components/insteon/translations/nl.json | 13 +++++++ .../components/ipp/translations/pl.json | 2 +- .../components/isy994/translations/pl.json | 6 ++-- .../components/juicenet/translations/pl.json | 6 ++-- .../components/kodi/translations/pl.json | 10 +++--- .../components/konnected/translations/nl.json | 5 +++ .../components/konnected/translations/pl.json | 4 +-- .../components/life360/translations/pl.json | 4 +-- .../components/melcloud/translations/pl.json | 4 +-- .../components/metoffice/translations/pl.json | 4 +-- .../components/mill/translations/pl.json | 2 +- .../components/monoprice/translations/pl.json | 4 +-- .../components/mqtt/translations/nl.json | 3 +- .../components/myq/translations/pl.json | 4 +-- .../components/neato/translations/pl.json | 2 +- .../components/netatmo/translations/pl.json | 3 +- .../components/nexia/translations/pl.json | 4 +-- .../nightscout/translations/pl.json | 10 ++++++ .../components/nuheat/translations/pl.json | 4 +-- .../components/nut/translations/pl.json | 4 +-- .../components/nws/translations/pl.json | 4 +-- .../components/nzbget/translations/pl.json | 7 +++- .../components/onvif/translations/pl.json | 2 +- .../ovo_energy/translations/pl.json | 9 +++++ .../components/plugwise/translations/pl.json | 2 +- .../components/point/translations/pl.json | 2 +- .../components/poolsense/translations/pl.json | 4 +-- .../components/powerwall/translations/pl.json | 2 +- .../progettihwsw/translations/pl.json | 6 +++- .../components/rachio/translations/pl.json | 6 ++-- .../components/rfxtrx/translations/pl.json | 7 ++++ .../components/ring/translations/pl.json | 6 ++-- .../components/risco/translations/pl.json | 6 ++-- .../components/roku/translations/pl.json | 4 +-- .../components/roon/translations/pl.json | 11 ++++++ .../components/rpi_power/translations/cs.json | 14 ++++++++ .../components/rpi_power/translations/no.json | 14 ++++++++ .../rpi_power/translations/zh-Hant.json | 14 ++++++++ .../components/sense/translations/pl.json | 6 ++-- .../components/sentry/translations/pl.json | 2 +- .../components/sharkiq/translations/pl.json | 7 ++-- .../components/shelly/translations/pl.json | 6 ++-- .../components/smappee/translations/pl.json | 6 ++++ .../smart_meter_texas/translations/pl.json | 6 ++-- .../components/smarthab/translations/pl.json | 4 +-- .../components/sms/translations/pl.json | 4 +-- .../components/solarlog/translations/pl.json | 4 +-- .../components/sonarr/translations/pl.json | 4 +-- .../components/songpal/translations/pl.json | 2 +- .../components/spider/translations/pl.json | 19 ++++++++++ .../squeezebox/translations/pl.json | 6 ++-- .../components/syncthru/translations/pl.json | 2 +- .../components/tado/translations/pl.json | 6 ++-- .../tellduslive/translations/pl.json | 2 +- .../components/tibber/translations/pl.json | 4 +-- .../components/tuya/translations/pl.json | 4 +-- .../components/unifi/translations/nl.json | 2 ++ .../components/unifi/translations/pl.json | 2 +- .../components/upb/translations/pl.json | 2 +- .../components/vilfo/translations/pl.json | 2 +- .../components/vizio/translations/pl.json | 2 +- .../components/volumio/translations/pl.json | 19 ++++++++++ .../components/wilight/translations/pl.json | 2 +- .../components/wled/translations/nl.json | 15 +++++++- .../components/wled/translations/pl.json | 2 +- .../components/wolflink/translations/pl.json | 4 +-- .../xiaomi_aqara/translations/pl.json | 6 ++-- .../xiaomi_miio/translations/pl.json | 2 +- .../components/yeelight/translations/nl.json | 11 +++++- .../components/yeelight/translations/pl.json | 2 +- 110 files changed, 456 insertions(+), 165 deletions(-) create mode 100644 homeassistant/components/alarmdecoder/translations/cs.json create mode 100644 homeassistant/components/azure_devops/translations/pl.json create mode 100644 homeassistant/components/eafm/translations/pl.json create mode 100644 homeassistant/components/flo/translations/pl.json create mode 100644 homeassistant/components/hlk_sw16/translations/pl.json create mode 100644 homeassistant/components/insteon/translations/nl.json create mode 100644 homeassistant/components/nightscout/translations/pl.json create mode 100644 homeassistant/components/rfxtrx/translations/pl.json create mode 100644 homeassistant/components/roon/translations/pl.json create mode 100644 homeassistant/components/rpi_power/translations/cs.json create mode 100644 homeassistant/components/rpi_power/translations/no.json create mode 100644 homeassistant/components/rpi_power/translations/zh-Hant.json create mode 100644 homeassistant/components/spider/translations/pl.json create mode 100644 homeassistant/components/volumio/translations/pl.json diff --git a/homeassistant/components/agent_dvr/translations/pl.json b/homeassistant/components/agent_dvr/translations/pl.json index 5045015087f..9c101555f78 100644 --- a/homeassistant/components/agent_dvr/translations/pl.json +++ b/homeassistant/components/agent_dvr/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { "already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.", diff --git a/homeassistant/components/airvisual/translations/pl.json b/homeassistant/components/airvisual/translations/pl.json index dea77a233aa..5851afceabd 100644 --- a/homeassistant/components/airvisual/translations/pl.json +++ b/homeassistant/components/airvisual/translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "Ten klucz API jest ju\u017c w u\u017cyciu." }, "error": { - "general_error": "Nieoczekiwany b\u0142\u0105d.", + "general_error": "Nieoczekiwany b\u0142\u0105d", "invalid_api_key": "Nieprawid\u0142owy klucz API.", "unable_to_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z jednostk\u0105 Node/Pro." }, diff --git a/homeassistant/components/alarmdecoder/translations/cs.json b/homeassistant/components/alarmdecoder/translations/cs.json new file mode 100644 index 00000000000..b42e092bb47 --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/cs.json @@ -0,0 +1,35 @@ +{ + "options": { + "step": { + "arm_settings": { + "title": "Konfigurovat AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Upravit" + }, + "description": "Co chcete upravit?", + "title": "Konfigurovat AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF Loop", + "zone_name": "N\u00e1zev z\u00f3ny", + "zone_relayaddr": "Relay adresa", + "zone_relaychan": "Relay kan\u00e1l", + "zone_rfid": "RF Serial", + "zone_type": "Typ z\u00f3ny" + }, + "description": "Zadejte podrobnosti pro z\u00f3nu {zone_number}. Chcete-li odstranit z\u00f3nu {zone_number}, ponechejte n\u00e1zev z\u00f3ny pr\u00e1zdn\u00fd.", + "title": "Konfigurovat AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "\u010c\u00edslo z\u00f3ny" + }, + "description": "Zadejte \u010d\u00edslo z\u00f3ny, kterou chcete p\u0159idat, upravit nebo odstranit.", + "title": "Konfigurovat AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/en.json b/homeassistant/components/alarmdecoder/translations/en.json index 55a4ad99d1c..66301414cc8 100644 --- a/homeassistant/components/alarmdecoder/translations/en.json +++ b/homeassistant/components/alarmdecoder/translations/en.json @@ -71,4 +71,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/pl.json b/homeassistant/components/alarmdecoder/translations/pl.json index e1bb88d7309..5b6b3dec7a6 100644 --- a/homeassistant/components/alarmdecoder/translations/pl.json +++ b/homeassistant/components/alarmdecoder/translations/pl.json @@ -1,4 +1,17 @@ { + "config": { + "error": { + "service_unavailable": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "protocol": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + } + } + } + }, "options": { "step": { "zone_details": { diff --git a/homeassistant/components/arcam_fmj/translations/pl.json b/homeassistant/components/arcam_fmj/translations/pl.json index 7b2d5da76e5..42ed803a161 100644 --- a/homeassistant/components/arcam_fmj/translations/pl.json +++ b/homeassistant/components/arcam_fmj/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "step": { "user": { diff --git a/homeassistant/components/august/translations/pl.json b/homeassistant/components/august/translations/pl.json index eeaa5269da4..0c38508399b 100644 --- a/homeassistant/components/august/translations/pl.json +++ b/homeassistant/components/august/translations/pl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane." + "already_configured": "Konto jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/awair/translations/pl.json b/homeassistant/components/awair/translations/pl.json index 07983402c42..76fe5a91cd9 100644 --- a/homeassistant/components/awair/translations/pl.json +++ b/homeassistant/components/awair/translations/pl.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane.", + "already_configured": "Konto jest ju\u017c skonfigurowane", "no_devices": "Nie znaleziono urz\u0105dze\u0144 w sieci.", - "reauth_successful": "Token dost\u0119pu pomy\u015blnie zaktualizowano." + "reauth_successful": "Token dost\u0119pu pomy\u015blnie zaktualizowano" }, "error": { "auth": "Token dost\u0119pu" diff --git a/homeassistant/components/axis/translations/pl.json b/homeassistant/components/axis/translations/pl.json index e6d55f5a579..154577573bb 100644 --- a/homeassistant/components/axis/translations/pl.json +++ b/homeassistant/components/axis/translations/pl.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "bad_config_file": "B\u0142\u0119dne dane z pliku konfiguracyjnego", "link_local_address": "Po\u0142\u0105czenie lokalnego adresu nie jest obs\u0142ugiwane", "not_axis_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem Axis" }, "error": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.", "device_unavailable": "Urz\u0105dzenie jest niedost\u0119pne", "faulty_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce" diff --git a/homeassistant/components/azure_devops/translations/pl.json b/homeassistant/components/azure_devops/translations/pl.json new file mode 100644 index 00000000000..93ec5bd3949 --- /dev/null +++ b/homeassistant/components/azure_devops/translations/pl.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Token dost\u0119pu pomy\u015blnie zaktualizowany" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/pl.json b/homeassistant/components/blink/translations/pl.json index 7d6d01266d9..1564ed2d685 100644 --- a/homeassistant/components/blink/translations/pl.json +++ b/homeassistant/components/blink/translations/pl.json @@ -1,11 +1,13 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_access_token": "Niepoprawny token dost\u0119pu", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "2fa": { diff --git a/homeassistant/components/bond/translations/pl.json b/homeassistant/components/bond/translations/pl.json index 7bf55561e03..4fae986f701 100644 --- a/homeassistant/components/bond/translations/pl.json +++ b/homeassistant/components/bond/translations/pl.json @@ -1,11 +1,19 @@ { "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." + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "confirm": { + "data": { + "access_token": "Token dost\u0119pu" + } + }, "user": { "data": { "access_token": "Token dost\u0119pu", diff --git a/homeassistant/components/bsblan/translations/pl.json b/homeassistant/components/bsblan/translations/pl.json index 21b42085761..d8a30744659 100644 --- a/homeassistant/components/bsblan/translations/pl.json +++ b/homeassistant/components/bsblan/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem BSB_LAN." diff --git a/homeassistant/components/control4/translations/pl.json b/homeassistant/components/control4/translations/pl.json index ae068e1b538..0076cfb69dd 100644 --- a/homeassistant/components/control4/translations/pl.json +++ b/homeassistant/components/control4/translations/pl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "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", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/daikin/translations/pl.json b/homeassistant/components/daikin/translations/pl.json index e990e3b76a3..7b40fa55a1d 100644 --- a/homeassistant/components/daikin/translations/pl.json +++ b/homeassistant/components/daikin/translations/pl.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "error": { - "device_fail": "Nieoczekiwany b\u0142\u0105d.", + "device_fail": "Nieoczekiwany b\u0142\u0105d", "device_timeout": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "forbidden": "Niepoprawne uwierzytelnienie." + "forbidden": "Niepoprawne uwierzytelnienie" }, "step": { "user": { diff --git a/homeassistant/components/deconz/translations/pl.json b/homeassistant/components/deconz/translations/pl.json index 27dbc0535ce..2fa9549585a 100644 --- a/homeassistant/components/deconz/translations/pl.json +++ b/homeassistant/components/deconz/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Mostek jest ju\u017c skonfigurowany.", + "already_configured": "Mostek jest ju\u017c skonfigurowany", "already_in_progress": "Konfiguracja mostka jest ju\u017c w toku.", "no_bridges": "Nie odkryto mostk\u00f3w deCONZ", "not_deconz_bridge": "To nie jest mostek deCONZ", diff --git a/homeassistant/components/denonavr/translations/pl.json b/homeassistant/components/denonavr/translations/pl.json index f025f6e2dd4..eb396842bf0 100644 --- a/homeassistant/components/denonavr/translations/pl.json +++ b/homeassistant/components/denonavr/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.", "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.", diff --git a/homeassistant/components/dexcom/translations/pl.json b/homeassistant/components/dexcom/translations/pl.json index f71ac6fb618..acc27dc93f4 100644 --- a/homeassistant/components/dexcom/translations/pl.json +++ b/homeassistant/components/dexcom/translations/pl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured_account": "Konto jest ju\u017c skonfigurowane." + "already_configured_account": "Konto jest ju\u017c skonfigurowane" }, "error": { - "account_error": "Niepoprawne uwierzytelnienie.", + "account_error": "Niepoprawne uwierzytelnienie", "session_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/directv/translations/pl.json b/homeassistant/components/directv/translations/pl.json index 6c7a1dda53a..db0dc7ea0a4 100644 --- a/homeassistant/components/directv/translations/pl.json +++ b/homeassistant/components/directv/translations/pl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" diff --git a/homeassistant/components/doorbird/translations/pl.json b/homeassistant/components/doorbird/translations/pl.json index 463ac347b5d..446fd21626a 100644 --- a/homeassistant/components/doorbird/translations/pl.json +++ b/homeassistant/components/doorbird/translations/pl.json @@ -7,8 +7,8 @@ }, "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", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "flow_title": "DoorBird {name} ({host})", "step": { diff --git a/homeassistant/components/dsmr/translations/pl.json b/homeassistant/components/dsmr/translations/pl.json index 815a6f19706..637a81a3f87 100644 --- a/homeassistant/components/dsmr/translations/pl.json +++ b/homeassistant/components/dsmr/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" } } } \ No newline at end of file diff --git a/homeassistant/components/dunehd/translations/pl.json b/homeassistant/components/dunehd/translations/pl.json index f6f760d6a3e..893a71d4b70 100644 --- a/homeassistant/components/dunehd/translations/pl.json +++ b/homeassistant/components/dunehd/translations/pl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_host": "Nieprawid\u0142owa nazwa hosta lub adres IP." }, diff --git a/homeassistant/components/eafm/translations/pl.json b/homeassistant/components/eafm/translations/pl.json new file mode 100644 index 00000000000..637a81a3f87 --- /dev/null +++ b/homeassistant/components/eafm/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/elgato/translations/pl.json b/homeassistant/components/elgato/translations/pl.json index 263c67a67ca..7088ede092a 100644 --- a/homeassistant/components/elgato/translations/pl.json +++ b/homeassistant/components/elgato/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem Elgato Key Light." }, "error": { diff --git a/homeassistant/components/elkm1/translations/pl.json b/homeassistant/components/elkm1/translations/pl.json index b38c9aaa6d2..c7f21554dee 100644 --- a/homeassistant/components/elkm1/translations/pl.json +++ b/homeassistant/components/elkm1/translations/pl.json @@ -6,8 +6,8 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/flick_electric/translations/pl.json b/homeassistant/components/flick_electric/translations/pl.json index fb6554d00d8..19c319a366b 100644 --- a/homeassistant/components/flick_electric/translations/pl.json +++ b/homeassistant/components/flick_electric/translations/pl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane." + "already_configured": "Konto jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/flo/translations/pl.json b/homeassistant/components/flo/translations/pl.json new file mode 100644 index 00000000000..25dab56796c --- /dev/null +++ b/homeassistant/components/flo/translations/pl.json @@ -0,0 +1,21 @@ +{ + "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": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/pl.json b/homeassistant/components/flume/translations/pl.json index ff1d73fe0ce..f899b1446a6 100644 --- a/homeassistant/components/flume/translations/pl.json +++ b/homeassistant/components/flume/translations/pl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane." + "already_configured": "Konto jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/flunearyou/translations/pl.json b/homeassistant/components/flunearyou/translations/pl.json index de344b82d00..cde9f39c3f9 100644 --- a/homeassistant/components/flunearyou/translations/pl.json +++ b/homeassistant/components/flunearyou/translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "Wsp\u00f3\u0142rz\u0119dne s\u0105 ju\u017c zarejestrowane." }, "error": { - "general_error": "Nieoczekiwany b\u0142\u0105d." + "general_error": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/garmin_connect/translations/pl.json b/homeassistant/components/garmin_connect/translations/pl.json index 982c7b2c50b..5aaa67a913e 100644 --- a/homeassistant/components/garmin_connect/translations/pl.json +++ b/homeassistant/components/garmin_connect/translations/pl.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane." + "already_configured": "Konto jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", + "invalid_auth": "Niepoprawne uwierzytelnienie", "too_many_requests": "Zbyt wiele \u017c\u0105da\u0144, spr\u00f3buj ponownie p\u00f3\u017aniej.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/gogogate2/translations/pl.json b/homeassistant/components/gogogate2/translations/pl.json index 41df279799f..2ea10491255 100644 --- a/homeassistant/components/gogogate2/translations/pl.json +++ b/homeassistant/components/gogogate2/translations/pl.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_auth": "Niepoprawne uwierzytelnienie." + "invalid_auth": "Niepoprawne uwierzytelnienie" }, "step": { "user": { diff --git a/homeassistant/components/griddy/translations/pl.json b/homeassistant/components/griddy/translations/pl.json index e28b4b6f2e6..4fe11d213e8 100644 --- a/homeassistant/components/griddy/translations/pl.json +++ b/homeassistant/components/griddy/translations/pl.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/guardian/translations/pl.json b/homeassistant/components/guardian/translations/pl.json index 22706a1babc..9c918c69796 100644 --- a/homeassistant/components/guardian/translations/pl.json +++ b/homeassistant/components/guardian/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem Guardian, spr\u00f3buj ponownie." }, "step": { diff --git a/homeassistant/components/hangouts/translations/pl.json b/homeassistant/components/hangouts/translations/pl.json index 69c3020bbfb..2dd3364bd53 100644 --- a/homeassistant/components/hangouts/translations/pl.json +++ b/homeassistant/components/hangouts/translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Google Hangouts jest ju\u017c skonfigurowany.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { "invalid_2fa": "Nieprawid\u0142owe uwierzytelnienie dwusk\u0142adnikowe, spr\u00f3buj ponownie.", diff --git a/homeassistant/components/harmony/translations/pl.json b/homeassistant/components/harmony/translations/pl.json index 12bbcfaca18..664a849061a 100644 --- a/homeassistant/components/harmony/translations/pl.json +++ b/homeassistant/components/harmony/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "flow_title": "Logitech Harmony Hub {name}", "step": { diff --git a/homeassistant/components/hlk_sw16/translations/pl.json b/homeassistant/components/hlk_sw16/translations/pl.json new file mode 100644 index 00000000000..063ff23f55c --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/pl.json b/homeassistant/components/homematicip_cloud/translations/pl.json index cfd8e96c2a2..c317bbddd26 100644 --- a/homeassistant/components/homematicip_cloud/translations/pl.json +++ b/homeassistant/components/homematicip_cloud/translations/pl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Punkt dost\u0119pu jest ju\u017c skonfigurowany.", "connection_aborted": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem HMIP", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { "invalid_pin": "Nieprawid\u0142owy kod PIN, spr\u00f3buj ponownie.", diff --git a/homeassistant/components/hue/translations/pl.json b/homeassistant/components/hue/translations/pl.json index 32242752051..6e2623d23ac 100644 --- a/homeassistant/components/hue/translations/pl.json +++ b/homeassistant/components/hue/translations/pl.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane.", - "already_configured": "Mostek jest ju\u017c skonfigurowany.", + "all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane", + "already_configured": "Mostek jest ju\u017c skonfigurowany", "already_in_progress": "Konfiguracja mostka jest ju\u017c w toku.", "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z mostkiem", "discover_timeout": "Nie mo\u017cna wykry\u0107 \u017cadnych mostk\u00f3w Hue", "no_bridges": "Nie wykryto mostk\u00f3w Hue", "not_hue_bridge": "To nie jest mostek Hue", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { "linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.", diff --git a/homeassistant/components/hunterdouglas_powerview/translations/pl.json b/homeassistant/components/hunterdouglas_powerview/translations/pl.json index cad41869ced..87d7b8a915e 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/pl.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "link": { diff --git a/homeassistant/components/hvv_departures/translations/pl.json b/homeassistant/components/hvv_departures/translations/pl.json index e4914cbebc0..7ea22e48d54 100644 --- a/homeassistant/components/hvv_departures/translations/pl.json +++ b/homeassistant/components/hvv_departures/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_auth": "Niepoprawne uwierzytelnienie.", + "invalid_auth": "Niepoprawne uwierzytelnienie", "no_results": "Brak wynik\u00f3w. Spr\u00f3buj z inn\u0105 stacj\u0105/adresem." }, "step": { diff --git a/homeassistant/components/insteon/translations/nl.json b/homeassistant/components/insteon/translations/nl.json new file mode 100644 index 00000000000..7a93eb9f843 --- /dev/null +++ b/homeassistant/components/insteon/translations/nl.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "modem_type": "Modemtype." + }, + "description": "Selecteer het Insteon-modemtype.", + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/pl.json b/homeassistant/components/ipp/translations/pl.json index 3d0e846707e..c5096f6af8e 100644 --- a/homeassistant/components/ipp/translations/pl.json +++ b/homeassistant/components/ipp/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "connection_upgrade": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z drukark\u0105 z powodu konieczno\u015bci uaktualnienia po\u0142\u0105czenia.", "ipp_error": "Wyst\u0105pi\u0142 b\u0142\u0105d IPP.", diff --git a/homeassistant/components/isy994/translations/pl.json b/homeassistant/components/isy994/translations/pl.json index 200a0355462..d35b9e91eb6 100644 --- a/homeassistant/components/isy994/translations/pl.json +++ b/homeassistant/components/isy994/translations/pl.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_auth": "Niepoprawne uwierzytelnienie.", + "invalid_auth": "Niepoprawne uwierzytelnienie", "invalid_host": "Wpis hosta nie by\u0142 w pe\u0142nym formacie URL, np. http://192.168.10.100:80.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "flow_title": "Urz\u0105dzenia uniwersalne ISY994 {name} ({host})", "step": { diff --git a/homeassistant/components/juicenet/translations/pl.json b/homeassistant/components/juicenet/translations/pl.json index 601ce0c9128..c308ad2524a 100644 --- a/homeassistant/components/juicenet/translations/pl.json +++ b/homeassistant/components/juicenet/translations/pl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane." + "already_configured": "Konto jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/kodi/translations/pl.json b/homeassistant/components/kodi/translations/pl.json index 85bc5cc8e05..28dc89056f3 100644 --- a/homeassistant/components/kodi/translations/pl.json +++ b/homeassistant/components/kodi/translations/pl.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "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." + "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." + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "credentials": { diff --git a/homeassistant/components/konnected/translations/nl.json b/homeassistant/components/konnected/translations/nl.json index dcb5f1ed6c4..8e9eba0d134 100644 --- a/homeassistant/components/konnected/translations/nl.json +++ b/homeassistant/components/konnected/translations/nl.json @@ -2,8 +2,13 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom voor het apparaat wordt al uitgevoerd.", + "not_konn_panel": "Geen herkend Konnected.io apparaat", "unknown": "Onbekende fout opgetreden" }, + "error": { + "cannot_connect": "Kan geen verbinding maken met een Konnected Panel op {host} : {port}" + }, "step": { "confirm": { "title": "Konnected Apparaat Klaar" diff --git a/homeassistant/components/konnected/translations/pl.json b/homeassistant/components/konnected/translations/pl.json index 1f9b60bbae3..95b3a9f5d5e 100644 --- a/homeassistant/components/konnected/translations/pl.json +++ b/homeassistant/components/konnected/translations/pl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.", "not_konn_panel": "Nie rozpoznano urz\u0105dzenia Konnected.io.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z panelem Konnected na {host}:{port}" diff --git a/homeassistant/components/life360/translations/pl.json b/homeassistant/components/life360/translations/pl.json index 19a6c6d8828..b0a5785a320 100644 --- a/homeassistant/components/life360/translations/pl.json +++ b/homeassistant/components/life360/translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", - "user_already_configured": "Konto jest ju\u017c skonfigurowane." + "user_already_configured": "Konto jest ju\u017c skonfigurowane" }, "create_entry": { "default": "Aby skonfigurowa\u0107 zaawansowane ustawienia, zapoznaj si\u0119 z [dokumentacj\u0105 Life360]({docs_url})." @@ -11,7 +11,7 @@ "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", "invalid_username": "Nieprawid\u0142owa nazwa u\u017cytkownika", "unexpected": "Nieoczekiwany b\u0142\u0105d komunikacji z serwerem Life360", - "user_already_configured": "Konto jest ju\u017c skonfigurowane." + "user_already_configured": "Konto jest ju\u017c skonfigurowane" }, "step": { "user": { diff --git a/homeassistant/components/melcloud/translations/pl.json b/homeassistant/components/melcloud/translations/pl.json index 44467601826..cd0c961089e 100644 --- a/homeassistant/components/melcloud/translations/pl.json +++ b/homeassistant/components/melcloud/translations/pl.json @@ -5,8 +5,8 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/metoffice/translations/pl.json b/homeassistant/components/metoffice/translations/pl.json index 095a0ff6e29..6b129f2965c 100644 --- a/homeassistant/components/metoffice/translations/pl.json +++ b/homeassistant/components/metoffice/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/mill/translations/pl.json b/homeassistant/components/mill/translations/pl.json index a7d0fb0618e..eaf1da95e9e 100644 --- a/homeassistant/components/mill/translations/pl.json +++ b/homeassistant/components/mill/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane." + "already_configured": "Konto jest ju\u017c skonfigurowane" }, "error": { "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" diff --git a/homeassistant/components/monoprice/translations/pl.json b/homeassistant/components/monoprice/translations/pl.json index b5af0e8851f..020e0f2c554 100644 --- a/homeassistant/components/monoprice/translations/pl.json +++ b/homeassistant/components/monoprice/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index 7953c744f27..36a51c46802 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -34,7 +34,8 @@ "button_4": "Vierde knop", "button_5": "Vijfde knop", "button_6": "Zesde knop", - "turn_off": "Uitschakelen" + "turn_off": "Uitschakelen", + "turn_on": "Inschakelen" } } } \ No newline at end of file diff --git a/homeassistant/components/myq/translations/pl.json b/homeassistant/components/myq/translations/pl.json index aefc8336903..d01f9e588fa 100644 --- a/homeassistant/components/myq/translations/pl.json +++ b/homeassistant/components/myq/translations/pl.json @@ -5,8 +5,8 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/neato/translations/pl.json b/homeassistant/components/neato/translations/pl.json index 80e0a1df48e..821cf79c971 100644 --- a/homeassistant/components/neato/translations/pl.json +++ b/homeassistant/components/neato/translations/pl.json @@ -9,7 +9,7 @@ }, "error": { "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", - "unexpected_error": "Nieoczekiwany b\u0142\u0105d." + "unexpected_error": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/netatmo/translations/pl.json b/homeassistant/components/netatmo/translations/pl.json index 8a549e4cd30..721c3a2a946 100644 --- a/homeassistant/components/netatmo/translations/pl.json +++ b/homeassistant/components/netatmo/translations/pl.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", - "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono" diff --git a/homeassistant/components/nexia/translations/pl.json b/homeassistant/components/nexia/translations/pl.json index 84844ddbd94..7f09569418d 100644 --- a/homeassistant/components/nexia/translations/pl.json +++ b/homeassistant/components/nexia/translations/pl.json @@ -5,8 +5,8 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/nightscout/translations/pl.json b/homeassistant/components/nightscout/translations/pl.json new file mode 100644 index 00000000000..c931afdae8d --- /dev/null +++ b/homeassistant/components/nightscout/translations/pl.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "unknown": "Nieoczekiwany b\u0142\u0105d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/pl.json b/homeassistant/components/nuheat/translations/pl.json index d55b545d040..d992afe9cc0 100644 --- a/homeassistant/components/nuheat/translations/pl.json +++ b/homeassistant/components/nuheat/translations/pl.json @@ -5,9 +5,9 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", + "invalid_auth": "Niepoprawne uwierzytelnienie", "invalid_thermostat": "Numer seryjny termostatu jest nieprawid\u0142owy.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/nut/translations/pl.json b/homeassistant/components/nut/translations/pl.json index 5fb9082d676..d24638341d2 100644 --- a/homeassistant/components/nut/translations/pl.json +++ b/homeassistant/components/nut/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "resources": { diff --git a/homeassistant/components/nws/translations/pl.json b/homeassistant/components/nws/translations/pl.json index ab1011d9d56..2671f0408c9 100644 --- a/homeassistant/components/nws/translations/pl.json +++ b/homeassistant/components/nws/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/nzbget/translations/pl.json b/homeassistant/components/nzbget/translations/pl.json index a5bd1b5cdcb..6b39cde94a5 100644 --- a/homeassistant/components/nzbget/translations/pl.json +++ b/homeassistant/components/nzbget/translations/pl.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, "error": { - "invalid_auth": "Niepoprawne uwierzytelnienie." + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" }, "flow_title": "NZBGet: {name}", "step": { diff --git a/homeassistant/components/onvif/translations/pl.json b/homeassistant/components/onvif/translations/pl.json index d60d45c746f..a2d25de2194 100644 --- a/homeassistant/components/onvif/translations/pl.json +++ b/homeassistant/components/onvif/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Proces konfiguracji dla urz\u0105dzenia ONVIF jest ju\u017c w toku.", "no_h264": "Nie by\u0142o dost\u0119pnych \u017cadnych strumieni H264. Sprawd\u017a konfiguracj\u0119 profilu w swoim urz\u0105dzeniu.", "no_mac": "Nie mo\u017cna utworzy\u0107 unikalnego identyfikatora urz\u0105dzenia ONVIF.", diff --git a/homeassistant/components/ovo_energy/translations/pl.json b/homeassistant/components/ovo_energy/translations/pl.json index a82c7f23ab7..7ab3219f18c 100644 --- a/homeassistant/components/ovo_energy/translations/pl.json +++ b/homeassistant/components/ovo_energy/translations/pl.json @@ -1,7 +1,16 @@ { "config": { "error": { + "already_configured": "Konto jest ju\u017c skonfigurowane", "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/pl.json b/homeassistant/components/plugwise/translations/pl.json index 135b9d838fc..16996161f09 100644 --- a/homeassistant/components/plugwise/translations/pl.json +++ b/homeassistant/components/plugwise/translations/pl.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", "invalid_auth": "Nieudane uwierzytelnienie, sprawd\u017a Smile ID", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "flow_title": "Smile: {name}", "step": { diff --git a/homeassistant/components/point/translations/pl.json b/homeassistant/components/point/translations/pl.json index 1596ba05916..286c9e67fc8 100644 --- a/homeassistant/components/point/translations/pl.json +++ b/homeassistant/components/point/translations/pl.json @@ -12,7 +12,7 @@ }, "error": { "follow_link": "Prosz\u0119 klikn\u0105\u0107 link i uwierzytelni\u0107 przed naci\u015bni\u0119ciem przycisku \"Zatwierd\u017a\"", - "no_token": "Niepoprawny token dost\u0119pu." + "no_token": "Niepoprawny token dost\u0119pu" }, "step": { "auth": { diff --git a/homeassistant/components/poolsense/translations/pl.json b/homeassistant/components/poolsense/translations/pl.json index d463be1c5dd..54f1a80b043 100644 --- a/homeassistant/components/poolsense/translations/pl.json +++ b/homeassistant/components/poolsense/translations/pl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { - "invalid_auth": "Niepoprawne uwierzytelnienie." + "invalid_auth": "Niepoprawne uwierzytelnienie" }, "step": { "user": { diff --git a/homeassistant/components/powerwall/translations/pl.json b/homeassistant/components/powerwall/translations/pl.json index 20eb71e7c27..cc56f1e116d 100644 --- a/homeassistant/components/powerwall/translations/pl.json +++ b/homeassistant/components/powerwall/translations/pl.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "Nieoczekiwany b\u0142\u0105d.", + "unknown": "Nieoczekiwany b\u0142\u0105d", "wrong_version": "Powerwall u\u017cywa wersji oprogramowania, kt\u00f3ra nie jest obs\u0142ugiwana. Rozwa\u017c uaktualnienie lub zg\u0142oszenie tego problemu, aby mo\u017cna go by\u0142o rozwi\u0105za\u0107." }, "step": { diff --git a/homeassistant/components/progettihwsw/translations/pl.json b/homeassistant/components/progettihwsw/translations/pl.json index b18a46d9b3b..cdfea7a9221 100644 --- a/homeassistant/components/progettihwsw/translations/pl.json +++ b/homeassistant/components/progettihwsw/translations/pl.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "relay_modes": { diff --git a/homeassistant/components/rachio/translations/pl.json b/homeassistant/components/rachio/translations/pl.json index e077fea03a4..55e8d18fe1c 100644 --- a/homeassistant/components/rachio/translations/pl.json +++ b/homeassistant/components/rachio/translations/pl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/rfxtrx/translations/pl.json b/homeassistant/components/rfxtrx/translations/pl.json new file mode 100644 index 00000000000..637a81a3f87 --- /dev/null +++ b/homeassistant/components/rfxtrx/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/ring/translations/pl.json b/homeassistant/components/ring/translations/pl.json index 96aa7d39159..b095647d06c 100644 --- a/homeassistant/components/ring/translations/pl.json +++ b/homeassistant/components/ring/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "2fa": { diff --git a/homeassistant/components/risco/translations/pl.json b/homeassistant/components/risco/translations/pl.json index 45add23dc8b..66bc71bc397 100644 --- a/homeassistant/components/risco/translations/pl.json +++ b/homeassistant/components/risco/translations/pl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "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", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/roku/translations/pl.json b/homeassistant/components/roku/translations/pl.json index 98881a1522d..21c969876e7 100644 --- a/homeassistant/components/roku/translations/pl.json +++ b/homeassistant/components/roku/translations/pl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" diff --git a/homeassistant/components/roon/translations/pl.json b/homeassistant/components/roon/translations/pl.json new file mode 100644 index 00000000000..d6cc1c7dd8b --- /dev/null +++ b/homeassistant/components/roon/translations/pl.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/cs.json b/homeassistant/components/rpi_power/translations/cs.json new file mode 100644 index 00000000000..b60cb60f985 --- /dev/null +++ b/homeassistant/components/rpi_power/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nelze naj\u00edt t\u0159\u00eddu syst\u00e9mu pot\u0159ebnou pro tuto komponentu, ujist\u011bte se, \u017ee je va\u0161e j\u00e1dro aktu\u00e1ln\u00ed a hardware podporov\u00e1n", + "single_instance_allowed": "Ji\u017e je nakonfigurov\u00e1no.Je mo\u017en\u00e1 pouze jedna konfigurace." + }, + "step": { + "confirm": { + "description": "Chcete zah\u00e1jit nastaven\u00ed?" + } + } + }, + "title": "Kontrola nap\u00e1jec\u00edho zdroje Raspberry Pi" +} \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/no.json b/homeassistant/components/rpi_power/translations/no.json new file mode 100644 index 00000000000..63f46667e79 --- /dev/null +++ b/homeassistant/components/rpi_power/translations/no.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Finner ikke systemklassen som trengs for denne komponenten, s\u00f8rg for at kjernen din er ny og at maskinvaren st\u00f8ttes", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "step": { + "confirm": { + "description": "Vil du starte oppsettet?" + } + } + }, + "title": "Raspberry Pi str\u00f8mforsyningskontroll" +} \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/zh-Hant.json b/homeassistant/components/rpi_power/translations/zh-Hant.json new file mode 100644 index 00000000000..37dbb151d8e --- /dev/null +++ b/homeassistant/components/rpi_power/translations/zh-Hant.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u627e\u4e0d\u5230\u7cfb\u7d71\u6240\u9700\u7684\u5143\u4ef6\uff0c\u8acb\u78ba\u5b9a Kernel \u70ba\u6700\u65b0\u7248\u672c\u3001\u540c\u6642\u786c\u9ad4\u70ba\u652f\u63f4\u72c0\u614b", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + } + } + }, + "title": "Raspberry Pi \u96fb\u6e90\u4f9b\u61c9\u6aa2\u67e5\u5de5\u5177" +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/pl.json b/homeassistant/components/sense/translations/pl.json index c32b61e30ad..7cf2ebe4709 100644 --- a/homeassistant/components/sense/translations/pl.json +++ b/homeassistant/components/sense/translations/pl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/sentry/translations/pl.json b/homeassistant/components/sentry/translations/pl.json index fa87b8510c7..00206d3e88b 100644 --- a/homeassistant/components/sentry/translations/pl.json +++ b/homeassistant/components/sentry/translations/pl.json @@ -6,7 +6,7 @@ }, "error": { "bad_dsn": "Nieprawid\u0142owy DSN", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/sharkiq/translations/pl.json b/homeassistant/components/sharkiq/translations/pl.json index cb691df13f1..283e2c4f440 100644 --- a/homeassistant/components/sharkiq/translations/pl.json +++ b/homeassistant/components/sharkiq/translations/pl.json @@ -1,12 +1,15 @@ { "config": { "abort": { + "already_configured_account": "Konto jest ju\u017c skonfigurowane", "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." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { - "invalid_auth": "Niepoprawne uwierzytelnienie." + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "reauth": { diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json index f0d63cb340e..858ac850385 100644 --- a/homeassistant/components/shelly/translations/pl.json +++ b/homeassistant/components/shelly/translations/pl.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "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", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "flow_title": "Shelly: {name}", "step": { diff --git a/homeassistant/components/smappee/translations/pl.json b/homeassistant/components/smappee/translations/pl.json index da5a481c22b..82d13f6f5e5 100644 --- a/homeassistant/components/smappee/translations/pl.json +++ b/homeassistant/components/smappee/translations/pl.json @@ -1,10 +1,16 @@ { "config": { "abort": { + "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." }, "step": { + "local": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + }, "pick_implementation": { "title": "Wybierz metod\u0119 uwierzytelniania" } diff --git a/homeassistant/components/smart_meter_texas/translations/pl.json b/homeassistant/components/smart_meter_texas/translations/pl.json index f1bb80d064f..8a08a06c699 100644 --- a/homeassistant/components/smart_meter_texas/translations/pl.json +++ b/homeassistant/components/smart_meter_texas/translations/pl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "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", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/smarthab/translations/pl.json b/homeassistant/components/smarthab/translations/pl.json index 7279eb6ca79..3d366edbf73 100644 --- a/homeassistant/components/smarthab/translations/pl.json +++ b/homeassistant/components/smarthab/translations/pl.json @@ -2,8 +2,8 @@ "config": { "error": { "service": "B\u0142\u0105d podczas pr\u00f3by osi\u0105gni\u0119cia SmartHab. Us\u0142uga mo\u017ce by\u0107 wy\u0142\u0105czna. Sprawd\u017a po\u0142\u0105czenie.", - "unknown_error": "Nieoczekiwany b\u0142\u0105d.", - "wrong_login": "Niepoprawne uwierzytelnienie." + "unknown_error": "Nieoczekiwany b\u0142\u0105d", + "wrong_login": "Niepoprawne uwierzytelnienie" }, "step": { "user": { diff --git a/homeassistant/components/sms/translations/pl.json b/homeassistant/components/sms/translations/pl.json index 0637e5c9d79..bfe331ee89e 100644 --- a/homeassistant/components/sms/translations/pl.json +++ b/homeassistant/components/sms/translations/pl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/solarlog/translations/pl.json b/homeassistant/components/solarlog/translations/pl.json index 6769d51c2c2..1577982d3d7 100644 --- a/homeassistant/components/solarlog/translations/pl.json +++ b/homeassistant/components/solarlog/translations/pl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, sprawd\u017a adres hosta" }, "step": { diff --git a/homeassistant/components/sonarr/translations/pl.json b/homeassistant/components/sonarr/translations/pl.json index 841f132ec5e..f2c56d3e540 100644 --- a/homeassistant/components/sonarr/translations/pl.json +++ b/homeassistant/components/sonarr/translations/pl.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_auth": "Niepoprawne uwierzytelnienie." + "invalid_auth": "Niepoprawne uwierzytelnienie" }, "flow_title": "Sonarr: {name}", "step": { diff --git a/homeassistant/components/songpal/translations/pl.json b/homeassistant/components/songpal/translations/pl.json index 7f811aa1621..6b5d29e06b3 100644 --- a/homeassistant/components/songpal/translations/pl.json +++ b/homeassistant/components/songpal/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "not_songpal_device": "To nie jest urz\u0105dzenie Songpal." }, "error": { diff --git a/homeassistant/components/spider/translations/pl.json b/homeassistant/components/spider/translations/pl.json new file mode 100644 index 00000000000..4b9e07cb01f --- /dev/null +++ b/homeassistant/components/spider/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "error": { + "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/squeezebox/translations/pl.json b/homeassistant/components/squeezebox/translations/pl.json index fac6adf40a8..4bf27bbb3ec 100644 --- a/homeassistant/components/squeezebox/translations/pl.json +++ b/homeassistant/components/squeezebox/translations/pl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "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", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "edit": { diff --git a/homeassistant/components/syncthru/translations/pl.json b/homeassistant/components/syncthru/translations/pl.json index bd174d000c8..2c092f86003 100644 --- a/homeassistant/components/syncthru/translations/pl.json +++ b/homeassistant/components/syncthru/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { "invalid_url": "Nieprawid\u0142owy URL", diff --git a/homeassistant/components/tado/translations/pl.json b/homeassistant/components/tado/translations/pl.json index f95374e0329..46c78ce438e 100644 --- a/homeassistant/components/tado/translations/pl.json +++ b/homeassistant/components/tado/translations/pl.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", + "invalid_auth": "Niepoprawne uwierzytelnienie", "no_homes": "Brak dom\u00f3w powi\u0105zanych z tym kontem Tado.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/tellduslive/translations/pl.json b/homeassistant/components/tellduslive/translations/pl.json index 8145717b40e..13054967365 100644 --- a/homeassistant/components/tellduslive/translations/pl.json +++ b/homeassistant/components/tellduslive/translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "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." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { "auth_error": "B\u0142\u0105d uwierzytelniania, spr\u00f3buj ponownie" diff --git a/homeassistant/components/tibber/translations/pl.json b/homeassistant/components/tibber/translations/pl.json index 8ef96358301..d69572b4a42 100644 --- a/homeassistant/components/tibber/translations/pl.json +++ b/homeassistant/components/tibber/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane." + "already_configured": "Konto jest ju\u017c skonfigurowane" }, "error": { "connection_error": "B\u0142\u0105d po\u0142\u0105czenia z Tibber.", - "invalid_access_token": "Niepoprawny token dost\u0119pu.", + "invalid_access_token": "Niepoprawny token dost\u0119pu", "timeout": "Przekroczono limit czasu \u0142\u0105czenia z Tibber." }, "step": { diff --git a/homeassistant/components/tuya/translations/pl.json b/homeassistant/components/tuya/translations/pl.json index 134050394a2..d306172fab0 100644 --- a/homeassistant/components/tuya/translations/pl.json +++ b/homeassistant/components/tuya/translations/pl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "auth_failed": "Niepoprawne uwierzytelnienie.", + "auth_failed": "Niepoprawne uwierzytelnienie", "conn_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "error": { - "auth_failed": "Niepoprawne uwierzytelnienie." + "auth_failed": "Niepoprawne uwierzytelnienie" }, "flow_title": "Konfiguracja integracji Tuya", "step": { diff --git a/homeassistant/components/unifi/translations/nl.json b/homeassistant/components/unifi/translations/nl.json index 5d64d73d1de..37a6148d377 100644 --- a/homeassistant/components/unifi/translations/nl.json +++ b/homeassistant/components/unifi/translations/nl.json @@ -36,6 +36,7 @@ "data": { "detection_time": "Tijd in seconden vanaf laatst gezien tot beschouwd als weg", "ignore_wired_bug": "Schakel UniFi bedrade buglogica uit", + "ssid_filter": "Selecteer SSID's om draadloze clients op te volgen", "track_clients": "Volg netwerkclients", "track_devices": "Netwerkapparaten volgen (Ubiquiti-apparaten)", "track_wired_clients": "Inclusief bedrade netwerkcli\u00ebnten" @@ -59,6 +60,7 @@ "data": { "allow_bandwidth_sensors": "Maak bandbreedtegebruiksensoren voor netwerkclients" }, + "description": "Configureer statistische sensoren", "title": "UniFi-opties 3/3" } } diff --git a/homeassistant/components/unifi/translations/pl.json b/homeassistant/components/unifi/translations/pl.json index 85a89e2eea9..4aecd4502fa 100644 --- a/homeassistant/components/unifi/translations/pl.json +++ b/homeassistant/components/unifi/translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "Witryna kontrolera jest ju\u017c skonfigurowana." }, "error": { - "faulty_credentials": "Niepoprawne uwierzytelnienie.", + "faulty_credentials": "Niepoprawne uwierzytelnienie", "service_unavailable": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown_client_mac": "Brak klienta z tym adresem MAC" }, diff --git a/homeassistant/components/upb/translations/pl.json b/homeassistant/components/upb/translations/pl.json index fcc6fb8bead..18b1b3a8c78 100644 --- a/homeassistant/components/upb/translations/pl.json +++ b/homeassistant/components/upb/translations/pl.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z UPB PIM, spr\u00f3buj ponownie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/vilfo/translations/pl.json b/homeassistant/components/vilfo/translations/pl.json index 29a8a056361..fb439da9bfe 100644 --- a/homeassistant/components/vilfo/translations/pl.json +++ b/homeassistant/components/vilfo/translations/pl.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia. Sprawd\u017a wprowadzone dane i spr\u00f3buj ponownie.", "invalid_auth": "Nieudane uwierzytelnienie. Sprawd\u017a token dost\u0119pu i spr\u00f3buj ponownie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/vizio/translations/pl.json b/homeassistant/components/vizio/translations/pl.json index 07b094b8d01..e5bcf0875a3 100644 --- a/homeassistant/components/vizio/translations/pl.json +++ b/homeassistant/components/vizio/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "updated_entry": "Ten wpis zosta\u0142 ju\u017c skonfigurowany, ale nazwa i/lub opcje zdefiniowane w konfiguracji nie pasuj\u0105 do wcze\u015bniej zaimportowanych warto\u015bci, wi\u0119c wpis konfiguracji zosta\u0142 odpowiednio zaktualizowany." }, "error": { diff --git a/homeassistant/components/volumio/translations/pl.json b/homeassistant/components/volumio/translations/pl.json new file mode 100644 index 00000000000..1708694e123 --- /dev/null +++ b/homeassistant/components/volumio/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/pl.json b/homeassistant/components/wilight/translations/pl.json index 815a6f19706..637a81a3f87 100644 --- a/homeassistant/components/wilight/translations/pl.json +++ b/homeassistant/components/wilight/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" } } } \ No newline at end of file diff --git a/homeassistant/components/wled/translations/nl.json b/homeassistant/components/wled/translations/nl.json index 794f68256a7..ebbc0e52cde 100644 --- a/homeassistant/components/wled/translations/nl.json +++ b/homeassistant/components/wled/translations/nl.json @@ -1,10 +1,23 @@ { "config": { + "abort": { + "already_configured": "Dit WLED-apparaat is al geconfigureerd.", + "connection_error": "Kan geen verbinding maken met WLED-apparaat." + }, + "error": { + "connection_error": "Kan geen verbinding maken met WLED-apparaat." + }, + "flow_title": "WLED: {name}", "step": { "user": { "data": { "host": "Hostnaam of IP-adres" - } + }, + "description": "Stel uw WLED-integratie in met Home Assistant." + }, + "zeroconf_confirm": { + "description": "Wil je de WLED genaamd `{name}` toevoegen aan Home Assistant?", + "title": "Ontdekt WLED-apparaat" } } } diff --git a/homeassistant/components/wled/translations/pl.json b/homeassistant/components/wled/translations/pl.json index ad33cc1ca40..608fbe65ef2 100644 --- a/homeassistant/components/wled/translations/pl.json +++ b/homeassistant/components/wled/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "error": { diff --git a/homeassistant/components/wolflink/translations/pl.json b/homeassistant/components/wolflink/translations/pl.json index 73d8caf4a02..d6d42eafb8a 100644 --- a/homeassistant/components/wolflink/translations/pl.json +++ b/homeassistant/components/wolflink/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_auth": "Niepoprawne uwierzytelnienie.", + "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "[%key::common::config_flow::error::unknown%]" }, "step": { diff --git a/homeassistant/components/xiaomi_aqara/translations/pl.json b/homeassistant/components/xiaomi_aqara/translations/pl.json index a603566d569..fba37ad0249 100644 --- a/homeassistant/components/xiaomi_aqara/translations/pl.json +++ b/homeassistant/components/xiaomi_aqara/translations/pl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", - "already_in_progress": "Konfiguracja dla tej bramki jest ju\u017c w toku.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja dla tej bramki jest ju\u017c w toku", "not_xiaomi_aqara": "To nie jest bramka Xiaomi Aqara, wykryte urz\u0105dzenie nie pasuje do znanych bramek." }, "error": { @@ -10,7 +10,7 @@ "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.", + "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}", diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json index 8fedd7e2b74..0c32eaf77ca 100644 --- a/homeassistant/components/xiaomi_miio/translations/pl.json +++ b/homeassistant/components/xiaomi_miio/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja tego urz\u0105dzenia Xiaomi Miio jest ju\u017c w toku." }, "error": { diff --git a/homeassistant/components/yeelight/translations/nl.json b/homeassistant/components/yeelight/translations/nl.json index 804e9a9e545..f9f78ffb6f6 100644 --- a/homeassistant/components/yeelight/translations/nl.json +++ b/homeassistant/components/yeelight/translations/nl.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, + "error": { + "cannot_connect": "Kon niet verbinden" + }, "step": { "pick_device": { "data": { @@ -8,7 +15,8 @@ }, "user": { "data": { - "host": "Host" + "host": "Host", + "ip_address": "IP adres" }, "description": "Als u host leeg laat, wordt detectie gebruikt om apparaten te vinden." } @@ -20,6 +28,7 @@ "data": { "model": "Model (optioneel)", "nightlight_switch": "Gebruik Nachtlichtschakelaar", + "save_on_change": "Bewaar status bij wijziging", "transition": "Overgangstijd (ms)", "use_music_mode": "Schakel de muziekmodus in" }, diff --git a/homeassistant/components/yeelight/translations/pl.json b/homeassistant/components/yeelight/translations/pl.json index 77d69d3fcf9..325e3cbc7ae 100644 --- a/homeassistant/components/yeelight/translations/pl.json +++ b/homeassistant/components/yeelight/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci." }, "error": { From a13c4d4c1739e30bf739f70f0967126679ce8dd8 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 190/514] 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 d650bcca3b80e9f3fb6d43cc72d1505d137ae6cb 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 191/514] 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 16e0ed9242be1bbc4f6d753c2e58d8569abc9afd 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 192/514] 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 9309297d76a180358d0e8abb48cf733d607d1686 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Sep 2020 12:48:38 +0200 Subject: [PATCH 193/514] 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 b515480a98fa6e5e57ea6670ca3c0e749b677b09 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Sep 2020 13:17:05 +0200 Subject: [PATCH 194/514] 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 80764261c3b5a39d632d9533182a5fc4ee5ff0bb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Sep 2020 13:47:52 +0200 Subject: [PATCH 195/514] Upgrade sentry-sdk to 0.17.6 (#40133) --- 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 999981c6c27..6f511837ca8 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.5"], + "requirements": ["sentry-sdk==0.17.6"], "codeowners": ["@dcramer", "@frenck"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0f1304c076b..90bbafd7668 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1967,7 +1967,7 @@ sense-hat==2.2.0 sense_energy==0.8.0 # homeassistant.components.sentry -sentry-sdk==0.17.5 +sentry-sdk==0.17.6 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 193d39ee4b7..ce9bd908996 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -918,7 +918,7 @@ samsungtvws==1.4.0 sense_energy==0.8.0 # homeassistant.components.sentry -sentry-sdk==0.17.5 +sentry-sdk==0.17.6 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From ff0562ad1e249967e9c14c4da1591aa74c121901 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Sep 2020 15:28:25 +0200 Subject: [PATCH 196/514] 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 9444009cbc9..c35437283cc 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 d25192d8c5818553581d8dcbe5128efc962e766a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Sep 2020 16:26:34 +0200 Subject: [PATCH 197/514] 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 2b7e735e3d51366bd88168fe08e5d855d9fe1be8 Mon Sep 17 00:00:00 2001 From: Niccolo Zapponi Date: Wed, 16 Sep 2020 16:00:32 +0100 Subject: [PATCH 198/514] Remove unsupported states from security systems in HomeKit (#40060) --- .../homekit/type_security_systems.py | 77 ++++++++++- .../homekit/test_type_security_systems.py | 120 ++++++++++++++++++ 2 files changed, 195 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 7d8dcac046d..a5530a45d56 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -2,8 +2,15 @@ import logging from pyhap.const import CATEGORY_ALARM_SYSTEM +from pyhap.loader import get_loader from homeassistant.components.alarm_control_panel import DOMAIN +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, @@ -36,6 +43,13 @@ HASS_TO_HOMEKIT = { STATE_ALARM_TRIGGERED: 4, } +HASS_TO_HOMEKIT_SERVICES = { + SERVICE_ALARM_ARM_HOME: 0, + SERVICE_ALARM_ARM_AWAY: 1, + SERVICE_ALARM_ARM_NIGHT: 2, + SERVICE_ALARM_DISARM: 3, +} + HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()} STATE_TO_SERVICE = { @@ -56,13 +70,72 @@ class SecuritySystem(HomeAccessory): state = self.hass.states.get(self.entity_id) self._alarm_code = self.config.get(ATTR_CODE) + supported_states = state.attributes.get( + "supported_features", + ( + SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER + ), + ) + + loader = get_loader() + default_current_states = loader.get_char( + "SecuritySystemCurrentState" + ).properties.get("ValidValues") + default_target_services = loader.get_char( + "SecuritySystemTargetState" + ).properties.get("ValidValues") + + current_supported_states = [ + HASS_TO_HOMEKIT[STATE_ALARM_DISARMED], + HASS_TO_HOMEKIT[STATE_ALARM_TRIGGERED], + ] + target_supported_services = [HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_DISARM]] + + if supported_states & SUPPORT_ALARM_ARM_HOME: + current_supported_states.append(HASS_TO_HOMEKIT[STATE_ALARM_ARMED_HOME]) + target_supported_services.append( + HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_ARM_HOME] + ) + + if supported_states & SUPPORT_ALARM_ARM_AWAY: + current_supported_states.append(HASS_TO_HOMEKIT[STATE_ALARM_ARMED_AWAY]) + target_supported_services.append( + HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_ARM_AWAY] + ) + + if supported_states & SUPPORT_ALARM_ARM_NIGHT: + current_supported_states.append(HASS_TO_HOMEKIT[STATE_ALARM_ARMED_NIGHT]) + target_supported_services.append( + HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_ARM_NIGHT] + ) + + new_current_states = { + key: val + for key, val in default_current_states.items() + if val in current_supported_states + } + new_target_services = { + key: val + for key, val in default_target_services.items() + if val in target_supported_services + } + serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM) self.char_current_state = serv_alarm.configure_char( - CHAR_CURRENT_SECURITY_STATE, value=3 + CHAR_CURRENT_SECURITY_STATE, + value=HASS_TO_HOMEKIT[STATE_ALARM_DISARMED], + valid_values=new_current_states, ) self.char_target_state = serv_alarm.configure_char( - CHAR_TARGET_SECURITY_STATE, value=3, setter_callback=self.set_security_state + CHAR_TARGET_SECURITY_STATE, + value=HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_DISARM], + valid_values=new_target_services, + setter_callback=self.set_security_state, ) + # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup self.async_update_state(state) diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index b139fac3657..d6bf74bb7cf 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -1,7 +1,14 @@ """Test different accessory types: Security Systems.""" +from pyhap.loader import get_loader import pytest from homeassistant.components.alarm_control_panel import DOMAIN +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) from homeassistant.components.homekit.const import ATTR_VALUE from homeassistant.components.homekit.type_security_systems import SecuritySystem from homeassistant.const import ( @@ -129,3 +136,116 @@ async def test_no_alarm_code(hass, hk_driver, config, events): assert acc.char_target_state.value == 0 assert len(events) == 1 assert events[-1].data[ATTR_VALUE] is None + + +async def test_supported_states(hass, hk_driver, events): + """Test different supported states.""" + code = "1234" + config = {ATTR_CODE: code} + entity_id = "alarm_control_panel.test" + + loader = get_loader() + default_current_states = loader.get_char( + "SecuritySystemCurrentState" + ).properties.get("ValidValues") + default_target_services = loader.get_char( + "SecuritySystemTargetState" + ).properties.get("ValidValues") + + # Set up a number of test configuration + test_configs = [ + { + "features": SUPPORT_ALARM_ARM_HOME, + "current_values": [ + default_current_states["Disarmed"], + default_current_states["AlarmTriggered"], + default_current_states["StayArm"], + ], + "target_values": [ + default_target_services["Disarm"], + default_target_services["StayArm"], + ], + }, + { + "features": SUPPORT_ALARM_ARM_AWAY, + "current_values": [ + default_current_states["Disarmed"], + default_current_states["AlarmTriggered"], + default_current_states["AwayArm"], + ], + "target_values": [ + default_target_services["Disarm"], + default_target_services["AwayArm"], + ], + }, + { + "features": SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY, + "current_values": [ + default_current_states["Disarmed"], + default_current_states["AlarmTriggered"], + default_current_states["StayArm"], + default_current_states["AwayArm"], + ], + "target_values": [ + default_target_services["Disarm"], + default_target_services["StayArm"], + default_target_services["AwayArm"], + ], + }, + { + "features": SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT, + "current_values": [ + default_current_states["Disarmed"], + default_current_states["AlarmTriggered"], + default_current_states["StayArm"], + default_current_states["AwayArm"], + default_current_states["NightArm"], + ], + "target_values": [ + default_target_services["Disarm"], + default_target_services["StayArm"], + default_target_services["AwayArm"], + default_target_services["NightArm"], + ], + }, + { + "features": SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER, + "current_values": [ + default_current_states["Disarmed"], + default_current_states["AlarmTriggered"], + default_current_states["StayArm"], + default_current_states["AwayArm"], + default_current_states["NightArm"], + ], + "target_values": [ + default_target_services["Disarm"], + default_target_services["StayArm"], + default_target_services["AwayArm"], + default_target_services["NightArm"], + ], + }, + ] + + for test_config in test_configs: + attrs = {"supported_features": test_config.get("features")} + + hass.states.async_set(entity_id, None, attributes=attrs) + await hass.async_block_till_done() + + acc = SecuritySystem(hass, hk_driver, "SecuritySystem", entity_id, 2, config) + await acc.run_handler() + await hass.async_block_till_done() + + valid_current_values = acc.char_current_state.properties.get("ValidValues") + valid_target_values = acc.char_target_state.properties.get("ValidValues") + + for val in valid_current_values.values(): + assert val in test_config.get("current_values") + + for val in valid_target_values.values(): + assert val in test_config.get("target_values") From 540b9256598577603e9b4a703901c1233ae4bec0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Sep 2020 17:04:57 +0200 Subject: [PATCH 199/514] 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 5ea04d64f601a0d2851fd20b847c42d8331efe84 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2020 10:35:01 -0500 Subject: [PATCH 200/514] Prompt to reauth when the august password is changed or token expires (#40103) * Prompt to reauth when the august password is changed or token expires * augment missing config flow coverage * augment test coverage * Adjust test * Update homeassistant/components/august/__init__.py Co-authored-by: Martin Hjelmare * block until patch complete Co-authored-by: Martin Hjelmare --- homeassistant/components/august/__init__.py | 40 ++++- .../components/august/config_flow.py | 62 +++++--- homeassistant/components/august/gateway.py | 78 +++++----- homeassistant/components/august/strings.json | 5 +- .../components/august/translations/en.json | 55 +++---- tests/components/august/mocks.py | 17 ++- tests/components/august/test_config_flow.py | 70 +++++++++ tests/components/august/test_gateway.py | 10 +- tests/components/august/test_init.py | 142 +++++++++++++++++- 9 files changed, 379 insertions(+), 100 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index e0d7749dcbb..feaf61450e8 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -3,13 +3,18 @@ import asyncio import itertools import logging -from aiohttp import ClientError +from aiohttp import ClientError, ClientResponseError from august.authenticator import ValidationResult from august.exceptions import AugustApiAIOHTTPError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME +from homeassistant.const import ( + CONF_PASSWORD, + CONF_TIMEOUT, + CONF_USERNAME, + HTTP_UNAUTHORIZED, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -29,7 +34,7 @@ from .const import ( MIN_TIME_BETWEEN_DETAIL_UPDATES, VERIFICATION_CODE_KEY, ) -from .exceptions import InvalidAuth, RequireValidation +from .exceptions import CannotConnect, InvalidAuth, RequireValidation from .gateway import AugustGateway from .subscriber import AugustSubscriberMixin @@ -113,10 +118,7 @@ async def async_setup_august(hass, config_entry, august_gateway): await august_gateway.async_authenticate() except RequireValidation: await async_request_validation(hass, config_entry, august_gateway) - return False - except InvalidAuth: - _LOGGER.error("Password is no longer valid. Please set up August again") - return False + raise # We still use the configurator to get a new 2fa code # when needed since config_flow doesn't have a way @@ -171,8 +173,30 @@ 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 as err: + except ClientResponseError as err: + if err.status == HTTP_UNAUTHORIZED: + _async_start_reauth(hass, entry) + return False + raise ConfigEntryNotReady from err + except InvalidAuth: + _async_start_reauth(hass, entry) + return False + except RequireValidation: + return False + except (CannotConnect, asyncio.TimeoutError) as err: + raise ConfigEntryNotReady from err + + +def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "reauth"}, + data=entry.data, + ) + ) + _LOGGER.error("Password is no longer valid. Please reauthenticate") async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py index bf6f1d9cd81..f595479c0cf 100644 --- a/homeassistant/components/august/config_flow.py +++ b/homeassistant/components/august/config_flow.py @@ -4,7 +4,7 @@ import logging from august.authenticator import ValidationResult import voluptuous as vol -from homeassistant import config_entries, core +from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from .const import ( @@ -19,18 +19,8 @@ from .gateway import AugustGateway _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_LOGIN_METHOD, default="phone"): vol.In(LOGIN_METHODS), - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int), - } -) - async def async_validate_input( - hass: core.HomeAssistant, data, august_gateway, ): @@ -79,6 +69,7 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Store an AugustGateway().""" self._august_gateway = None self.user_auth_details = {} + self._needs_reset = False super().__init__() async def async_step_user(self, user_input=None): @@ -87,30 +78,45 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._august_gateway = AugustGateway(self.hass) errors = {} if user_input is not None: - await self._august_gateway.async_setup(user_input) + combined_inputs = {**self.user_auth_details, **user_input} + await self._august_gateway.async_setup(combined_inputs) + if self._needs_reset: + self._needs_reset = False + await self._august_gateway.async_reset_authentication() try: info = await async_validate_input( - self.hass, - user_input, + combined_inputs, self._august_gateway, ) - await self.async_set_unique_id(user_input[CONF_USERNAME]) - return self.async_create_entry(title=info["title"], data=info["data"]) except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" except RequireValidation: - self.user_auth_details = user_input + self.user_auth_details.update(user_input) return await self.async_step_validation() except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" + if not errors: + self.user_auth_details.update(user_input) + + existing_entry = await self.async_set_unique_id( + combined_inputs[CONF_USERNAME] + ) + if existing_entry: + self.hass.config_entries.async_update_entry( + existing_entry, data=info["data"] + ) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + return self.async_create_entry(title=info["title"], data=info["data"]) + return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="user", data_schema=self._async_build_schema(), errors=errors ) async def async_step_validation(self, user_input=None): @@ -135,3 +141,23 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return await self.async_step_user(user_input) + + async def async_step_reauth(self, data): + """Handle configuration by re-auth.""" + self.user_auth_details = dict(data) + self._needs_reset = True + return await self.async_step_user() + + def _async_build_schema(self): + """Generate the config flow schema.""" + base_schema = { + vol.Required(CONF_LOGIN_METHOD, default="phone"): vol.In(LOGIN_METHODS), + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int), + } + for key in self.user_auth_details: + if key == CONF_PASSWORD or key not in base_schema: + continue + del base_schema[key] + return vol.Schema(base_schema) diff --git a/homeassistant/components/august/gateway.py b/homeassistant/components/august/gateway.py index 6918907611f..b72bb52e710 100644 --- a/homeassistant/components/august/gateway.py +++ b/homeassistant/components/august/gateway.py @@ -2,12 +2,18 @@ import asyncio import logging +import os -from aiohttp import ClientError +from aiohttp import ClientError, ClientResponseError from august.api_async import ApiAsync from august.authenticator_async import AuthenticationState, AuthenticatorAsync -from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME +from homeassistant.const import ( + CONF_PASSWORD, + CONF_TIMEOUT, + CONF_USERNAME, + HTTP_UNAUTHORIZED, +) from homeassistant.helpers import aiohttp_client from .const import ( @@ -32,29 +38,14 @@ class AugustGateway: self._access_token_cache_file = None self._hass = hass self._config = None - self._api = None - self._authenticator = None - self._authentication = None - - @property - def authenticator(self): - """August authentication object from py-august.""" - return self._authenticator - - @property - def authentication(self): - """August authentication object from py-august.""" - return self._authentication + self.api = None + self.authenticator = None + self.authentication = None @property def access_token(self): """Access token for the api.""" - return self._authentication.access_token - - @property - def api(self): - """August api object from py-august.""" - return self._api + return self.authentication.access_token def config_entry(self): """Config entry.""" @@ -78,12 +69,12 @@ class AugustGateway: ) self._config = conf - self._api = ApiAsync( + self.api = ApiAsync( self._aiohttp_session, timeout=self._config.get(CONF_TIMEOUT) ) - self._authenticator = AuthenticatorAsync( - self._api, + self.authenticator = AuthenticatorAsync( + self.api, self._config[CONF_LOGIN_METHOD], self._config[CONF_USERNAME], self._config[CONF_PASSWORD], @@ -93,30 +84,47 @@ class AugustGateway: ), ) - await self._authenticator.async_setup_authentication() + await self.authenticator.async_setup_authentication() async def async_authenticate(self): """Authenticate with the details provided to setup.""" - self._authentication = None + self.authentication = None try: - self._authentication = await self.authenticator.async_authenticate() + self.authentication = await self.authenticator.async_authenticate() + if self.authentication.state == AuthenticationState.AUTHENTICATED: + # Call the locks api to verify we are actually + # authenticated because we can be authenticated + # by have no access + await self.api.async_get_operable_locks(self.access_token) + except ClientResponseError as ex: + if ex.status == HTTP_UNAUTHORIZED: + raise InvalidAuth from ex + + raise CannotConnect from ex except ClientError as ex: _LOGGER.error("Unable to connect to August service: %s", str(ex)) raise CannotConnect from ex - if self._authentication.state == AuthenticationState.BAD_PASSWORD: + if self.authentication.state == AuthenticationState.BAD_PASSWORD: raise InvalidAuth - if self._authentication.state == AuthenticationState.REQUIRES_VALIDATION: + if self.authentication.state == AuthenticationState.REQUIRES_VALIDATION: raise RequireValidation - if self._authentication.state != AuthenticationState.AUTHENTICATED: - _LOGGER.error( - "Unknown authentication state: %s", self._authentication.state - ) + if self.authentication.state != AuthenticationState.AUTHENTICATED: + _LOGGER.error("Unknown authentication state: %s", self.authentication.state) raise InvalidAuth - return self._authentication + return self.authentication + + async def async_reset_authentication(self): + """Remove the cache file.""" + await self._hass.async_add_executor_job(self._reset_authentication) + + def _reset_authentication(self): + """Remove the cache file.""" + if os.path.exists(self._access_token_cache_file): + os.unlink(self._access_token_cache_file) async def async_refresh_access_token_if_needed(self): """Refresh the august access token if needed.""" @@ -130,4 +138,4 @@ class AugustGateway: self.authentication.access_token_expires, refreshed_authentication.access_token_expires, ) - self._authentication = refreshed_authentication + self.authentication = refreshed_authentication diff --git a/homeassistant/components/august/strings.json b/homeassistant/components/august/strings.json index 880c13c7fe2..254e8146984 100644 --- a/homeassistant/components/august/strings.json +++ b/homeassistant/components/august/strings.json @@ -6,7 +6,8 @@ "invalid_auth": "Invalid authentication" }, "abort": { - "already_configured": "Account is already configured" + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" }, "step": { "validation": { @@ -28,4 +29,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/august/translations/en.json b/homeassistant/components/august/translations/en.json index b8bf1b1bc03..254e8146984 100644 --- a/homeassistant/components/august/translations/en.json +++ b/homeassistant/components/august/translations/en.json @@ -1,31 +1,32 @@ { - "config": { - "abort": { - "already_configured": "Account is already configured" + "config": { + "error": { + "unknown": "Unexpected error", + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication" + }, + "abort": { + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "step": { + "validation": { + "title": "Two factor authentication", + "data": { + "code": "Verification code" }, - "error": { - "cannot_connect": "Failed to connect, please try again", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "description": "Please check your {login_method} ({username}) and enter the verification code below" + }, + "user": { + "description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.", + "data": { + "timeout": "Timeout (seconds)", + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]", + "login_method": "Login Method" }, - "step": { - "user": { - "data": { - "login_method": "Login Method", - "password": "Password", - "timeout": "Timeout (seconds)", - "username": "Username" - }, - "description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.", - "title": "Setup an August account" - }, - "validation": { - "data": { - "code": "Verification code" - }, - "description": "Please check your {login_method} ({username}) and enter the verification code below", - "title": "Two factor authentication" - } - } + "title": "Setup an August account" + } } -} \ No newline at end of file + } +} diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index c471dfca2a9..93b64ebbd3f 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -43,12 +43,21 @@ def _mock_get_config(): } +def _mock_authenticator(auth_state): + """Mock an august authenticator.""" + authenticator = MagicMock() + type(authenticator).state = PropertyMock(return_value=auth_state) + return authenticator + + @patch("homeassistant.components.august.gateway.ApiAsync") @patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate") async def _mock_setup_august(hass, api_instance, authenticate_mock, api_mock): """Set up august integration.""" authenticate_mock.side_effect = MagicMock( - return_value=_mock_august_authentication("original_token", 1234) + return_value=_mock_august_authentication( + "original_token", 1234, AuthenticationState.AUTHENTICATED + ) ) api_mock.return_value = api_instance assert await async_setup_component(hass, DOMAIN, _mock_get_config()) @@ -185,11 +194,9 @@ async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects): return await _mock_setup_august(hass, api_instance) -def _mock_august_authentication(token_text, token_timestamp): +def _mock_august_authentication(token_text, token_timestamp, state): authentication = MagicMock(name="august.authentication") - type(authentication).state = PropertyMock( - return_value=AuthenticationState.AUTHENTICATED - ) + type(authentication).state = PropertyMock(return_value=state) type(authentication).access_token = PropertyMock(return_value=token_text) type(authentication).access_token_expires = PropertyMock( return_value=token_timestamp diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index 2f20347acae..1c23976a6f9 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -17,6 +17,7 @@ from homeassistant.components.august.exceptions import ( from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from tests.async_mock import patch +from tests.common import MockConfigEntry async def test_form(hass): @@ -84,6 +85,29 @@ async def test_form_invalid_auth(hass): assert result2["errors"] == {"base": "invalid_auth"} +async def test_user_unexpected_exception(hass): + """Test we handle an unexpected exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", + side_effect=ValueError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_LOGIN_METHOD: "email", + CONF_USERNAME: "my@email.tld", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + async def test_form_cannot_connect(hass): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( @@ -197,3 +221,49 @@ async def test_form_needs_validate(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_reauth(hass): + """Test reauthenticate.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LOGIN_METHOD: "email", + CONF_USERNAME: "my@email.tld", + CONF_PASSWORD: "test-password", + CONF_INSTALL_ID: None, + CONF_TIMEOUT: 10, + CONF_ACCESS_TOKEN_CACHE_FILE: ".my@email.tld.august.conf", + }, + unique_id="my@email.tld", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=entry.data + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", + return_value=True, + ), patch( + "homeassistant.components.august.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.august.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "new-test-password", + }, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/august/test_gateway.py b/tests/components/august/test_gateway.py index ec035b9ec38..c1aa0723baa 100644 --- a/tests/components/august/test_gateway.py +++ b/tests/components/august/test_gateway.py @@ -1,4 +1,6 @@ """The gateway tests for the august platform.""" +from august.authenticator_common import AuthenticationState + from homeassistant.components.august.const import DOMAIN from homeassistant.components.august.gateway import AugustGateway @@ -11,6 +13,7 @@ async def test_refresh_access_token(hass): await _patched_refresh_access_token(hass, "new_token", 5678) +@patch("homeassistant.components.august.gateway.ApiAsync.async_get_operable_locks") @patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate") @patch("homeassistant.components.august.gateway.AuthenticatorAsync.should_refresh") @patch( @@ -23,9 +26,12 @@ async def _patched_refresh_access_token( refresh_access_token_mock, should_refresh_mock, authenticate_mock, + async_get_operable_locks_mock, ): authenticate_mock.side_effect = MagicMock( - return_value=_mock_august_authentication("original_token", 1234) + return_value=_mock_august_authentication( + "original_token", 1234, AuthenticationState.AUTHENTICATED + ) ) august_gateway = AugustGateway(hass) mocked_config = _mock_get_config() @@ -38,7 +44,7 @@ async def _patched_refresh_access_token( should_refresh_mock.return_value = True refresh_access_token_mock.return_value = _mock_august_authentication( - new_token, new_token_expire_time + new_token, new_token_expire_time, AuthenticationState.AUTHENTICATED ) await august_gateway.async_refresh_access_token_if_needed() refresh_access_token_mock.assert_called() diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index bcc05e51c71..f954ff83c25 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -1,6 +1,8 @@ """The tests for the august platform.""" import asyncio +from aiohttp import ClientResponseError +from august.authenticator_common import AuthenticationState from august.exceptions import AugustApiAIOHTTPError from homeassistant import setup @@ -12,7 +14,10 @@ from homeassistant.components.august.const import ( DOMAIN, ) from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ( + ENTRY_STATE_SETUP_ERROR, + ENTRY_STATE_SETUP_RETRY, +) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PASSWORD, @@ -30,6 +35,7 @@ from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.august.mocks import ( _create_august_with_devices, + _mock_august_authentication, _mock_doorsense_enabled_august_lock_detail, _mock_doorsense_missing_august_lock_detail, _mock_get_config, @@ -54,8 +60,8 @@ async def test_august_is_offline(hass): side_effect=asyncio.TimeoutError, ): await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() - await hass.async_block_till_done() assert config_entry.state == ENTRY_STATE_SETUP_RETRY @@ -158,7 +164,7 @@ async def test_set_up_from_yaml(hass): await hass.async_block_till_done() assert len(mock_setup_august.mock_calls) == 1 call = mock_setup_august.call_args - args, kwargs = call + args, _ = call imported_config_entry = args[1] # The import must use DEFAULT_AUGUST_CONFIG_FILE so they # do not loose their token when config is migrated @@ -170,3 +176,133 @@ async def test_set_up_from_yaml(hass): CONF_TIMEOUT: None, CONF_USERNAME: "mocked_username", } + + +async def test_auth_fails(hass): + """Config entry state is ENTRY_STATE_SETUP_ERROR when auth fails.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=_mock_get_config()[DOMAIN], + title="August august", + ) + config_entry.add_to_hass(hass) + assert hass.config_entries.flow.async_progress() == [] + + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "august.authenticator_async.AuthenticatorAsync.async_authenticate", + side_effect=ClientResponseError(None, None, status=401), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ENTRY_STATE_SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + + assert flows[0]["step_id"] == "user" + + +async def test_bad_password(hass): + """Config entry state is ENTRY_STATE_SETUP_ERROR when the password has been changed.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=_mock_get_config()[DOMAIN], + title="August august", + ) + config_entry.add_to_hass(hass) + assert hass.config_entries.flow.async_progress() == [] + + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "august.authenticator_async.AuthenticatorAsync.async_authenticate", + return_value=_mock_august_authentication( + "original_token", 1234, AuthenticationState.BAD_PASSWORD + ), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ENTRY_STATE_SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + + assert flows[0]["step_id"] == "user" + + +async def test_http_failure(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", + ) + config_entry.add_to_hass(hass) + assert hass.config_entries.flow.async_progress() == [] + + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "august.authenticator_async.AuthenticatorAsync.async_authenticate", + side_effect=ClientResponseError(None, None, status=500), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ENTRY_STATE_SETUP_RETRY + + assert hass.config_entries.flow.async_progress() == [] + + +async def test_unknown_auth_state(hass): + """Config entry state is ENTRY_STATE_SETUP_ERROR when august is in an unknown auth state.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=_mock_get_config()[DOMAIN], + title="August august", + ) + config_entry.add_to_hass(hass) + assert hass.config_entries.flow.async_progress() == [] + + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "august.authenticator_async.AuthenticatorAsync.async_authenticate", + return_value=_mock_august_authentication("original_token", 1234, None), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ENTRY_STATE_SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + + assert flows[0]["step_id"] == "user" + + +async def test_requires_validation_state(hass): + """Config entry state is ENTRY_STATE_SETUP_ERROR when august requires validation.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=_mock_get_config()[DOMAIN], + title="August august", + ) + config_entry.add_to_hass(hass) + assert hass.config_entries.flow.async_progress() == [] + + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "august.authenticator_async.AuthenticatorAsync.async_authenticate", + return_value=_mock_august_authentication( + "original_token", 1234, AuthenticationState.REQUIRES_VALIDATION + ), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ENTRY_STATE_SETUP_ERROR + + assert hass.config_entries.flow.async_progress() == [] From f83f3c927ad207ce52b83cd84ccf94165f916ef4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Sep 2020 21:38:40 +0200 Subject: [PATCH 201/514] 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 a2cf09fb5423662df37fd4bd2de4c8005690c401 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 16 Sep 2020 22:31:43 +0200 Subject: [PATCH 202/514] 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 90bbafd7668..5beac3fd4f8 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 ce9bd908996..9654dd741ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -373,7 +373,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 00093faae223cb0c79f327ccd4323274660ddda7 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Wed, 16 Sep 2020 14:23:50 -0700 Subject: [PATCH 203/514] Clean up vera typings (#40143) Co-authored-by: J. Nick Koston --- homeassistant/components/vera/__init__.py | 21 ++++++----- .../components/vera/binary_sensor.py | 14 +++++--- homeassistant/components/vera/climate.py | 36 ++++++++++--------- homeassistant/components/vera/config_flow.py | 7 ++-- homeassistant/components/vera/cover.py | 22 +++++++----- homeassistant/components/vera/light.py | 24 +++++++------ homeassistant/components/vera/lock.py | 20 ++++++----- homeassistant/components/vera/scene.py | 12 ++++--- homeassistant/components/vera/sensor.py | 19 +++++----- homeassistant/components/vera/switch.py | 20 ++++++----- 10 files changed, 113 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 0d34a521276..fdc8503ed70 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -2,7 +2,7 @@ import asyncio from collections import defaultdict import logging -from typing import Type +from typing import Any, Dict, Generic, List, Optional, Type, TypeVar import pyvera as veraApi from requests.exceptions import RequestException @@ -168,7 +168,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return True -def map_vera_device(vera_device, remap): +def map_vera_device(vera_device: veraApi.VeraDevice, remap: List[int]) -> str: """Map vera classes to Home Assistant types.""" type_map = { @@ -198,10 +198,13 @@ def map_vera_device(vera_device, remap): ) -class VeraDevice(Entity): +DeviceType = TypeVar("DeviceType", bound=veraApi.VeraDevice) + + +class VeraDevice(Generic[DeviceType], Entity): """Representation of a Vera device entity.""" - def __init__(self, vera_device, controller_data: ControllerData): + def __init__(self, vera_device: DeviceType, controller_data: ControllerData): """Initialize the device.""" self.vera_device = vera_device self.controller = controller_data.controller @@ -217,26 +220,26 @@ class VeraDevice(Entity): else: self._unique_id = f"vera_{controller_data.config_entry.unique_id}_{self.vera_device.vera_device_id}" - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to updates.""" self.controller.register(self.vera_device, self._update_callback) - def _update_callback(self, _device): + def _update_callback(self, _device: DeviceType) -> None: """Update the state.""" self.schedule_update_ha_state(True) @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return self._name @property - def should_poll(self): + def should_poll(self) -> bool: """Get polling requirement from vera device.""" return self.vera_device.should_poll @property - def device_state_attributes(self): + def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return the state attributes of the device.""" attr = {} diff --git a/homeassistant/components/vera/binary_sensor.py b/homeassistant/components/vera/binary_sensor.py index 7ab24e9544f..2e66d38e249 100644 --- a/homeassistant/components/vera/binary_sensor.py +++ b/homeassistant/components/vera/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Vera binary sensors.""" import logging -from typing import Callable, List +from typing import Callable, List, Optional + +import pyvera as veraApi from homeassistant.components.binary_sensor import ( DOMAIN as PLATFORM_DOMAIN, @@ -32,20 +34,22 @@ async def async_setup_entry( ) -class VeraBinarySensor(VeraDevice, BinarySensorEntity): +class VeraBinarySensor(VeraDevice[veraApi.VeraBinarySensor], BinarySensorEntity): """Representation of a Vera Binary Sensor.""" - def __init__(self, vera_device, controller_data: ControllerData): + def __init__( + self, vera_device: veraApi.VeraBinarySensor, controller_data: ControllerData + ): """Initialize the binary_sensor.""" self._state = False VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property - def is_on(self): + def is_on(self) -> Optional[bool]: """Return true if sensor is on.""" return self._state - def update(self): + def update(self) -> None: """Get the latest data and update the state.""" self._state = self.vera_device.is_tripped diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index a8ba647c1d6..0946de4a379 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -1,6 +1,8 @@ """Support for Vera thermostats.""" import logging -from typing import Callable, List +from typing import Any, Callable, List, Optional + +import pyvera as veraApi from homeassistant.components.climate import ( DOMAIN as PLATFORM_DOMAIN, @@ -49,21 +51,23 @@ async def async_setup_entry( ) -class VeraThermostat(VeraDevice, ClimateEntity): +class VeraThermostat(VeraDevice[veraApi.VeraThermostat], ClimateEntity): """Representation of a Vera Thermostat.""" - def __init__(self, vera_device, controller_data: ControllerData): + def __init__( + self, vera_device: veraApi.VeraThermostat, controller_data: ControllerData + ): """Initialize the Vera device.""" VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property - def supported_features(self): + def supported_features(self) -> Optional[int]: """Return the list of supported features.""" return SUPPORT_FLAGS @property - def hvac_mode(self): + def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode. Need to be one of HVAC_MODE_*. @@ -78,7 +82,7 @@ class VeraThermostat(VeraDevice, ClimateEntity): return HVAC_MODE_OFF @property - def hvac_modes(self): + def hvac_modes(self) -> List[str]: """Return the list of available hvac operation modes. Need to be a subset of HVAC_MODES. @@ -86,7 +90,7 @@ class VeraThermostat(VeraDevice, ClimateEntity): return SUPPORT_HVAC @property - def fan_mode(self): + def fan_mode(self) -> Optional[str]: """Return the fan setting.""" mode = self.vera_device.get_fan_mode() if mode == "ContinuousOn": @@ -94,11 +98,11 @@ class VeraThermostat(VeraDevice, ClimateEntity): return FAN_AUTO @property - def fan_modes(self): + def fan_modes(self) -> Optional[List[str]]: """Return a list of available fan modes.""" return FAN_OPERATION_LIST - def set_fan_mode(self, fan_mode): + def set_fan_mode(self, fan_mode) -> None: """Set new target temperature.""" if fan_mode == FAN_ON: self.vera_device.fan_on() @@ -108,14 +112,14 @@ class VeraThermostat(VeraDevice, ClimateEntity): self.schedule_update_ha_state() @property - def current_power_w(self): + def current_power_w(self) -> Optional[float]: """Return the current power usage in W.""" power = self.vera_device.power if power: return convert(power, float, 0.0) @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" vera_temp_units = self.vera_device.vera_controller.temperature_units @@ -125,28 +129,28 @@ class VeraThermostat(VeraDevice, ClimateEntity): return TEMP_CELSIUS @property - def current_temperature(self): + def current_temperature(self) -> Optional[float]: """Return the current temperature.""" return self.vera_device.get_current_temperature() @property - def operation(self): + def operation(self) -> str: """Return current operation ie. heat, cool, idle.""" return self.vera_device.get_hvac_mode() @property - def target_temperature(self): + def target_temperature(self) -> Optional[float]: """Return the temperature we try to reach.""" return self.vera_device.get_current_goal_temperature() - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if kwargs.get(ATTR_TEMPERATURE) is not None: self.vera_device.set_temperature(kwargs.get(ATTR_TEMPERATURE)) self.schedule_update_ha_state() - def set_hvac_mode(self, hvac_mode): + def set_hvac_mode(self, hvac_mode) -> None: """Set new target hvac mode.""" if hvac_mode == HVAC_MODE_OFF: self.vera_device.turn_off() diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index 26ae509337b..754d2eca542 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -8,6 +8,7 @@ from requests.exceptions import RequestException import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import callback from homeassistant.helpers.entity_registry import EntityRegistry @@ -68,11 +69,11 @@ def options_data(user_input: dict) -> dict: class OptionsFlowHandler(config_entries.OptionsFlow): """Options for the component.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: ConfigEntry): """Init object.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input: dict = None): """Manage the options.""" if user_input is not None: return self.async_create_entry( @@ -91,7 +92,7 @@ class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry) -> OptionsFlowHandler: + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler: """Get the options flow.""" return OptionsFlowHandler(config_entry) diff --git a/homeassistant/components/vera/cover.py b/homeassistant/components/vera/cover.py index bad36727c15..49b15e91eb2 100644 --- a/homeassistant/components/vera/cover.py +++ b/homeassistant/components/vera/cover.py @@ -1,6 +1,8 @@ """Support for Vera cover - curtains, rollershutters etc.""" import logging -from typing import Callable, List +from typing import Any, Callable, List + +import pyvera as veraApi from homeassistant.components.cover import ( ATTR_POSITION, @@ -33,16 +35,18 @@ async def async_setup_entry( ) -class VeraCover(VeraDevice, CoverEntity): +class VeraCover(VeraDevice[veraApi.VeraCurtain], CoverEntity): """Representation a Vera Cover.""" - def __init__(self, vera_device, controller_data: ControllerData): + def __init__( + self, vera_device: veraApi.VeraCurtain, controller_data: ControllerData + ): """Initialize the Vera device.""" VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property - def current_cover_position(self): + def current_cover_position(self) -> int: """ Return current position of cover. @@ -55,28 +59,28 @@ class VeraCover(VeraDevice, CoverEntity): return 100 return position - def set_cover_position(self, **kwargs): + def set_cover_position(self, **kwargs) -> None: """Move the cover to a specific position.""" self.vera_device.set_level(kwargs.get(ATTR_POSITION)) self.schedule_update_ha_state() @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the cover is closed.""" if self.current_cover_position is not None: return self.current_cover_position == 0 - def open_cover(self, **kwargs): + def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" self.vera_device.open() self.schedule_update_ha_state() - def close_cover(self, **kwargs): + def close_cover(self, **kwargs: Any) -> None: """Close the cover.""" self.vera_device.close() self.schedule_update_ha_state() - def stop_cover(self, **kwargs): + def stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" self.vera_device.stop() self.schedule_update_ha_state() diff --git a/homeassistant/components/vera/light.py b/homeassistant/components/vera/light.py index 84f36fe3877..47d2d039d2a 100644 --- a/homeassistant/components/vera/light.py +++ b/homeassistant/components/vera/light.py @@ -1,6 +1,8 @@ """Support for Vera lights.""" import logging -from typing import Callable, List +from typing import Any, Callable, List, Optional, Tuple + +import pyvera as veraApi from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -37,10 +39,12 @@ async def async_setup_entry( ) -class VeraLight(VeraDevice, LightEntity): +class VeraLight(VeraDevice[veraApi.VeraDimmer], LightEntity): """Representation of a Vera Light, including dimmable.""" - def __init__(self, vera_device, controller_data: ControllerData): + def __init__( + self, vera_device: veraApi.VeraDimmer, controller_data: ControllerData + ): """Initialize the light.""" self._state = False self._color = None @@ -49,23 +53,23 @@ class VeraLight(VeraDevice, LightEntity): self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property - def brightness(self): + def brightness(self) -> Optional[int]: """Return the brightness of the light.""" return self._brightness @property - def hs_color(self): + def hs_color(self) -> Optional[Tuple[float, float]]: """Return the color of the light.""" return self._color @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" if self._color: return SUPPORT_BRIGHTNESS | SUPPORT_COLOR return SUPPORT_BRIGHTNESS - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_HS_COLOR in kwargs and self._color: rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) @@ -78,18 +82,18 @@ class VeraLight(VeraDevice, LightEntity): self._state = True self.schedule_update_ha_state(True) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any): """Turn the light off.""" self.vera_device.switch_off() self._state = False self.schedule_update_ha_state() @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._state - def update(self): + def update(self) -> None: """Call to update state.""" self._state = self.vera_device.is_switched_on() if self.vera_device.is_dimmable: diff --git a/homeassistant/components/vera/lock.py b/homeassistant/components/vera/lock.py index 6a1158d18c4..46f8c6f189e 100644 --- a/homeassistant/components/vera/lock.py +++ b/homeassistant/components/vera/lock.py @@ -1,6 +1,8 @@ """Support for Vera locks.""" import logging -from typing import Callable, List +from typing import Any, Callable, Dict, List, Optional + +import pyvera as veraApi from homeassistant.components.lock import ( DOMAIN as PLATFORM_DOMAIN, @@ -36,32 +38,32 @@ async def async_setup_entry( ) -class VeraLock(VeraDevice, LockEntity): +class VeraLock(VeraDevice[veraApi.VeraLock], LockEntity): """Representation of a Vera lock.""" - def __init__(self, vera_device, controller_data: ControllerData): + def __init__(self, vera_device: veraApi.VeraLock, controller_data: ControllerData): """Initialize the Vera device.""" self._state = None VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) - def lock(self, **kwargs): + def lock(self, **kwargs: Any) -> None: """Lock the device.""" self.vera_device.lock() self._state = STATE_LOCKED - def unlock(self, **kwargs): + def unlock(self, **kwargs: Any) -> None: """Unlock the device.""" self.vera_device.unlock() self._state = STATE_UNLOCKED @property - def is_locked(self): + def is_locked(self) -> Optional[bool]: """Return true if device is on.""" return self._state == STATE_LOCKED @property - def device_state_attributes(self): + def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Who unlocked the lock and did a low battery alert fire. Reports on the previous poll cycle. @@ -78,7 +80,7 @@ class VeraLock(VeraDevice, LockEntity): return data @property - def changed_by(self): + def changed_by(self) -> Optional[str]: """Who unlocked the lock. Reports on the previous poll cycle. @@ -89,7 +91,7 @@ class VeraLock(VeraDevice, LockEntity): return last_user[0] return None - def update(self): + def update(self) -> None: """Update state by the Vera device callback.""" self._state = ( STATE_LOCKED if self.vera_device.is_locked(True) else STATE_UNLOCKED diff --git a/homeassistant/components/vera/scene.py b/homeassistant/components/vera/scene.py index c12f07c15af..8bd4473e1c8 100644 --- a/homeassistant/components/vera/scene.py +++ b/homeassistant/components/vera/scene.py @@ -1,6 +1,8 @@ """Support for Vera scenes.""" import logging -from typing import Any, Callable, List +from typing import Any, Callable, Dict, List, Optional + +import pyvera as veraApi from homeassistant.components.scene import Scene from homeassistant.config_entries import ConfigEntry @@ -29,7 +31,7 @@ async def async_setup_entry( class VeraScene(Scene): """Representation of a Vera scene entity.""" - def __init__(self, vera_scene, controller_data: ControllerData): + def __init__(self, vera_scene: veraApi.VeraScene, controller_data: ControllerData): """Initialize the scene.""" self.vera_scene = vera_scene self.controller = controller_data.controller @@ -40,7 +42,7 @@ class VeraScene(Scene): slugify(vera_scene.name), vera_scene.scene_id ) - def update(self): + def update(self) -> None: """Update the scene status.""" self.vera_scene.refresh() @@ -49,11 +51,11 @@ class VeraScene(Scene): self.vera_scene.activate() @property - def name(self): + def name(self) -> str: """Return the name of the scene.""" return self._name @property - def device_state_attributes(self): + def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return the state attributes of the scene.""" return {"vera_scene_id": self.vera_scene.vera_scene_id} diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index 697af6f4562..0c51094fbc0 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -1,7 +1,7 @@ """Support for Vera sensors.""" from datetime import timedelta import logging -from typing import Callable, List +from typing import Callable, List, Optional, cast import pyvera as veraApi @@ -35,10 +35,12 @@ async def async_setup_entry( ) -class VeraSensor(VeraDevice, Entity): +class VeraSensor(VeraDevice[veraApi.VeraSensor], Entity): """Representation of a Vera Sensor.""" - def __init__(self, vera_device, controller_data: ControllerData): + def __init__( + self, vera_device: veraApi.VeraSensor, controller_data: ControllerData + ): """Initialize the sensor.""" self.current_value = None self._temperature_units = None @@ -47,12 +49,12 @@ class VeraSensor(VeraDevice, Entity): self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property - def state(self): + def state(self) -> str: """Return the name of the sensor.""" return self.current_value @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> Optional[str]: """Return the unit of measurement of this entity, if any.""" if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR: @@ -66,7 +68,7 @@ class VeraSensor(VeraDevice, Entity): if self.vera_device.category == veraApi.CATEGORY_POWER_METER: return "watts" - def update(self): + def update(self) -> None: """Update the state.""" if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR: @@ -86,8 +88,9 @@ class VeraSensor(VeraDevice, Entity): elif self.vera_device.category == veraApi.CATEGORY_HUMIDITY_SENSOR: self.current_value = self.vera_device.humidity elif self.vera_device.category == veraApi.CATEGORY_SCENE_CONTROLLER: - value = self.vera_device.get_last_scene_id(True) - time = self.vera_device.get_last_scene_time(True) + controller = cast(veraApi.VeraSceneController, self.vera_device) + value = controller.get_last_scene_id(True) + time = controller.get_last_scene_time(True) if time == self.last_changed_time: self.current_value = None else: diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py index 9e5af432ce8..9e8360bf673 100644 --- a/homeassistant/components/vera/switch.py +++ b/homeassistant/components/vera/switch.py @@ -1,6 +1,8 @@ """Support for Vera switches.""" import logging -from typing import Callable, List +from typing import Any, Callable, List, Optional + +import pyvera as veraApi from homeassistant.components.switch import ( DOMAIN as PLATFORM_DOMAIN, @@ -33,39 +35,41 @@ async def async_setup_entry( ) -class VeraSwitch(VeraDevice, SwitchEntity): +class VeraSwitch(VeraDevice[veraApi.VeraSwitch], SwitchEntity): """Representation of a Vera Switch.""" - def __init__(self, vera_device, controller_data: ControllerData): + def __init__( + self, vera_device: veraApi.VeraSwitch, controller_data: ControllerData + ): """Initialize the Vera device.""" self._state = False VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn device on.""" self.vera_device.switch_on() self._state = True self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn device off.""" self.vera_device.switch_off() self._state = False self.schedule_update_ha_state() @property - def current_power_w(self): + def current_power_w(self) -> Optional[float]: """Return the current power usage in W.""" power = self.vera_device.power if power: return convert(power, float, 0.0) @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._state - def update(self): + def update(self) -> None: """Update device state.""" self._state = self.vera_device.is_switched_on() From f6584c18665bab17d54ac418581918e97c100c45 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 17 Sep 2020 00:08:34 +0000 Subject: [PATCH 204/514] [ci skip] Translation update --- .../alarmdecoder/translations/ca.json | 2 +- .../alarmdecoder/translations/it.json | 74 +++++++++++++++++++ .../alarmdecoder/translations/no.json | 2 +- .../alarmdecoder/translations/zh-Hant.json | 2 +- .../components/august/translations/ca.json | 3 +- .../components/august/translations/en.json | 56 +++++++------- .../components/august/translations/it.json | 3 +- .../components/august/translations/ru.json | 3 +- .../homekit_controller/translations/it.json | 31 ++++++-- .../components/rpi_power/translations/it.json | 14 ++++ .../synology_dsm/translations/it.json | 3 +- 11 files changed, 153 insertions(+), 40 deletions(-) create mode 100644 homeassistant/components/alarmdecoder/translations/it.json create mode 100644 homeassistant/components/rpi_power/translations/it.json diff --git a/homeassistant/components/alarmdecoder/translations/ca.json b/homeassistant/components/alarmdecoder/translations/ca.json index 421ebb20a21..3042a991c5f 100644 --- a/homeassistant/components/alarmdecoder/translations/ca.json +++ b/homeassistant/components/alarmdecoder/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositiu AlarmDecoder ja est\u00e0 configurat." + "already_configured": "El dispositiu ja est\u00e0 configurat" }, "create_entry": { "default": "S'ha connectat correctament amb AlarmDecoder." diff --git a/homeassistant/components/alarmdecoder/translations/it.json b/homeassistant/components/alarmdecoder/translations/it.json new file mode 100644 index 00000000000..ca5bf39cefd --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/it.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "create_entry": { + "default": "Collegato con successo ad AlarmDecoder." + }, + "error": { + "service_unavailable": "Impossibile connettersi" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Velocit\u00e0 di trasmissione del dispositivo", + "device_path": "Percorso del dispositivo", + "host": "Host", + "port": "Porta" + }, + "title": "Configurare le impostazioni di connessione" + }, + "user": { + "data": { + "protocol": "Protocollo" + }, + "title": "Scegliere il protocollo AlarmDecoder" + } + } + }, + "options": { + "error": { + "int": "Il campo sottostante deve essere un numero intero.", + "loop_range": "Il Ciclo RF deve essere un numero intero compreso tra 1 e 4.", + "loop_rfid": "Il Ciclo RF non pu\u00f2 essere utilizzato senza il Seriale RF ", + "relay_inclusive": "L'indirizzo del rel\u00e8 e il canale del rel\u00e8 sono codipendenti e devono essere inclusi insieme." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Modalit\u00e0 notturna alternativa", + "auto_bypass": "Bypass automatico all'attivazione", + "code_arm_required": "Codice richiesto per l'attivazione" + }, + "title": "Configurare AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Modifica" + }, + "description": "Cosa vorresti modificare?", + "title": "Configurare AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "Ciclo RF", + "zone_name": "Nome zona", + "zone_relayaddr": "Indirizzo rel\u00e8", + "zone_relaychan": "Canale rel\u00e8", + "zone_rfid": "Seriale RF", + "zone_type": "Tipo di zona" + }, + "description": "Immettere i dettagli per la zona {zone_number}. Per eliminare la zona {zone_number}, lasciare vuoto il campo Nome zona.", + "title": "Configurare AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "Numero di zona" + }, + "description": "Immettere il numero di zona che si desidera aggiungere, modificare o rimuovere.", + "title": "Configurare AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/no.json b/homeassistant/components/alarmdecoder/translations/no.json index a3513ef1c18..36c5f21c60c 100644 --- a/homeassistant/components/alarmdecoder/translations/no.json +++ b/homeassistant/components/alarmdecoder/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "AlarmDecoder-enheten er allerede konfigurert." + "already_configured": "Enheten er allerede konfigurert" }, "create_entry": { "default": "Vellykket koblet til AlarmDecoder." diff --git a/homeassistant/components/alarmdecoder/translations/zh-Hant.json b/homeassistant/components/alarmdecoder/translations/zh-Hant.json index cae94eb64ef..4caf58203c8 100644 --- a/homeassistant/components/alarmdecoder/translations/zh-Hant.json +++ b/homeassistant/components/alarmdecoder/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "AlarmDecoder \u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "create_entry": { "default": "\u6210\u529f\u9023\u7dda\u81f3 AlarmDecoder\u3002" diff --git a/homeassistant/components/august/translations/ca.json b/homeassistant/components/august/translations/ca.json index 4f8f9cebe63..e8299249d73 100644 --- a/homeassistant/components/august/translations/ca.json +++ b/homeassistant/components/august/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat" + "already_configured": "El compte ja ha estat configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", diff --git a/homeassistant/components/august/translations/en.json b/homeassistant/components/august/translations/en.json index 254e8146984..08d09e12095 100644 --- a/homeassistant/components/august/translations/en.json +++ b/homeassistant/components/august/translations/en.json @@ -1,32 +1,32 @@ { - "config": { - "error": { - "unknown": "Unexpected error", - "cannot_connect": "Failed to connect, please try again", - "invalid_auth": "Invalid authentication" - }, - "abort": { - "already_configured": "Account is already configured", - "reauth_successful": "Re-authentication was successful" - }, - "step": { - "validation": { - "title": "Two factor authentication", - "data": { - "code": "Verification code" + "config": { + "abort": { + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" }, - "description": "Please check your {login_method} ({username}) and enter the verification code below" - }, - "user": { - "description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.", - "data": { - "timeout": "Timeout (seconds)", - "password": "[%key:common::config_flow::data::password%]", - "username": "[%key:common::config_flow::data::username%]", - "login_method": "Login Method" + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" }, - "title": "Setup an August account" - } + "step": { + "user": { + "data": { + "login_method": "Login Method", + "password": "Password", + "timeout": "Timeout (seconds)", + "username": "Username" + }, + "description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.", + "title": "Setup an August account" + }, + "validation": { + "data": { + "code": "Verification code" + }, + "description": "Please check your {login_method} ({username}) and enter the verification code below", + "title": "Two factor authentication" + } + } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/it.json b/homeassistant/components/august/translations/it.json index 3a5f2676acd..8f88b34e5e4 100644 --- a/homeassistant/components/august/translations/it.json +++ b/homeassistant/components/august/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "L'account \u00e8 gi\u00e0 configurato" + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La riautenticazione ha avuto successo" }, "error": { "cannot_connect": "Impossibile connettersi, si prega di riprovare.", diff --git a/homeassistant/components/august/translations/ru.json b/homeassistant/components/august/translations/ru.json index 9a49caed547..516b978a6e5 100644 --- a/homeassistant/components/august/translations/ru.json +++ b/homeassistant/components/august/translations/ru.json @@ -1,7 +1,8 @@ { "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 \u0432 Home Assistant." + "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": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "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/homekit_controller/translations/it.json b/homeassistant/components/homekit_controller/translations/it.json index f07fd6df8b0..1867d51d7df 100644 --- a/homeassistant/components/homekit_controller/translations/it.json +++ b/homeassistant/components/homekit_controller/translations/it.json @@ -7,6 +7,7 @@ "already_paired": "Questo accessorio \u00e8 gi\u00e0 associato a un altro dispositivo. Si prega di resettare l'accessorio e riprovare.", "ignored_model": "Il supporto di HomeKit per questo modello \u00e8 bloccato poich\u00e9 \u00e8 disponibile un'integrazione nativa con pi\u00f9 funzionalit\u00e0.", "invalid_config_entry": "Questo dispositivo viene visualizzato come pronto per l'associazione, ma c'\u00e8 gi\u00e0 una voce di configurazione in conflitto in Home Assistant che prima deve essere rimossa.", + "invalid_properties": "Propriet\u00e0 non valide annunciate dal dispositivo.", "no_devices": "Non \u00e8 stato possibile trovare dispositivi non associati" }, "error": { @@ -19,7 +20,7 @@ "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}", + "flow_title": "{name} tramite il Protocollo degli Accessori HomeKit", "step": { "busy_error": { "description": "Interrompere l'associazione su tutti i controller o provare a riavviare il dispositivo, quindi continuare a riprendere l'associazione.", @@ -33,8 +34,8 @@ "data": { "pairing_code": "Codice di abbinamento" }, - "description": "Immettere il codice di abbinamento HomeKit (nel formato XXX-XX-XXX) per utilizzare questo accessorio", - "title": "Abbina con accessorio HomeKit" + "description": "Il controller HomeKit comunica con {name} sulla rete locale utilizzando una connessione crittografata sicura senza un controller HomeKit separato o iCloud. Inserisci il tuo codice di associazione HomeKit (nel formato XXX-XX-XXX) per utilizzare questo accessorio. Questo codice si trova solitamente sul dispositivo stesso o nella confezione.", + "title": "Associazione con un dispositivo tramite il Protocollo degli Accessori 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.", @@ -48,10 +49,30 @@ "data": { "device": "Dispositivo" }, - "description": "Selezionare il dispositivo che si desidera abbinare", - "title": "Abbina con accessorio HomeKit" + "description": "Il controller HomeKit comunica sulla rete locale utilizzando una connessione crittografata sicura senza un controller HomeKit separato o iCloud. Seleziona il dispositivo che desideri associare:", + "title": "Selezione del dispositivo" } } }, + "device_automation": { + "trigger_subtype": { + "button1": "Pulsante 1", + "button10": "Pulsante 10", + "button2": "Pulsante 2", + "button3": "Pulsante 3", + "button4": "Pulsante 4", + "button5": "Pulsante 5", + "button6": "Pulsante 6", + "button7": "Pulsante 7", + "button8": "Pulsante 8", + "button9": "Pulsante 9", + "doorbell": "Campanello" + }, + "trigger_type": { + "double_press": "\"{subtype}\" premuto due volte", + "long_press": "\"{subtype}\" premuto e tenuto premuto", + "single_press": "\"{subtype}\" premuto" + } + }, "title": "Controller HomeKit" } \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/it.json b/homeassistant/components/rpi_power/translations/it.json new file mode 100644 index 00000000000..4e7a14d05e8 --- /dev/null +++ b/homeassistant/components/rpi_power/translations/it.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Impossibile trovare la classe di sistema necessaria per questo componente, assicurarsi che il kernel sia recente e che l'hardware sia supportato", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "step": { + "confirm": { + "description": "Vuoi iniziare la configurazione?" + } + } + }, + "title": "Controllo alimentazione Raspberry Pi" +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/it.json b/homeassistant/components/synology_dsm/translations/it.json index 1bb6dc026d8..933f9760b5b 100644 --- a/homeassistant/components/synology_dsm/translations/it.json +++ b/homeassistant/components/synology_dsm/translations/it.json @@ -44,7 +44,8 @@ "step": { "init": { "data": { - "scan_interval": "Minuti tra una scansione e l'altra" + "scan_interval": "Minuti tra una scansione e l'altra", + "timeout": "Timeout (in secondi)" } } } From 271ffac4a9fde8456f68016e44be8a6edb5f4c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 17 Sep 2020 08:20:00 +0300 Subject: [PATCH 205/514] Fix static/class async mocks on Python 3.8.0 and .1 (#40147) * forked_daapd * shelly * simplipy --- .../forked_daapd/test_config_flow.py | 8 ++-- tests/components/shelly/test_config_flow.py | 38 +++++++++++-------- .../components/simplisafe/test_config_flow.py | 28 +++++++++----- 3 files changed, 47 insertions(+), 27 deletions(-) diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index 181c963ee4f..f655e727667 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -16,7 +16,7 @@ from homeassistant.config_entries import ( ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT -from tests.async_mock import patch +from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry SAMPLE_CONFIG = { @@ -69,7 +69,8 @@ async def test_show_form(hass): async def test_config_flow(hass, config_entry): """Test that the user step works.""" with patch( - "homeassistant.components.forked_daapd.config_flow.ForkedDaapdAPI.test_connection" + "homeassistant.components.forked_daapd.config_flow.ForkedDaapdAPI.test_connection", + new=AsyncMock(), ) as mock_test_connection, patch( "homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI.get_request", autospec=True, @@ -119,7 +120,8 @@ async def test_zeroconf_updates_title(hass, config_entry): async def test_config_flow_no_websocket(hass, config_entry): """Test config flow setup without websocket enabled on server.""" with patch( - "homeassistant.components.forked_daapd.config_flow.ForkedDaapdAPI.test_connection" + "homeassistant.components.forked_daapd.config_flow.ForkedDaapdAPI.test_connection", + new=AsyncMock(), ) as mock_test_connection: # test invalid config data mock_test_connection.return_value = ["websocket_not_enabled"] diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 93192e89df3..366b52ca4e7 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -25,9 +25,11 @@ async def test_form(hass): 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"}}, + new=AsyncMock( + return_value=Mock( + shutdown=AsyncMock(), + settings={"name": "Test name", "device": {"mac": "test-mac"}}, + ) ), ), patch( "homeassistant.components.shelly.async_setup", return_value=True @@ -72,9 +74,11 @@ async def test_form_auth(hass): with patch( "aioshelly.Device.create", - return_value=Mock( - shutdown=AsyncMock(), - settings={"name": "Test name", "device": {"mac": "test-mac"}}, + new=AsyncMock( + return_value=Mock( + shutdown=AsyncMock(), + settings={"name": "Test name", "device": {"mac": "test-mac"}}, + ) ), ), patch( "homeassistant.components.shelly.async_setup", return_value=True @@ -136,7 +140,7 @@ async def test_form_errors_test_connection(hass, error): "aioshelly.get_info", return_value={"mac": "test-mac", "auth": False} ), patch( "aioshelly.Device.create", - side_effect=exc, + new=AsyncMock(side_effect=exc), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -199,7 +203,7 @@ async def test_form_auth_errors_test_connection(hass, error): with patch( "aioshelly.Device.create", - side_effect=exc, + new=AsyncMock(side_effect=exc), ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], @@ -227,9 +231,11 @@ async def test_zeroconf(hass): with patch( "aioshelly.Device.create", - return_value=Mock( - shutdown=AsyncMock(), - settings={"name": "Test name", "device": {"mac": "test-mac"}}, + new=AsyncMock( + return_value=Mock( + shutdown=AsyncMock(), + settings={"name": "Test name", "device": {"mac": "test-mac"}}, + ) ), ), patch( "homeassistant.components.shelly.async_setup", return_value=True @@ -274,7 +280,7 @@ async def test_zeroconf_confirm_error(hass, error): with patch( "aioshelly.Device.create", - side_effect=exc, + new=AsyncMock(side_effect=exc), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -349,9 +355,11 @@ async def test_zeroconf_require_auth(hass): with patch( "aioshelly.Device.create", - return_value=Mock( - shutdown=AsyncMock(), - settings={"name": "Test name", "device": {"mac": "test-mac"}}, + new=AsyncMock( + return_value=Mock( + shutdown=AsyncMock(), + settings={"name": "Test name", "device": {"mac": "test-mac"}}, + ) ), ), patch( "homeassistant.components.shelly.async_setup", return_value=True diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 86cd0fc384c..e3d0b0479c4 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -10,7 +10,7 @@ from homeassistant.components.simplisafe import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME -from tests.async_mock import MagicMock, PropertyMock, patch +from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch from tests.common import MockConfigEntry @@ -49,7 +49,7 @@ async def test_invalid_credentials(hass): with patch( "simplipy.API.login_via_credentials", - side_effect=InvalidCredentialsError, + new=AsyncMock(side_effect=InvalidCredentialsError), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf @@ -105,7 +105,9 @@ async def test_step_import(hass): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch("simplipy.API.login_via_credentials", return_value=mock_api()): + ), patch( + "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api()) + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf ) @@ -140,7 +142,9 @@ async def test_step_reauth(hass): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch("simplipy.API.login_via_credentials", return_value=mock_api()): + ), patch( + "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api()) + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} ) @@ -160,7 +164,9 @@ async def test_step_user(hass): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch("simplipy.API.login_via_credentials", return_value=mock_api()): + ), patch( + "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api()) + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf ) @@ -183,7 +189,8 @@ async def test_step_user_mfa(hass): } with patch( - "simplipy.API.login_via_credentials", side_effect=PendingAuthorizationError + "simplipy.API.login_via_credentials", + new=AsyncMock(side_effect=PendingAuthorizationError), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf @@ -191,7 +198,8 @@ async def test_step_user_mfa(hass): assert result["step_id"] == "mfa" with patch( - "simplipy.API.login_via_credentials", side_effect=PendingAuthorizationError + "simplipy.API.login_via_credentials", + new=AsyncMock(side_effect=PendingAuthorizationError), ): # Simulate the user pressing the MFA submit button without having clicked # the link in the MFA email: @@ -202,7 +210,9 @@ async def test_step_user_mfa(hass): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch("simplipy.API.login_via_credentials", return_value=mock_api()): + ), patch( + "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api()) + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) @@ -222,7 +232,7 @@ async def test_unknown_error(hass): with patch( "simplipy.API.login_via_credentials", - side_effect=SimplipyError, + new=AsyncMock(side_effect=SimplipyError), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf From 70173488a88c3609d9899479b0e07dc1ee48c6f1 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Wed, 16 Sep 2020 22:58:51 -0700 Subject: [PATCH 206/514] Add config support to zoneminder integration (#37060) * Add config support to zoneminder integration. * Fixing spelling issue. Adding self to maintainers. Updating config flows generated file. * Maintain zoneminder functionality without breaking changes. * Addressing lint feedback. Updating code owners. * Using non-blocking calls. * Adding tests package file. * Update service description. Co-authored-by: Rohan Kapoor * Resolving conflicts in requirements file. * Resolving more conflicts. * Addressing PR feedback. * Merging from dev. Co-authored-by: Rohan Kapoor --- .coveragerc | 1 - CODEOWNERS | 2 +- .../components/zoneminder/__init__.py | 184 ++++++++++++------ .../components/zoneminder/binary_sensor.py | 34 +++- homeassistant/components/zoneminder/camera.py | 41 ++-- homeassistant/components/zoneminder/common.py | 110 +++++++++++ .../components/zoneminder/config_flow.py | 99 ++++++++++ homeassistant/components/zoneminder/const.py | 14 ++ .../components/zoneminder/manifest.json | 3 +- homeassistant/components/zoneminder/sensor.py | 73 +++++-- .../components/zoneminder/services.yaml | 7 +- .../components/zoneminder/strings.json | 28 +++ homeassistant/components/zoneminder/switch.py | 70 +++++-- homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/zoneminder/__init__.py | 1 + .../zoneminder/test_binary_sensor.py | 65 +++++++ tests/components/zoneminder/test_camera.py | 89 +++++++++ .../components/zoneminder/test_config_flow.py | 119 +++++++++++ tests/components/zoneminder/test_init.py | 122 ++++++++++++ tests/components/zoneminder/test_sensor.py | 167 ++++++++++++++++ tests/components/zoneminder/test_switch.py | 126 ++++++++++++ 22 files changed, 1239 insertions(+), 120 deletions(-) create mode 100644 homeassistant/components/zoneminder/common.py create mode 100644 homeassistant/components/zoneminder/config_flow.py create mode 100644 homeassistant/components/zoneminder/const.py create mode 100644 homeassistant/components/zoneminder/strings.json create mode 100644 tests/components/zoneminder/__init__.py create mode 100644 tests/components/zoneminder/test_binary_sensor.py create mode 100644 tests/components/zoneminder/test_camera.py create mode 100644 tests/components/zoneminder/test_config_flow.py create mode 100644 tests/components/zoneminder/test_init.py create mode 100644 tests/components/zoneminder/test_sensor.py create mode 100644 tests/components/zoneminder/test_switch.py diff --git a/.coveragerc b/.coveragerc index c95f47815e8..e35695fb8b2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1028,7 +1028,6 @@ omit = homeassistant/components/zhong_hong/climate.py homeassistant/components/xbee/* homeassistant/components/ziggo_mediabox_xl/media_player.py - homeassistant/components/zoneminder/* homeassistant/components/supla/* homeassistant/components/zwave/util.py homeassistant/components/ozw/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index ec55887e883..8804380fc72 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -504,7 +504,7 @@ homeassistant/components/zeroconf/* @Kane610 homeassistant/components/zerproc/* @emlove homeassistant/components/zha/* @dmulcahey @adminiuga homeassistant/components/zone/* @home-assistant/core -homeassistant/components/zoneminder/* @rohankapoorcom +homeassistant/components/zoneminder/* @rohankapoorcom @vangorra homeassistant/components/zwave/* @home-assistant/z-wave # Individual files diff --git a/homeassistant/components/zoneminder/__init__.py b/homeassistant/components/zoneminder/__init__.py index c631406b0e3..92186b7a0b5 100644 --- a/homeassistant/components/zoneminder/__init__.py +++ b/homeassistant/components/zoneminder/__init__.py @@ -2,97 +2,169 @@ import logging import voluptuous as vol -from zoneminder.zm import ZoneMinder +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +import homeassistant.config_entries as config_entries +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ID, ATTR_NAME, CONF_HOST, CONF_PASSWORD, CONF_PATH, + CONF_PLATFORM, + CONF_SOURCE, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, ) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import async_load_platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv + +from . import const +from .common import ( + ClientAvailabilityResult, + async_test_client_availability, + create_client_from_config, + del_client_from_data, + get_client_from_data, + is_client_in_data, + set_client_to_data, + set_platform_configs, +) _LOGGER = logging.getLogger(__name__) - -CONF_PATH_ZMS = "path_zms" - -DEFAULT_PATH = "/zm/" -DEFAULT_PATH_ZMS = "/zm/cgi-bin/nph-zms" -DEFAULT_SSL = False -DEFAULT_TIMEOUT = 10 -DEFAULT_VERIFY_SSL = True -DOMAIN = "zoneminder" +PLATFORM_DOMAINS = tuple( + [BINARY_SENSOR_DOMAIN, CAMERA_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN] +) HOST_CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, - vol.Optional(CONF_PATH_ZMS, default=DEFAULT_PATH_ZMS): cv.string, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_PATH, default=const.DEFAULT_PATH): cv.string, + vol.Optional(const.CONF_PATH_ZMS, default=const.DEFAULT_PATH_ZMS): cv.string, + vol.Optional(CONF_SSL, default=const.DEFAULT_SSL): cv.boolean, vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=const.DEFAULT_VERIFY_SSL): cv.boolean, } ) -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.All(cv.ensure_list, [HOST_CONFIG_SCHEMA])}, extra=vol.ALLOW_EXTRA +CONFIG_SCHEMA = vol.All( + cv.deprecated(const.DOMAIN, invalidation_version="0.118"), + vol.Schema( + {const.DOMAIN: vol.All(cv.ensure_list, [HOST_CONFIG_SCHEMA])}, + extra=vol.ALLOW_EXTRA, + ), ) -SERVICE_SET_RUN_STATE = "set_run_state" SET_RUN_STATE_SCHEMA = vol.Schema( {vol.Required(ATTR_ID): cv.string, vol.Required(ATTR_NAME): cv.string} ) -def setup(hass, config): +async def async_setup(hass: HomeAssistant, base_config: dict): """Set up the ZoneMinder component.""" - hass.data[DOMAIN] = {} + # Collect the platform specific configs. It's necessary to collect these configs + # here instead of the platform's setup_platform function because the invocation order + # of setup_platform and async_setup_entry is not consistent. + set_platform_configs( + hass, + SENSOR_DOMAIN, + [ + platform_config + for platform_config in base_config.get(SENSOR_DOMAIN, []) + if platform_config[CONF_PLATFORM] == const.DOMAIN + ], + ) + set_platform_configs( + hass, + SWITCH_DOMAIN, + [ + platform_config + for platform_config in base_config.get(SWITCH_DOMAIN, []) + if platform_config[CONF_PLATFORM] == const.DOMAIN + ], + ) - success = True + config = base_config.get(const.DOMAIN) - for conf in config[DOMAIN]: - protocol = "https" if conf[CONF_SSL] else "http" + if not config: + return True - host_name = conf[CONF_HOST] - server_origin = f"{protocol}://{host_name}" - zm_client = ZoneMinder( - server_origin, - conf.get(CONF_USERNAME), - conf.get(CONF_PASSWORD), - conf.get(CONF_PATH), - conf.get(CONF_PATH_ZMS), - conf.get(CONF_VERIFY_SSL), - ) - hass.data[DOMAIN][host_name] = zm_client - - success = zm_client.login() and success - - def set_active_state(call): - """Set the ZoneMinder run state to the given state name.""" - zm_id = call.data[ATTR_ID] - state_name = call.data[ATTR_NAME] - if zm_id not in hass.data[DOMAIN]: - _LOGGER.error("Invalid ZoneMinder host provided: %s", zm_id) - if not hass.data[DOMAIN][zm_id].set_active_state(state_name): - _LOGGER.error( - "Unable to change ZoneMinder state. Host: %s, state: %s", - zm_id, - state_name, + for config_item in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + const.DOMAIN, + context={CONF_SOURCE: config_entries.SOURCE_IMPORT}, + data=config_item, ) + ) - hass.services.register( - DOMAIN, SERVICE_SET_RUN_STATE, set_active_state, schema=SET_RUN_STATE_SCHEMA - ) + return True - hass.async_create_task( - async_load_platform(hass, "binary_sensor", DOMAIN, {}, config) - ) - return success +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up Zoneminder config entry.""" + zm_client = create_client_from_config(config_entry.data) + + result = await async_test_client_availability(hass, zm_client) + if result != ClientAvailabilityResult.AVAILABLE: + raise ConfigEntryNotReady + + set_client_to_data(hass, config_entry.unique_id, zm_client) + + for platform_domain in PLATFORM_DOMAINS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, platform_domain) + ) + + if not hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE): + + @callback + def set_active_state(call): + """Set the ZoneMinder run state to the given state name.""" + zm_id = call.data[ATTR_ID] + state_name = call.data[ATTR_NAME] + if not is_client_in_data(hass, zm_id): + _LOGGER.error("Invalid ZoneMinder host provided: %s", zm_id) + return + + if not get_client_from_data(hass, zm_id).set_active_state(state_name): + _LOGGER.error( + "Unable to change ZoneMinder state. Host: %s, state: %s", + zm_id, + state_name, + ) + + hass.services.async_register( + const.DOMAIN, + const.SERVICE_SET_RUN_STATE, + set_active_state, + schema=SET_RUN_STATE_SCHEMA, + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload Zoneminder config entry.""" + for platform_domain in PLATFORM_DOMAINS: + hass.async_create_task( + hass.config_entries.async_forward_entry_unload( + config_entry, platform_domain + ) + ) + + # If this is the last config to exist, remove the service too. + if len(hass.config_entries.async_entries(const.DOMAIN)) <= 1: + hass.services.async_remove(const.DOMAIN, const.SERVICE_SET_RUN_STATE) + + del_client_from_data(hass, config_entry.unique_id) + + return True diff --git a/homeassistant/components/zoneminder/binary_sensor.py b/homeassistant/components/zoneminder/binary_sensor.py index 73d6877ef2d..73f7ce2f4c9 100644 --- a/homeassistant/components/zoneminder/binary_sensor.py +++ b/homeassistant/components/zoneminder/binary_sensor.py @@ -1,29 +1,43 @@ """Support for ZoneMinder binary sensors.""" +from typing import Callable, List, Optional + +from zoneminder.zm import ZoneMinder + from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, BinarySensorEntity, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity -from . import DOMAIN as ZONEMINDER_DOMAIN +from .common import get_client_from_data -async def async_setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the ZoneMinder binary sensor platform.""" - sensors = [] - for host_name, zm_client in hass.data[ZONEMINDER_DOMAIN].items(): - sensors.append(ZMAvailabilitySensor(host_name, zm_client)) - add_entities(sensors) - return True +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], Optional[bool]], None], +) -> None: + """Set up the sensor config entry.""" + zm_client = get_client_from_data(hass, config_entry.unique_id) + async_add_entities([ZMAvailabilitySensor(zm_client, config_entry)]) class ZMAvailabilitySensor(BinarySensorEntity): """Representation of the availability of ZoneMinder as a binary sensor.""" - def __init__(self, host_name, client): + def __init__(self, client: ZoneMinder, config_entry: ConfigEntry): """Initialize availability sensor.""" self._state = None - self._name = host_name + self._name = config_entry.unique_id self._client = client + self._config_entry = config_entry + + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return f"{self._config_entry.unique_id}_availability" @property def name(self): diff --git a/homeassistant/components/zoneminder/camera.py b/homeassistant/components/zoneminder/camera.py index 6144fe11226..c4ef1b14772 100644 --- a/homeassistant/components/zoneminder/camera.py +++ b/homeassistant/components/zoneminder/camera.py @@ -1,5 +1,8 @@ """Support for ZoneMinder camera streaming.""" import logging +from typing import Callable, List, Optional + +from zoneminder.monitor import Monitor from homeassistant.components.mjpeg.camera import ( CONF_MJPEG_URL, @@ -7,9 +10,12 @@ from homeassistant.components.mjpeg.camera import ( MjpegCamera, filter_urllib3_logging, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_VERIFY_SSL +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity -from . import DOMAIN as ZONEMINDER_DOMAIN +from .common import get_client_from_data _LOGGER = logging.getLogger(__name__) @@ -17,23 +23,28 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ZoneMinder cameras.""" filter_urllib3_logging() - cameras = [] - for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): - monitors = zm_client.get_monitors() - if not monitors: - _LOGGER.warning("Could not fetch monitors from ZoneMinder host: %s") - return - for monitor in monitors: - _LOGGER.info("Initializing camera %s", monitor.id) - cameras.append(ZoneMinderCamera(monitor, zm_client.verify_ssl)) - add_entities(cameras) + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], Optional[bool]], None], +) -> None: + """Set up the sensor config entry.""" + zm_client = get_client_from_data(hass, config_entry.unique_id) + + async_add_entities( + [ + ZoneMinderCamera(monitor, zm_client.verify_ssl, config_entry) + for monitor in await hass.async_add_job(zm_client.get_monitors) + ] + ) class ZoneMinderCamera(MjpegCamera): """Representation of a ZoneMinder Monitor Stream.""" - def __init__(self, monitor, verify_ssl): + def __init__(self, monitor: Monitor, verify_ssl: bool, config_entry: ConfigEntry): """Initialize as a subclass of MjpegCamera.""" device_info = { CONF_NAME: monitor.name, @@ -45,6 +56,12 @@ class ZoneMinderCamera(MjpegCamera): self._is_recording = None self._is_available = None self._monitor = monitor + self._config_entry = config_entry + + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return f"{self._config_entry.unique_id}_{self._monitor.id}_camera" @property def should_poll(self): diff --git a/homeassistant/components/zoneminder/common.py b/homeassistant/components/zoneminder/common.py new file mode 100644 index 00000000000..970289ea136 --- /dev/null +++ b/homeassistant/components/zoneminder/common.py @@ -0,0 +1,110 @@ +"""Common code for the ZoneMinder component.""" +from enum import Enum +from typing import List + +import requests +from zoneminder.zm import ZoneMinder + +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PATH, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import HomeAssistant + +from . import const + + +def prime_domain_data(hass: HomeAssistant) -> None: + """Prime the data structures.""" + hass.data.setdefault(const.DOMAIN, {}) + + +def prime_platform_configs(hass: HomeAssistant, domain: str) -> None: + """Prime the data structures.""" + prime_domain_data(hass) + hass.data[const.DOMAIN].setdefault(const.PLATFORM_CONFIGS, {}) + hass.data[const.DOMAIN][const.PLATFORM_CONFIGS].setdefault(domain, []) + + +def set_platform_configs(hass: HomeAssistant, domain: str, configs: List[dict]) -> None: + """Set platform configs.""" + prime_platform_configs(hass, domain) + hass.data[const.DOMAIN][const.PLATFORM_CONFIGS][domain] = configs + + +def get_platform_configs(hass: HomeAssistant, domain: str) -> List[dict]: + """Get platform configs.""" + prime_platform_configs(hass, domain) + return hass.data[const.DOMAIN][const.PLATFORM_CONFIGS][domain] + + +def prime_config_data(hass: HomeAssistant, unique_id: str) -> None: + """Prime the data structures.""" + prime_domain_data(hass) + hass.data[const.DOMAIN].setdefault(const.CONFIG_DATA, {}) + hass.data[const.DOMAIN][const.CONFIG_DATA].setdefault(unique_id, {}) + + +def set_client_to_data(hass: HomeAssistant, unique_id: str, client: ZoneMinder) -> None: + """Put a ZoneMinder client in the Home Assistant data.""" + prime_config_data(hass, unique_id) + hass.data[const.DOMAIN][const.CONFIG_DATA][unique_id][const.API_CLIENT] = client + + +def is_client_in_data(hass: HomeAssistant, unique_id: str) -> bool: + """Check if ZoneMinder client is in the Home Assistant data.""" + prime_config_data(hass, unique_id) + return const.API_CLIENT in hass.data[const.DOMAIN][const.CONFIG_DATA][unique_id] + + +def get_client_from_data(hass: HomeAssistant, unique_id: str) -> ZoneMinder: + """Get a ZoneMinder client from the Home Assistant data.""" + prime_config_data(hass, unique_id) + return hass.data[const.DOMAIN][const.CONFIG_DATA][unique_id][const.API_CLIENT] + + +def del_client_from_data(hass: HomeAssistant, unique_id: str) -> None: + """Delete a ZoneMinder client from the Home Assistant data.""" + prime_config_data(hass, unique_id) + del hass.data[const.DOMAIN][const.CONFIG_DATA][unique_id][const.API_CLIENT] + + +def create_client_from_config(conf: dict) -> ZoneMinder: + """Create a new ZoneMinder client from a config.""" + protocol = "https" if conf[CONF_SSL] else "http" + + host_name = conf[CONF_HOST] + server_origin = f"{protocol}://{host_name}" + + return ZoneMinder( + server_origin, + conf.get(CONF_USERNAME), + conf.get(CONF_PASSWORD), + conf.get(CONF_PATH), + conf.get(const.CONF_PATH_ZMS), + conf.get(CONF_VERIFY_SSL), + ) + + +class ClientAvailabilityResult(Enum): + """Client availability test result.""" + + AVAILABLE = "available" + ERROR_AUTH_FAIL = "auth_fail" + ERROR_CONNECTION_ERROR = "connection_error" + + +async def async_test_client_availability( + hass: HomeAssistant, client: ZoneMinder +) -> ClientAvailabilityResult: + """Test the availability of a ZoneMinder client.""" + try: + if await hass.async_add_job(client.login): + return ClientAvailabilityResult.AVAILABLE + return ClientAvailabilityResult.ERROR_AUTH_FAIL + except requests.exceptions.ConnectionError: + return ClientAvailabilityResult.ERROR_CONNECTION_ERROR diff --git a/homeassistant/components/zoneminder/config_flow.py b/homeassistant/components/zoneminder/config_flow.py new file mode 100644 index 00000000000..1d50e8c1bb1 --- /dev/null +++ b/homeassistant/components/zoneminder/config_flow.py @@ -0,0 +1,99 @@ +"""ZoneMinder config flow.""" +from urllib.parse import urlparse + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PATH, + CONF_SOURCE, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) + +from .common import ( + ClientAvailabilityResult, + async_test_client_availability, + create_client_from_config, +) +from .const import ( + CONF_PATH_ZMS, + DEFAULT_PATH, + DEFAULT_PATH_ZMS, + DEFAULT_SSL, + DEFAULT_VERIFY_SSL, +) +from .const import DOMAIN # pylint: disable=unused-import + + +class ZoneminderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Flow handler for zoneminder integration.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_import(self, config: dict): + """Handle a flow initialized by import.""" + return await self.async_step_finish( + {**config, **{CONF_SOURCE: config_entries.SOURCE_IMPORT}} + ) + + async def async_step_user(self, user_input: dict = None): + """Handle user step.""" + user_input = user_input or {} + errors = {} + + if user_input: + zm_client = create_client_from_config(user_input) + result = await async_test_client_availability(self.hass, zm_client) + if result == ClientAvailabilityResult.AVAILABLE: + return await self.async_step_finish(user_input) + + errors["base"] = result.value + + return self.async_show_form( + step_id=config_entries.SOURCE_USER, + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=user_input.get(CONF_HOST)): str, + vol.Optional( + CONF_USERNAME, default=user_input.get(CONF_USERNAME) + ): str, + vol.Optional( + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD) + ): str, + vol.Optional( + CONF_PATH, default=user_input.get(CONF_PATH, DEFAULT_PATH) + ): str, + vol.Optional( + CONF_PATH_ZMS, + default=user_input.get(CONF_PATH_ZMS, DEFAULT_PATH_ZMS), + ): str, + vol.Optional( + CONF_SSL, default=user_input.get(CONF_SSL, DEFAULT_SSL) + ): bool, + vol.Optional( + CONF_VERIFY_SSL, + default=user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL), + ): bool, + } + ), + errors=errors, + ) + + async def async_step_finish(self, config: dict): + """Finish config flow.""" + zm_client = create_client_from_config(config) + hostname = urlparse(zm_client.get_zms_url()).hostname + result = await async_test_client_availability(self.hass, zm_client) + + if result != ClientAvailabilityResult.AVAILABLE: + return self.async_abort(reason=str(result.value)) + + await self.async_set_unique_id(hostname) + self._abort_if_unique_id_configured(config) + + return self.async_create_entry(title=hostname, data=config) diff --git a/homeassistant/components/zoneminder/const.py b/homeassistant/components/zoneminder/const.py new file mode 100644 index 00000000000..ad890a1d4d6 --- /dev/null +++ b/homeassistant/components/zoneminder/const.py @@ -0,0 +1,14 @@ +"""Constants for zoneminder component.""" + +CONF_PATH_ZMS = "path_zms" + +DEFAULT_PATH = "/zm/" +DEFAULT_PATH_ZMS = "/zm/cgi-bin/nph-zms" +DEFAULT_SSL = False +DEFAULT_VERIFY_SSL = True +DOMAIN = "zoneminder" +SERVICE_SET_RUN_STATE = "set_run_state" + +PLATFORM_CONFIGS = "platform_configs" +CONFIG_DATA = "config_data" +API_CLIENT = "api_client" diff --git a/homeassistant/components/zoneminder/manifest.json b/homeassistant/components/zoneminder/manifest.json index b3a87510e5a..13bec8c4d9a 100644 --- a/homeassistant/components/zoneminder/manifest.json +++ b/homeassistant/components/zoneminder/manifest.json @@ -1,7 +1,8 @@ { "domain": "zoneminder", "name": "ZoneMinder", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zoneminder", "requirements": ["zm-py==0.4.0"], - "codeowners": ["@rohankapoorcom"] + "codeowners": ["@rohankapoorcom", "@vangorra"] } diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py index 75531e79e13..8605e842813 100644 --- a/homeassistant/components/zoneminder/sensor.py +++ b/homeassistant/components/zoneminder/sensor.py @@ -1,15 +1,19 @@ """Support for ZoneMinder sensors.""" import logging +from typing import Callable, List, Optional import voluptuous as vol -from zoneminder.monitor import TimePeriod +from zoneminder.monitor import Monitor, TimePeriod +from zoneminder.zm import ZoneMinder -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from . import DOMAIN as ZONEMINDER_DOMAIN +from .common import get_client_from_data, get_platform_configs _LOGGER = logging.getLogger(__name__) @@ -37,35 +41,50 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the ZoneMinder sensor platform.""" - include_archived = config.get(CONF_INCLUDE_ARCHIVED) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], Optional[bool]], None], +) -> None: + """Set up the sensor config entry.""" + zm_client = get_client_from_data(hass, config_entry.unique_id) + monitors = await hass.async_add_job(zm_client.get_monitors) + + if not monitors: + _LOGGER.warning("Did not fetch any monitors from ZoneMinder") sensors = [] - for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): - monitors = zm_client.get_monitors() - if not monitors: - _LOGGER.warning("Could not fetch any monitors from ZoneMinder") + for monitor in monitors: + sensors.append(ZMSensorMonitors(monitor, config_entry)) - for monitor in monitors: - sensors.append(ZMSensorMonitors(monitor)) + for config in get_platform_configs(hass, SENSOR_DOMAIN): + include_archived = config.get(CONF_INCLUDE_ARCHIVED) for sensor in config[CONF_MONITORED_CONDITIONS]: - sensors.append(ZMSensorEvents(monitor, include_archived, sensor)) + sensors.append( + ZMSensorEvents(monitor, include_archived, sensor, config_entry) + ) - sensors.append(ZMSensorRunState(zm_client)) - add_entities(sensors) + sensors.append(ZMSensorRunState(zm_client, config_entry)) + + async_add_entities(sensors, True) class ZMSensorMonitors(Entity): """Get the status of each ZoneMinder monitor.""" - def __init__(self, monitor): + def __init__(self, monitor: Monitor, config_entry: ConfigEntry): """Initialize monitor sensor.""" self._monitor = monitor + self._config_entry = config_entry self._state = None self._is_available = None + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return f"{self._config_entry.unique_id}_{self._monitor.id}_status" + @property def name(self): """Return the name of the sensor.""" @@ -94,14 +113,26 @@ class ZMSensorMonitors(Entity): class ZMSensorEvents(Entity): """Get the number of events for each monitor.""" - def __init__(self, monitor, include_archived, sensor_type): + def __init__( + self, + monitor: Monitor, + include_archived: bool, + sensor_type: str, + config_entry: ConfigEntry, + ): """Initialize event sensor.""" self._monitor = monitor self._include_archived = include_archived self.time_period = TimePeriod.get_time_period(sensor_type) + self._config_entry = config_entry self._state = None + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return f"{self._config_entry.unique_id}_{self._monitor.id}_{self.time_period.value}_{self._include_archived}_events" + @property def name(self): """Return the name of the sensor.""" @@ -125,11 +156,17 @@ class ZMSensorEvents(Entity): class ZMSensorRunState(Entity): """Get the ZoneMinder run state.""" - def __init__(self, client): + def __init__(self, client: ZoneMinder, config_entry: ConfigEntry): """Initialize run state sensor.""" self._state = None self._is_available = None self._client = client + self._config_entry = config_entry + + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return f"{self._config_entry.unique_id}_runstate" @property def name(self): diff --git a/homeassistant/components/zoneminder/services.yaml b/homeassistant/components/zoneminder/services.yaml index a6fb85b641d..52e8a3bf0bb 100644 --- a/homeassistant/components/zoneminder/services.yaml +++ b/homeassistant/components/zoneminder/services.yaml @@ -1,6 +1,9 @@ set_run_state: - description: Set the ZoneMinder run state + description: "Set the ZoneMinder run state" fields: + id: + description: "The host name or IP address of the ZoneMinder instance." + example: "10.10.0.2" name: - description: The string name of the ZoneMinder run state to set as active. + description: "The string name of the ZoneMinder run state to set as active." example: "Home" diff --git a/homeassistant/components/zoneminder/strings.json b/homeassistant/components/zoneminder/strings.json new file mode 100644 index 00000000000..8b722c9af2c --- /dev/null +++ b/homeassistant/components/zoneminder/strings.json @@ -0,0 +1,28 @@ +{ + "config": { + "flow_title": "ZoneMinder", + "step": { + "user": { + "title": "Add ZoneMinder Server.", + "data": { + "host": "Host and Port (ex 10.10.0.4:8010)", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "path": "ZM Path", + "path_zms": "ZMS Path", + "ssl": "Use SSL for connections to ZoneMinder", + "verify_ssl": "Verify SSL Certificate" + } + } + }, + "abort": { + "auth_fail": "Username or password is incorrect.", + "connection_error": "Failed to connect to a ZoneMinder server." + }, + "error": { + "auth_fail": "Username or password is incorrect.", + "connection_error": "Failed to connect to a ZoneMinder server." + }, + "create_entry": { "default": "ZoneMinder server added." } + } +} diff --git a/homeassistant/components/zoneminder/switch.py b/homeassistant/components/zoneminder/switch.py index 0428ddbf888..d8d1cc78797 100644 --- a/homeassistant/components/zoneminder/switch.py +++ b/homeassistant/components/zoneminder/switch.py @@ -1,41 +1,61 @@ """Support for ZoneMinder switches.""" import logging +from typing import Callable, List, Optional import voluptuous as vol -from zoneminder.monitor import MonitorState +from zoneminder.monitor import Monitor, MonitorState -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + PLATFORM_SCHEMA, + SwitchEntity, +) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON -import homeassistant.helpers.config_validation as cv +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity -from . import DOMAIN as ZONEMINDER_DOMAIN +from .common import get_client_from_data, get_platform_configs _LOGGER = logging.getLogger(__name__) +MONITOR_STATES = { + MonitorState[name].value: MonitorState[name] + for name in dir(MonitorState) + if not name.startswith("_") +} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_COMMAND_ON): cv.string, - vol.Required(CONF_COMMAND_OFF): cv.string, + vol.Required(CONF_COMMAND_ON): vol.All(vol.In(MONITOR_STATES.keys())), + vol.Required(CONF_COMMAND_OFF): vol.All(vol.In(MONITOR_STATES.keys())), } ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the ZoneMinder switch platform.""" +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], Optional[bool]], None], +) -> None: + """Set up the sensor config entry.""" + zm_client = get_client_from_data(hass, config_entry.unique_id) + monitors = await hass.async_add_job(zm_client.get_monitors) - on_state = MonitorState(config.get(CONF_COMMAND_ON)) - off_state = MonitorState(config.get(CONF_COMMAND_OFF)) + if not monitors: + _LOGGER.warning("Could not fetch monitors from ZoneMinder") + return switches = [] - for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): - monitors = zm_client.get_monitors() - if not monitors: - _LOGGER.warning("Could not fetch monitors from ZoneMinder") - return + for monitor in monitors: + for config in get_platform_configs(hass, SWITCH_DOMAIN): + on_state = MONITOR_STATES[config[CONF_COMMAND_ON]] + off_state = MONITOR_STATES[config[CONF_COMMAND_OFF]] - for monitor in monitors: - switches.append(ZMSwitchMonitors(monitor, on_state, off_state)) - add_entities(switches) + switches.append( + ZMSwitchMonitors(monitor, on_state, off_state, config_entry) + ) + + async_add_entities(switches, True) class ZMSwitchMonitors(SwitchEntity): @@ -43,13 +63,25 @@ class ZMSwitchMonitors(SwitchEntity): icon = "mdi:record-rec" - def __init__(self, monitor, on_state, off_state): + def __init__( + self, + monitor: Monitor, + on_state: MonitorState, + off_state: MonitorState, + config_entry: ConfigEntry, + ): """Initialize the switch.""" self._monitor = monitor self._on_state = on_state self._off_state = off_state + self._config_entry = config_entry self._state = None + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return f"{self._config_entry.unique_id}_{self._monitor.id}_switch_{self._on_state.value}_{self._off_state.value}" + @property def name(self): """Return the name of the switch.""" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 21336463393..045c5c26285 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -214,5 +214,6 @@ FLOWS = [ "yeelight", "zerproc", "zha", + "zoneminder", "zwave" ] diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9654dd741ed..61a0b79d5be 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1081,3 +1081,6 @@ zigpy-znp==0.1.1 # homeassistant.components.zha zigpy==0.23.2 + +# homeassistant.components.zoneminder +zm-py==0.4.0 diff --git a/tests/components/zoneminder/__init__.py b/tests/components/zoneminder/__init__.py new file mode 100644 index 00000000000..9ea5189a7b9 --- /dev/null +++ b/tests/components/zoneminder/__init__.py @@ -0,0 +1 @@ +"""Tests for the zoneminder component.""" diff --git a/tests/components/zoneminder/test_binary_sensor.py b/tests/components/zoneminder/test_binary_sensor.py new file mode 100644 index 00000000000..ee9883283e9 --- /dev/null +++ b/tests/components/zoneminder/test_binary_sensor.py @@ -0,0 +1,65 @@ +"""Binary sensor tests.""" +from zoneminder.zm import ZoneMinder + +from homeassistant.components.zoneminder import const +from homeassistant.config import async_process_ha_core_config +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + CONF_PASSWORD, + CONF_PATH, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.async_mock import MagicMock, patch +from tests.common import MockConfigEntry + + +async def test_async_setup_entry(hass: HomeAssistant) -> None: + """Test setup of binary sensor entities.""" + with patch( + "homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder + ) as zoneminder_mock: + zm_client: ZoneMinder = MagicMock(spec=ZoneMinder) + zm_client.get_zms_url.return_value = "http://host1/path_zms1" + zm_client.login.return_value = True + zm_client.is_available = True + + zoneminder_mock.return_value = zm_client + + config_entry = MockConfigEntry( + domain=const.DOMAIN, + unique_id="host1", + data={ + CONF_HOST: "host1", + CONF_USERNAME: "username1", + CONF_PASSWORD: "password1", + CONF_PATH: "path1", + const.CONF_PATH_ZMS: "path_zms1", + CONF_SSL: False, + CONF_VERIFY_SSL: True, + }, + ) + config_entry.add_to_hass(hass) + + await async_process_ha_core_config(hass, {}) + await async_setup_component(hass, HASS_DOMAIN, {}) + await async_setup_component(hass, const.DOMAIN, {}) + await hass.async_block_till_done() + + await hass.services.async_call( + HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "binary_sensor.host1"} + ) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.host1").state == "on" + + zm_client.is_available = False + await hass.services.async_call( + HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "binary_sensor.host1"} + ) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.host1").state == "off" diff --git a/tests/components/zoneminder/test_camera.py b/tests/components/zoneminder/test_camera.py new file mode 100644 index 00000000000..06f4c3554df --- /dev/null +++ b/tests/components/zoneminder/test_camera.py @@ -0,0 +1,89 @@ +"""Binary sensor tests.""" +from zoneminder.monitor import Monitor +from zoneminder.zm import ZoneMinder + +from homeassistant.components.zoneminder import const +from homeassistant.config import async_process_ha_core_config +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + CONF_PASSWORD, + CONF_PATH, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.async_mock import MagicMock, patch +from tests.common import MockConfigEntry + + +async def test_async_setup_entry(hass: HomeAssistant) -> None: + """Test setup of camera entities.""" + with patch( + "homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder + ) as zoneminder_mock: + monitor1 = MagicMock(spec=Monitor) + monitor1.name = "monitor1" + monitor1.mjpeg_image_url = "mjpeg_image_url1" + monitor1.still_image_url = "still_image_url1" + monitor1.is_recording = True + monitor1.is_available = True + + monitor2 = MagicMock(spec=Monitor) + monitor2.name = "monitor2" + monitor2.mjpeg_image_url = "mjpeg_image_url2" + monitor2.still_image_url = "still_image_url2" + monitor2.is_recording = False + monitor2.is_available = False + + zm_client: ZoneMinder = MagicMock(spec=ZoneMinder) + zm_client.get_zms_url.return_value = "http://host1/path_zms1" + zm_client.login.return_value = True + zm_client.get_monitors.return_value = [monitor1, monitor2] + + zoneminder_mock.return_value = zm_client + + config_entry = MockConfigEntry( + domain=const.DOMAIN, + unique_id="host1", + data={ + CONF_HOST: "host1", + CONF_USERNAME: "username1", + CONF_PASSWORD: "password1", + CONF_PATH: "path1", + const.CONF_PATH_ZMS: "path_zms1", + CONF_SSL: False, + CONF_VERIFY_SSL: True, + }, + ) + config_entry.add_to_hass(hass) + + await async_process_ha_core_config(hass, {}) + await async_setup_component(hass, HASS_DOMAIN, {}) + await async_setup_component(hass, const.DOMAIN, {}) + await hass.async_block_till_done() + + await hass.services.async_call( + HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "camera.monitor1"} + ) + await hass.services.async_call( + HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "camera.monitor2"} + ) + await hass.async_block_till_done() + assert hass.states.get("camera.monitor1").state == "recording" + assert hass.states.get("camera.monitor2").state == "unavailable" + + monitor1.is_recording = False + monitor2.is_recording = True + await hass.services.async_call( + HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "camera.monitor1"} + ) + await hass.services.async_call( + HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "camera.monitor2"} + ) + await hass.async_block_till_done() + assert hass.states.get("camera.monitor1").state == "idle" + assert hass.states.get("camera.monitor2").state == "unavailable" diff --git a/tests/components/zoneminder/test_config_flow.py b/tests/components/zoneminder/test_config_flow.py new file mode 100644 index 00000000000..279613e2b38 --- /dev/null +++ b/tests/components/zoneminder/test_config_flow.py @@ -0,0 +1,119 @@ +"""Config flow tests.""" +import requests +from zoneminder.zm import ZoneMinder + +from homeassistant import config_entries +from homeassistant.components.zoneminder import ClientAvailabilityResult, const +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PATH, + CONF_SOURCE, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import HomeAssistant + +from tests.async_mock import MagicMock, patch + + +async def test_import(hass: HomeAssistant) -> None: + """Test import from configuration yaml.""" + with patch( + "homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder + ) as zoneminder_mock: + conf_data = { + CONF_HOST: "host1", + CONF_USERNAME: "username1", + CONF_PASSWORD: "password1", + CONF_PATH: "path1", + const.CONF_PATH_ZMS: "path_zms1", + CONF_SSL: False, + CONF_VERIFY_SSL: True, + } + + zm_client: ZoneMinder = MagicMock(spec=ZoneMinder) + zm_client.get_zms_url.return_value = "http://host1/path_zms1" + zoneminder_mock.return_value = zm_client + + zm_client.login.return_value = False + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=conf_data, + ) + assert result + assert result["type"] == "abort" + assert result["reason"] == "auth_fail" + + zm_client.login.return_value = True + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=conf_data, + ) + assert result + assert result["type"] == "create_entry" + assert result["data"] == { + **conf_data, + CONF_SOURCE: config_entries.SOURCE_IMPORT, + } + + +async def test_user(hass: HomeAssistant) -> None: + """Test user initiated creation.""" + with patch( + "homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder + ) as zoneminder_mock: + conf_data = { + CONF_HOST: "host1", + CONF_USERNAME: "username1", + CONF_PASSWORD: "password1", + CONF_PATH: "path1", + const.CONF_PATH_ZMS: "path_zms1", + CONF_SSL: False, + CONF_VERIFY_SSL: True, + } + + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result + assert result["type"] == "form" + + zm_client: ZoneMinder = MagicMock(spec=ZoneMinder) + zoneminder_mock.return_value = zm_client + + zm_client.login.side_effect = requests.exceptions.ConnectionError() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + conf_data, + ) + assert result + assert result["type"] == "form" + assert result["errors"] == { + "base": ClientAvailabilityResult.ERROR_CONNECTION_ERROR.value + } + + zm_client.login.side_effect = None + zm_client.login.return_value = False + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + conf_data, + ) + assert result + assert result["type"] == "form" + assert result["errors"] == { + "base": ClientAvailabilityResult.ERROR_AUTH_FAIL.value + } + + zm_client.login.return_value = True + zm_client.get_zms_url.return_value = "http://host1/path_zms1" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + conf_data, + ) + assert result + assert result["type"] == "create_entry" + assert result["data"] == conf_data diff --git a/tests/components/zoneminder/test_init.py b/tests/components/zoneminder/test_init.py new file mode 100644 index 00000000000..333106946bd --- /dev/null +++ b/tests/components/zoneminder/test_init.py @@ -0,0 +1,122 @@ +"""Tests for init functions.""" +from datetime import timedelta + +from zoneminder.zm import ZoneMinder + +from homeassistant import config_entries +from homeassistant.components.zoneminder import const +from homeassistant.components.zoneminder.common import is_client_in_data +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.const import ( + ATTR_ID, + ATTR_NAME, + CONF_HOST, + CONF_PASSWORD, + CONF_PATH, + CONF_SOURCE, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.async_mock import MagicMock, patch +from tests.common import async_fire_time_changed + + +async def test_no_yaml_config(hass: HomeAssistant) -> None: + """Test empty yaml config.""" + with patch( + "homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder + ) as zoneminder_mock: + zm_client: ZoneMinder = MagicMock(spec=ZoneMinder) + zm_client.get_zms_url.return_value = "http://host1/path_zms1" + zm_client.login.return_value = True + zm_client.get_monitors.return_value = [] + + zoneminder_mock.return_value = zm_client + + hass_config = {const.DOMAIN: []} + await async_setup_component(hass, const.DOMAIN, hass_config) + await hass.async_block_till_done() + assert not hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE) + + +async def test_yaml_config_import(hass: HomeAssistant) -> None: + """Test yaml config import.""" + with patch( + "homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder + ) as zoneminder_mock: + zm_client: ZoneMinder = MagicMock(spec=ZoneMinder) + zm_client.get_zms_url.return_value = "http://host1/path_zms1" + zm_client.login.return_value = True + zm_client.get_monitors.return_value = [] + + zoneminder_mock.return_value = zm_client + + hass_config = {const.DOMAIN: [{CONF_HOST: "host1"}]} + await async_setup_component(hass, const.DOMAIN, hass_config) + await hass.async_block_till_done() + assert hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE) + + +async def test_load_call_service_and_unload(hass: HomeAssistant) -> None: + """Test config entry load/unload and calling of service.""" + with patch( + "homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder + ) as zoneminder_mock: + zm_client: ZoneMinder = MagicMock(spec=ZoneMinder) + zm_client.get_zms_url.return_value = "http://host1/path_zms1" + zm_client.login.side_effect = [True, True, False, True] + zm_client.get_monitors.return_value = [] + zm_client.is_available.return_value = True + + zoneminder_mock.return_value = zm_client + + await hass.config_entries.flow.async_init( + const.DOMAIN, + context={CONF_SOURCE: config_entries.SOURCE_USER}, + data={ + CONF_HOST: "host1", + CONF_USERNAME: "username1", + CONF_PASSWORD: "password1", + CONF_PATH: "path1", + const.CONF_PATH_ZMS: "path_zms1", + CONF_SSL: False, + CONF_VERIFY_SSL: True, + }, + ) + await hass.async_block_till_done() + + config_entry = next(iter(hass.config_entries.async_entries(const.DOMAIN)), None) + assert config_entry + + assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert not is_client_in_data(hass, "host1") + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert config_entry.state == ENTRY_STATE_LOADED + assert is_client_in_data(hass, "host1") + + assert hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE) + + await hass.services.async_call( + const.DOMAIN, + const.SERVICE_SET_RUN_STATE, + {ATTR_ID: "host1", ATTR_NAME: "away"}, + ) + await hass.async_block_till_done() + zm_client.set_active_state.assert_called_with("away") + + await config_entry.async_unload(hass) + await hass.async_block_till_done() + assert config_entry.state == ENTRY_STATE_NOT_LOADED + assert not is_client_in_data(hass, "host1") + assert not hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE) diff --git a/tests/components/zoneminder/test_sensor.py b/tests/components/zoneminder/test_sensor.py new file mode 100644 index 00000000000..0b9db899387 --- /dev/null +++ b/tests/components/zoneminder/test_sensor.py @@ -0,0 +1,167 @@ +"""Binary sensor tests.""" +from zoneminder.monitor import Monitor, MonitorState, TimePeriod +from zoneminder.zm import ZoneMinder + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.zoneminder import const +from homeassistant.components.zoneminder.sensor import CONF_INCLUDE_ARCHIVED +from homeassistant.config import async_process_ha_core_config +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + CONF_MONITORED_CONDITIONS, + CONF_PASSWORD, + CONF_PATH, + CONF_PLATFORM, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.async_mock import MagicMock, patch +from tests.common import MockConfigEntry + + +async def test_async_setup_entry(hass: HomeAssistant) -> None: + """Test setup of sensor entities.""" + + def _get_events(monitor_id: int, time_period: TimePeriod, include_archived: bool): + enum_list = [name for name in dir(TimePeriod) if not name.startswith("_")] + tp_index = enum_list.index(time_period.name) + return (100 * monitor_id) + (tp_index * 10) + include_archived + + def _monitor1_get_events(time_period: TimePeriod, include_archived: bool): + return _get_events(1, time_period, include_archived) + + def _monitor2_get_events(time_period: TimePeriod, include_archived: bool): + return _get_events(2, time_period, include_archived) + + with patch( + "homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder + ) as zoneminder_mock: + monitor1 = MagicMock(spec=Monitor) + monitor1.name = "monitor1" + monitor1.mjpeg_image_url = "mjpeg_image_url1" + monitor1.still_image_url = "still_image_url1" + monitor1.is_recording = True + monitor1.is_available = True + monitor1.function = MonitorState.MONITOR + monitor1.get_events.side_effect = _monitor1_get_events + + monitor2 = MagicMock(spec=Monitor) + monitor2.name = "monitor2" + monitor2.mjpeg_image_url = "mjpeg_image_url2" + monitor2.still_image_url = "still_image_url2" + monitor2.is_recording = False + monitor2.is_available = False + monitor2.function = MonitorState.MODECT + monitor2.get_events.side_effect = _monitor2_get_events + + zm_client: ZoneMinder = MagicMock(spec=ZoneMinder) + zm_client.get_zms_url.return_value = "http://host1/path_zms1" + zm_client.login.return_value = True + zm_client.get_monitors.return_value = [monitor1, monitor2] + + zoneminder_mock.return_value = zm_client + + config_entry = MockConfigEntry( + domain=const.DOMAIN, + unique_id="host1", + data={ + CONF_HOST: "host1", + CONF_USERNAME: "username1", + CONF_PASSWORD: "password1", + CONF_PATH: "path1", + const.CONF_PATH_ZMS: "path_zms1", + CONF_SSL: False, + CONF_VERIFY_SSL: True, + }, + ) + config_entry.add_to_hass(hass) + + hass_config = { + HASS_DOMAIN: {}, + SENSOR_DOMAIN: [ + { + CONF_PLATFORM: const.DOMAIN, + CONF_INCLUDE_ARCHIVED: True, + CONF_MONITORED_CONDITIONS: ["all", "day"], + } + ], + } + + await async_process_ha_core_config(hass, hass_config[HASS_DOMAIN]) + await async_setup_component(hass, HASS_DOMAIN, hass_config) + await async_setup_component(hass, SENSOR_DOMAIN, hass_config) + await hass.async_block_till_done() + await async_setup_component(hass, const.DOMAIN, hass_config) + await hass.async_block_till_done() + + await hass.services.async_call( + HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor1_status"} + ) + await hass.services.async_call( + HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor1_events"} + ) + await hass.services.async_call( + HASS_DOMAIN, + "update_entity", + {ATTR_ENTITY_ID: "sensor.monitor1_events_last_day"}, + ) + await hass.services.async_call( + HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor2_status"} + ) + await hass.services.async_call( + HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor2_events"} + ) + await hass.services.async_call( + HASS_DOMAIN, + "update_entity", + {ATTR_ENTITY_ID: "sensor.monitor2_events_last_day"}, + ) + await hass.async_block_till_done() + assert ( + hass.states.get("sensor.monitor1_status").state + == MonitorState.MONITOR.value + ) + assert hass.states.get("sensor.monitor1_events").state == "101" + assert hass.states.get("sensor.monitor1_events_last_day").state == "111" + assert hass.states.get("sensor.monitor2_status").state == "unavailable" + assert hass.states.get("sensor.monitor2_events").state == "201" + assert hass.states.get("sensor.monitor2_events_last_day").state == "211" + + monitor1.function = MonitorState.NONE + monitor2.function = MonitorState.NODECT + await hass.services.async_call( + HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor1_status"} + ) + await hass.services.async_call( + HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor1_events"} + ) + await hass.services.async_call( + HASS_DOMAIN, + "update_entity", + {ATTR_ENTITY_ID: "sensor.monitor1_events_last_day"}, + ) + await hass.services.async_call( + HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor2_status"} + ) + await hass.services.async_call( + HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor2_events"} + ) + await hass.services.async_call( + HASS_DOMAIN, + "update_entity", + {ATTR_ENTITY_ID: "sensor.monitor2_events_last_day"}, + ) + await hass.async_block_till_done() + assert ( + hass.states.get("sensor.monitor1_status").state == MonitorState.NONE.value + ) + assert hass.states.get("sensor.monitor1_events").state == "101" + assert hass.states.get("sensor.monitor1_events_last_day").state == "111" + assert hass.states.get("sensor.monitor2_status").state == "unavailable" + assert hass.states.get("sensor.monitor2_events").state == "201" + assert hass.states.get("sensor.monitor2_events_last_day").state == "211" diff --git a/tests/components/zoneminder/test_switch.py b/tests/components/zoneminder/test_switch.py new file mode 100644 index 00000000000..3665b2fa17e --- /dev/null +++ b/tests/components/zoneminder/test_switch.py @@ -0,0 +1,126 @@ +"""Binary sensor tests.""" +from zoneminder.monitor import Monitor, MonitorState +from zoneminder.zm import ZoneMinder + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.zoneminder import const +from homeassistant.config import async_process_ha_core_config +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_COMMAND_OFF, + CONF_COMMAND_ON, + CONF_HOST, + CONF_PASSWORD, + CONF_PATH, + CONF_PLATFORM, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.async_mock import MagicMock, patch +from tests.common import MockConfigEntry + + +async def test_async_setup_entry(hass: HomeAssistant) -> None: + """Test setup of sensor entities.""" + + with patch( + "homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder + ) as zoneminder_mock: + monitor1 = MagicMock(spec=Monitor) + monitor1.name = "monitor1" + monitor1.mjpeg_image_url = "mjpeg_image_url1" + monitor1.still_image_url = "still_image_url1" + monitor1.is_recording = True + monitor1.is_available = True + monitor1.function = MonitorState.MONITOR + + monitor2 = MagicMock(spec=Monitor) + monitor2.name = "monitor2" + monitor2.mjpeg_image_url = "mjpeg_image_url2" + monitor2.still_image_url = "still_image_url2" + monitor2.is_recording = False + monitor2.is_available = False + monitor2.function = MonitorState.MODECT + + zm_client: ZoneMinder = MagicMock(spec=ZoneMinder) + zm_client.get_zms_url.return_value = "http://host1/path_zms1" + zm_client.login.return_value = True + zm_client.get_monitors.return_value = [monitor1, monitor2] + + zoneminder_mock.return_value = zm_client + + config_entry = MockConfigEntry( + domain=const.DOMAIN, + unique_id="host1", + data={ + CONF_HOST: "host1", + CONF_USERNAME: "username1", + CONF_PASSWORD: "password1", + CONF_PATH: "path1", + const.CONF_PATH_ZMS: "path_zms1", + CONF_SSL: False, + CONF_VERIFY_SSL: True, + }, + ) + config_entry.add_to_hass(hass) + + hass_config = { + HASS_DOMAIN: {}, + SWITCH_DOMAIN: [ + { + CONF_PLATFORM: const.DOMAIN, + CONF_COMMAND_ON: MonitorState.MONITOR.value, + CONF_COMMAND_OFF: MonitorState.MODECT.value, + }, + { + CONF_PLATFORM: const.DOMAIN, + CONF_COMMAND_ON: MonitorState.MODECT.value, + CONF_COMMAND_OFF: MonitorState.MONITOR.value, + }, + ], + } + + await async_process_ha_core_config(hass, hass_config[HASS_DOMAIN]) + await async_setup_component(hass, HASS_DOMAIN, hass_config) + await async_setup_component(hass, SWITCH_DOMAIN, hass_config) + await hass.async_block_till_done() + await async_setup_component(hass, const.DOMAIN, hass_config) + await hass.async_block_till_done() + + await hass.services.async_call( + SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: "switch.monitor1_state"} + ) + await hass.services.async_call( + SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: "switch.monitor1_state_2"} + ) + await hass.async_block_till_done() + assert hass.states.get("switch.monitor1_state").state == STATE_ON + assert hass.states.get("switch.monitor1_state_2").state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: "switch.monitor1_state"} + ) + await hass.services.async_call( + SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: "switch.monitor1_state_2"} + ) + await hass.async_block_till_done() + assert hass.states.get("switch.monitor1_state").state == STATE_OFF + assert hass.states.get("switch.monitor1_state_2").state == STATE_ON + + monitor1.function = MonitorState.NONE + monitor2.function = MonitorState.NODECT + await hass.services.async_call( + HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "switch.monitor1_state"} + ) + await hass.services.async_call( + HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "switch.monitor1_state_2"} + ) + await hass.async_block_till_done() + assert hass.states.get("switch.monitor1_state").state == STATE_OFF + assert hass.states.get("switch.monitor1_state_2").state == STATE_OFF From b4d29653c603cb1d9acb4bfefa233a0a99c3bc0f Mon Sep 17 00:00:00 2001 From: cagnulein Date: Thu, 17 Sep 2020 11:35:13 +0200 Subject: [PATCH 207/514] 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 a82421306fdd008c1cec169e5f1c944cf71c0945 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 208/514] 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 5beac3fd4f8..ca6691c0fde 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 61a0b79d5be..77a11b4ba78 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -665,7 +665,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 0bb8a49ea2a9ee37b92baf5b1155fb34c968e82a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 17 Sep 2020 14:44:19 +0200 Subject: [PATCH 209/514] 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 ca6691c0fde..1683ef87d61 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 77a11b4ba78..38e922b1b43 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -373,7 +373,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 30de9848277e283cf490e3776bba96cd2ab5330a Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 17 Sep 2020 09:01:28 -0500 Subject: [PATCH 210/514] 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 95e998d25adcc10919bab488dc341bf03f1e9e55 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 17 Sep 2020 16:45:55 +0200 Subject: [PATCH 211/514] 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 44952a94cfb411ac4a10f9a8b4bf3edd737993d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Sep 2020 09:47:23 -0500 Subject: [PATCH 212/514] 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 2489a6c6ef5d2b0ca91fe2638e66da8374ab7671 Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Thu, 17 Sep 2020 17:07:03 +0200 Subject: [PATCH 213/514] Fix typo in strings for wolflink (#40164) --- homeassistant/components/wolflink/strings.sensor.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/wolflink/strings.sensor.json b/homeassistant/components/wolflink/strings.sensor.json index 2ce7df6fae5..75c8199a117 100644 --- a/homeassistant/components/wolflink/strings.sensor.json +++ b/homeassistant/components/wolflink/strings.sensor.json @@ -6,7 +6,7 @@ "aus": "Disabled", "standby": "Standby", "auto": "Auto", - "permanent": "Permament", + "permanent": "Permanent", "initialisierung": "Initialization", "antilegionellenfunktion": "Anti-legionella Function", "fernschalter_ein": "Remote control enabled", From e9abb357e416692c9b87f4ed579c82dafdd6ff9e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Sep 2020 14:45:30 -0500 Subject: [PATCH 214/514] Log template listeners when debug logging is on (#40180) --- homeassistant/helpers/event.py | 14 ++++++++++++++ tests/helpers/test_event.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index dae93896987..f2f8b5ac974 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -581,6 +581,11 @@ class _TrackTemplateResultInfo: self._last_info = self._info.copy() self._create_listeners() + _LOGGER.debug( + "Template group %s listens for %s", + self._track_templates, + self.listeners, + ) @property def listeners(self) -> Dict: @@ -726,6 +731,10 @@ class _TrackTemplateResultInfo: ): continue + _LOGGER.debug( + "Template update %s triggered by event: %s", template.template, event + ) + self._info[template] = template.async_render_to_info( track_template_.variables ) @@ -751,6 +760,11 @@ class _TrackTemplateResultInfo: if info_changed: self._update_listeners() + _LOGGER.debug( + "Template group %s listens for %s", + self._track_templates, + self.listeners, + ) self._last_info = self._info.copy() if not updates: diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 821285cfbe1..479984b97f1 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -668,7 +668,7 @@ async def test_track_template_error(hass, caplog): hass.states.async_set("switch.not_exist", "off") await hass.async_block_till_done() - assert "lunch" not in caplog.text + assert "no filter named 'lunch'" not in caplog.text assert "TemplateAssertionError" not in caplog.text From 1887f11ec932738b215440d9c17f586c4247e314 Mon Sep 17 00:00:00 2001 From: Daniel de Jong <64607512+daniel-jong@users.noreply.github.com> Date: Thu, 17 Sep 2020 22:48:52 +0200 Subject: [PATCH 215/514] Do not default Pilight lights to max brightness (#39549) Fix pilight lights would always turned on at max brightness instead of just turning on. Some 433mhz dimmers (like the KAKU series) remember their last brightness setting. Fix pilight lights would not respect configured dimlevel_min --- homeassistant/components/pilight/light.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/pilight/light.py b/homeassistant/components/pilight/light.py index 12d175817d7..11eeb02293e 100644 --- a/homeassistant/components/pilight/light.py +++ b/homeassistant/components/pilight/light.py @@ -61,7 +61,20 @@ class PilightLight(PilightBaseDevice, LightEntity): def turn_on(self, **kwargs): """Turn the switch on by calling pilight.send service with on code.""" - self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255) - dimlevel = int(self._brightness / (255 / self._dimlevel_max)) + # Update brightness only if provided as an argument. + # This will allow the switch to keep its previous brightness level. + dimlevel = None + + if ATTR_BRIGHTNESS in kwargs: + self._brightness = kwargs[ATTR_BRIGHTNESS] + + # Calculate pilight brightness (as a range of 0 to 15) + # By creating a percentage + percentage = self._brightness / 255 + # Then calculate the dimmer range (aka amount of available brightness steps). + dimrange = self._dimlevel_max - self._dimlevel_min + # Finally calculate the pilight brightness. + # We add dimlevel_min back in to ensure the minimum is always reached. + dimlevel = int(percentage * dimrange + self._dimlevel_min) self.set_state(turn_on=True, dimlevel=dimlevel) From 0a0a8fc67dbbd6bb5e0775b6a2a5942065686209 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 18 Sep 2020 00:07:17 +0000 Subject: [PATCH 216/514] [ci skip] Translation update --- .../accuweather/translations/fr.json | 23 ++++- .../alarmdecoder/translations/fr.json | 74 ++++++++++++++++ .../components/almond/translations/fr.json | 3 +- .../components/august/translations/es.json | 3 +- .../components/august/translations/fr.json | 3 +- .../components/august/translations/no.json | 3 +- .../august/translations/zh-Hant.json | 3 +- .../azure_devops/translations/fr.json | 27 +++++- .../components/blebox/translations/fr.json | 3 +- .../components/bond/translations/fr.json | 1 + .../components/control4/translations/fr.json | 10 +++ .../components/deconz/translations/fr.json | 5 ++ .../components/doorbird/translations/fr.json | 3 +- .../components/elkm1/translations/fr.json | 2 + .../components/firmata/translations/fr.json | 7 ++ .../components/flume/translations/fr.json | 1 + .../home_connect/translations/fr.json | 3 +- .../components/homekit/translations/fr.json | 1 + .../homekit_controller/translations/fr.json | 26 ++++++ .../components/hue/translations/fr.json | 4 +- .../humidifier/translations/fr.json | 2 + .../translations/fr.json | 3 +- .../hvv_departures/translations/fr.json | 5 +- .../components/insteon/translations/fr.json | 18 +++- .../components/isy994/translations/fr.json | 3 +- .../components/konnected/translations/fr.json | 6 +- .../lutron_caseta/translations/fr.json | 4 + .../components/mikrotik/translations/fr.json | 1 + .../components/netatmo/translations/fr.json | 1 + .../nightscout/translations/fr.json | 1 + .../components/nuheat/translations/fr.json | 1 + .../components/nws/translations/fr.json | 1 + .../ovo_energy/translations/fr.json | 1 + .../panasonic_viera/translations/fr.json | 6 ++ .../pvpc_hourly_pricing/translations/fr.json | 1 + .../components/roomba/translations/fr.json | 1 + .../components/rpi_power/translations/fr.json | 14 +++ .../simplisafe/translations/fr.json | 15 +++- .../components/smappee/translations/fr.json | 17 +++- .../smartthings/translations/fr.json | 6 ++ .../components/sms/translations/fr.json | 3 +- .../components/somfy/translations/fr.json | 3 +- .../components/songpal/translations/fr.json | 5 ++ .../components/spider/translations/fr.json | 3 +- .../components/spotify/translations/fr.json | 1 + .../squeezebox/translations/fr.json | 6 +- .../synology_dsm/translations/fr.json | 10 +++ .../components/tado/translations/fr.json | 1 + .../components/toon/translations/fr.json | 3 +- .../components/unifi/translations/fr.json | 5 +- .../components/vera/translations/fr.json | 8 ++ .../components/vilfo/translations/fr.json | 1 + .../components/volumio/translations/fr.json | 7 +- .../components/withings/translations/fr.json | 3 +- .../components/wolflink/translations/fr.json | 9 +- .../wolflink/translations/sensor.en.json | 2 +- .../wolflink/translations/sensor.fr.json | 87 +++++++++++++++++++ .../zoneminder/translations/ca.json | 30 +++++++ .../zoneminder/translations/en.json | 30 +++++++ .../zoneminder/translations/es.json | 30 +++++++ .../zoneminder/translations/fr.json | 30 +++++++ .../zoneminder/translations/nl.json | 12 +++ .../zoneminder/translations/no.json | 30 +++++++ .../zoneminder/translations/ru.json | 30 +++++++ 64 files changed, 625 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/alarmdecoder/translations/fr.json create mode 100644 homeassistant/components/firmata/translations/fr.json create mode 100644 homeassistant/components/rpi_power/translations/fr.json create mode 100644 homeassistant/components/wolflink/translations/sensor.fr.json create mode 100644 homeassistant/components/zoneminder/translations/ca.json create mode 100644 homeassistant/components/zoneminder/translations/en.json create mode 100644 homeassistant/components/zoneminder/translations/es.json create mode 100644 homeassistant/components/zoneminder/translations/fr.json create mode 100644 homeassistant/components/zoneminder/translations/nl.json create mode 100644 homeassistant/components/zoneminder/translations/no.json create mode 100644 homeassistant/components/zoneminder/translations/ru.json diff --git a/homeassistant/components/accuweather/translations/fr.json b/homeassistant/components/accuweather/translations/fr.json index 33001cf5b84..40cf1ccc0b9 100644 --- a/homeassistant/components/accuweather/translations/fr.json +++ b/homeassistant/components/accuweather/translations/fr.json @@ -5,11 +5,30 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 API invalide" + "invalid_api_key": "Cl\u00e9 API invalide", + "requests_exceeded": "Le nombre autoris\u00e9 de requ\u00eates adress\u00e9es \u00e0 l'API AccuWeather a \u00e9t\u00e9 d\u00e9pass\u00e9. Vous devez attendre ou modifier la cl\u00e9 API." }, "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." + "data": { + "api_key": "Cl\u00e9 d'API", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom de l'int\u00e9gration" + }, + "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.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Pr\u00e9visions m\u00e9t\u00e9orologiques" + }, + "description": "En raison des limitations de la version gratuite de la cl\u00e9 API AccuWeather, lorsque vous activez les pr\u00e9visions m\u00e9t\u00e9orologiques, les mises \u00e0 jour des donn\u00e9es seront effectu\u00e9es toutes les 64 minutes au lieu de toutes les 32 minutes.", + "title": "Options AccuWeather" } } } diff --git a/homeassistant/components/alarmdecoder/translations/fr.json b/homeassistant/components/alarmdecoder/translations/fr.json new file mode 100644 index 00000000000..c48cf00cded --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/fr.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "create_entry": { + "default": "Connexion r\u00e9ussie \u00e0 AlarmDecoder." + }, + "error": { + "service_unavailable": "\u00c9chec de connexion" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "D\u00e9bit en bauds de l'appareil", + "device_path": "Chemin du p\u00e9riph\u00e9rique", + "host": "H\u00f4te", + "port": "Port" + }, + "title": "Configurer les param\u00e8tres de connexion" + }, + "user": { + "data": { + "protocol": "Protocole" + }, + "title": "Choisissez le protocole AlarmDecoder" + } + } + }, + "options": { + "error": { + "int": "Le champ ci-dessous doit \u00eatre un entier.", + "loop_range": "La boucle RF doit \u00eatre un entier compris entre 1 et 4.", + "loop_rfid": "La boucle RF ne peut pas \u00eatre utilis\u00e9e sans s\u00e9rie RF.", + "relay_inclusive": "L'adresse de relais et le canal de relais d\u00e9pendent du codage et doivent \u00eatre inclus ensemble." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Mode nuit alternatif", + "auto_bypass": "Bypass automatique \u00e0 l'armement", + "code_arm_required": "Code requis pour l'armement" + }, + "title": "Configurer AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Modifier" + }, + "description": "Que voulez-vous modifier?", + "title": "Configurer AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "Boucle RF", + "zone_name": "Nom de zone", + "zone_relayaddr": "Adresse de relais", + "zone_relaychan": "Canal de relais", + "zone_rfid": "RF S\u00e9rie", + "zone_type": "Type de zone" + }, + "description": "Entrez les d\u00e9tails de la zone {zone_number} . Pour supprimer la zone {zone_number} , laissez le nom de zone vide.", + "title": "Configurer AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "Num\u00e9ro de zone" + }, + "description": "Saisissez le num\u00e9ro de zone que vous souhaitez ajouter, modifier ou supprimer.", + "title": "Configurer AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/fr.json b/homeassistant/components/almond/translations/fr.json index f39a1660bb9..7b7f4bff1e4 100644 --- a/homeassistant/components/almond/translations/fr.json +++ b/homeassistant/components/almond/translations/fr.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "Vous ne pouvez configurer qu'un seul compte Almond", "cannot_connect": "Impossible de se connecter au serveur Almond", - "missing_configuration": "Veuillez consulter la documentation pour savoir comment configurer Almond." + "missing_configuration": "Veuillez consulter la documentation pour savoir comment configurer Almond.", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/august/translations/es.json b/homeassistant/components/august/translations/es.json index 28d9743c073..2ec72c9e4eb 100644 --- a/homeassistant/components/august/translations/es.json +++ b/homeassistant/components/august/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "La cuenta ya est\u00e1 configurada" + "already_configured": "La cuenta ya est\u00e1 configurada", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", diff --git a/homeassistant/components/august/translations/fr.json b/homeassistant/components/august/translations/fr.json index 752b7dc3712..82568b681fd 100644 --- a/homeassistant/components/august/translations/fr.json +++ b/homeassistant/components/august/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", diff --git a/homeassistant/components/august/translations/no.json b/homeassistant/components/august/translations/no.json index 838508f132d..feb4e4e759b 100644 --- a/homeassistant/components/august/translations/no.json +++ b/homeassistant/components/august/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigurert" + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Reautentisering var vellykket" }, "error": { "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", diff --git a/homeassistant/components/august/translations/zh-Hant.json b/homeassistant/components/august/translations/zh-Hant.json index 6b7e206d4c4..56a7bc4ef95 100644 --- a/homeassistant/components/august/translations/zh-Hant.json +++ b/homeassistant/components/august/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", diff --git a/homeassistant/components/azure_devops/translations/fr.json b/homeassistant/components/azure_devops/translations/fr.json index 7d50110a24e..528c76767ea 100644 --- a/homeassistant/components/azure_devops/translations/fr.json +++ b/homeassistant/components/azure_devops/translations/fr.json @@ -3,6 +3,31 @@ "abort": { "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9", "reauth_successful": "Jeton d'acc\u00e8s mis \u00e0 jour avec succ\u00e8s" + }, + "error": { + "authorization_error": "Erreur d'autorisation. V\u00e9rifiez que vous avez acc\u00e8s au projet et que vous disposez des informations d'identification correctes.", + "connection_error": "Impossible de se connecter \u00e0 Azure DevOps.", + "project_error": "Impossible d'obtenir les informations sur le projet." + }, + "flow_title": "Azure DevOps: {project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "Jeton d'acc\u00e8s personnel (PAT)" + }, + "description": "L'authentification a \u00e9chou\u00e9 pour {project_url} . Veuillez saisir vos informations d'identification actuelles.", + "title": "R\u00e9authentification" + }, + "user": { + "data": { + "organization": "Organisation", + "personal_access_token": "Jeton d'acc\u00e8s personnel (PAT)", + "project": "Projet" + }, + "description": "Configurez une instance Azure DevOps pour acc\u00e9der \u00e0 votre projet. Un jeton d'acc\u00e8s personnel n'est requis que pour un projet priv\u00e9.", + "title": "Ajouter un projet Azure DevOps" + } } - } + }, + "title": "Azure DevOps" } \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/fr.json b/homeassistant/components/blebox/translations/fr.json index 75d506a8212..d30d026d177 100644 --- a/homeassistant/components/blebox/translations/fr.json +++ b/homeassistant/components/blebox/translations/fr.json @@ -6,7 +6,8 @@ }, "error": { "cannot_connect": "Impossible de connecter le p\u00e9riph\u00e9rique BleBox. (V\u00e9rifiez les journaux pour les erreurs.)", - "unknown": "Erreur inconnue lors de la connexion au p\u00e9riph\u00e9rique BleBox. (V\u00e9rifiez les journaux pour les erreurs.)" + "unknown": "Erreur inconnue lors de la connexion au p\u00e9riph\u00e9rique BleBox. (V\u00e9rifiez les journaux pour les erreurs.)", + "unsupported_version": "L'appareil BleBox a un micrologiciel obsol\u00e8te. Veuillez d'abord le mettre \u00e0 jour." }, "flow_title": "P\u00e9riph\u00e9rique Blebox: {name} ({host)}", "step": { diff --git a/homeassistant/components/bond/translations/fr.json b/homeassistant/components/bond/translations/fr.json index cabbb73a370..496a21339cb 100644 --- a/homeassistant/components/bond/translations/fr.json +++ b/homeassistant/components/bond/translations/fr.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Echec de connexion", "invalid_auth": "Authentification invalide", + "old_firmware": "Ancien micrologiciel non pris en charge sur l'appareil Bond - veuillez mettre \u00e0 niveau avant de continuer", "unknown": "Erreur inattendue" }, "flow_title": "Bond : {bond_id} ({h\u00f4te})", diff --git a/homeassistant/components/control4/translations/fr.json b/homeassistant/components/control4/translations/fr.json index 1b3803499de..7d9bd88a810 100644 --- a/homeassistant/components/control4/translations/fr.json +++ b/homeassistant/components/control4/translations/fr.json @@ -14,6 +14,16 @@ "host": "Adresse IP", "password": "Mot de passe", "username": "Nom d'utilisateur" + }, + "description": "Veuillez saisir les d\u00e9tails de votre compte Control4 et l'adresse IP de votre contr\u00f4leur local." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Secondes entre les mises \u00e0 jour" } } } diff --git a/homeassistant/components/deconz/translations/fr.json b/homeassistant/components/deconz/translations/fr.json index 4fad292f274..e873d90be17 100644 --- a/homeassistant/components/deconz/translations/fr.json +++ b/homeassistant/components/deconz/translations/fr.json @@ -26,6 +26,11 @@ "host": "Nom d'h\u00f4te ou adresse IP", "port": "Port" } + }, + "user": { + "data": { + "host": "S\u00e9lectionnez la passerelle deCONZ d\u00e9couverte" + } } } }, diff --git a/homeassistant/components/doorbird/translations/fr.json b/homeassistant/components/doorbird/translations/fr.json index 304760dbf58..fd8bf04d29e 100644 --- a/homeassistant/components/doorbird/translations/fr.json +++ b/homeassistant/components/doorbird/translations/fr.json @@ -28,7 +28,8 @@ "init": { "data": { "events": "Liste d'\u00e9v\u00e9nements s\u00e9par\u00e9s par des virgules." - } + }, + "description": "Ajoutez un nom d'\u00e9v\u00e9nement s\u00e9par\u00e9 par des virgules pour chaque \u00e9v\u00e9nement que vous souhaitez suivre. Apr\u00e8s les avoir saisis ici, utilisez l'application DoorBird pour les affecter \u00e0 un \u00e9v\u00e9nement sp\u00e9cifique. Consultez la documentation sur https://www.home-assistant.io/integrations/doorbird/#events. Exemple: somebody_pressed_the_button, motion" } } } diff --git a/homeassistant/components/elkm1/translations/fr.json b/homeassistant/components/elkm1/translations/fr.json index 81265e587b2..618299def29 100644 --- a/homeassistant/components/elkm1/translations/fr.json +++ b/homeassistant/components/elkm1/translations/fr.json @@ -16,8 +16,10 @@ "password": "Mot de passe", "prefix": "Un pr\u00e9fixe unique (laissez vide si vous n'avez qu'un seul ElkM1).", "protocol": "Protocole", + "temperature_unit": "L'unit\u00e9 de temp\u00e9rature utilis\u00e9e par ElkM1.", "username": "Nom d'utilisateur" }, + "description": "La cha\u00eene d'adresse doit \u00eatre au format \u00abadresse [: port]\u00bb pour \u00abs\u00e9curis\u00e9\u00bb et \u00abnon s\u00e9curis\u00e9\u00bb. Exemple: '192.168.1.1'. Le port est facultatif et vaut par d\u00e9faut 2101 pour \u00abnon s\u00e9curis\u00e9\u00bb et 2601 pour \u00abs\u00e9curis\u00e9\u00bb. Pour le protocole s\u00e9rie, l'adresse doit \u00eatre au format \u00abtty [: baud]\u00bb. Exemple: '/ dev / ttyS1'. Le baud est facultatif et par d\u00e9faut \u00e0 115200.", "title": "Se connecter a Elk-M1 Control" } } diff --git a/homeassistant/components/firmata/translations/fr.json b/homeassistant/components/firmata/translations/fr.json new file mode 100644 index 00000000000..a66d58dce87 --- /dev/null +++ b/homeassistant/components/firmata/translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "Impossible de se connecter \u00e0 la carte Firmata pendant la configuration" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/fr.json b/homeassistant/components/flume/translations/fr.json index a746d793bc4..fdb7ab8ed9a 100644 --- a/homeassistant/components/flume/translations/fr.json +++ b/homeassistant/components/flume/translations/fr.json @@ -12,6 +12,7 @@ "user": { "data": { "client_id": "ID du client", + "client_secret": "Secret client", "password": "Mot de passe", "username": "Nom d'utilisateur" }, diff --git a/homeassistant/components/home_connect/translations/fr.json b/homeassistant/components/home_connect/translations/fr.json index 630960b1c91..42a0c34fe81 100644 --- a/homeassistant/components/home_connect/translations/fr.json +++ b/homeassistant/components/home_connect/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "missing_configuration": "Le composant Home Connect n'est pas configur\u00e9. Veuillez suivre la documentation." + "missing_configuration": "Le composant Home Connect n'est pas configur\u00e9. Veuillez suivre la documentation.", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )" }, "create_entry": { "default": "Authentification r\u00e9ussie avec Home Connect." diff --git a/homeassistant/components/homekit/translations/fr.json b/homeassistant/components/homekit/translations/fr.json index 3c6162e378e..4bac36a8467 100644 --- a/homeassistant/components/homekit/translations/fr.json +++ b/homeassistant/components/homekit/translations/fr.json @@ -32,6 +32,7 @@ "data": { "camera_copy": "Cam\u00e9ras prenant en charge les flux H.264 natifs" }, + "description": "V\u00e9rifiez toutes les cam\u00e9ras prenant en charge les flux H.264 natifs. Si la cam\u00e9ra ne produit pas de flux H.264, le syst\u00e8me transcodera la vid\u00e9o en H.264 pour HomeKit. Le transcodage n\u00e9cessite un processeur performant et il est peu probable qu'il fonctionne sur des ordinateurs \u00e0 carte unique.", "title": "S\u00e9lectionnez le codec vid\u00e9o de la cam\u00e9ra." }, "exclude": { diff --git a/homeassistant/components/homekit_controller/translations/fr.json b/homeassistant/components/homekit_controller/translations/fr.json index 1e5671a67af..c0e0600210b 100644 --- a/homeassistant/components/homekit_controller/translations/fr.json +++ b/homeassistant/components/homekit_controller/translations/fr.json @@ -7,6 +7,7 @@ "already_paired": "Cet accessoire est d\u00e9j\u00e0 associ\u00e9 \u00e0 un autre appareil. R\u00e9initialisez l\u2019accessoire et r\u00e9essayez.", "ignored_model": "La prise en charge de HomeKit pour ce mod\u00e8le est bloqu\u00e9e car une int\u00e9gration native plus compl\u00e8te est disponible.", "invalid_config_entry": "Cet appareil est pr\u00eat \u00e0 \u00eatre coupl\u00e9, mais il existe d\u00e9j\u00e0 une entr\u00e9e de configuration en conflit dans Home Assistant \u00e0 supprimer.", + "invalid_properties": "Propri\u00e9t\u00e9s invalides annonc\u00e9es par l'appareil.", "no_devices": "Aucun appareil non appair\u00e9 n'a pu \u00eatre trouv\u00e9" }, "error": { @@ -15,6 +16,7 @@ "max_peers_error": "L'appareil a refus\u00e9 d'ajouter le couplage car il ne dispose pas de stockage de couplage libre.", "max_tries_error": "Le p\u00e9riph\u00e9rique a refus\u00e9 d'ajouter le couplage car il a re\u00e7u plus de 100 tentatives d'authentification infructueuses.", "pairing_failed": "Une erreur non g\u00e9r\u00e9e s'est produite lors de la tentative d'appairage avec cet appareil. Il se peut qu'il s'agisse d'une panne temporaire ou que votre appareil ne soit pas pris en charge actuellement.", + "protocol_error": "Erreur de communication avec l'accessoire. L'appareil peut ne pas \u00eatre en mode d'appairage et peut n\u00e9cessiter une pression sur un bouton physique ou virtuel.", "unable_to_pair": "Impossible d'appairer, veuillez r\u00e9essayer.", "unknown_error": "L'appareil a signal\u00e9 une erreur inconnue. L'appairage a \u00e9chou\u00e9." }, @@ -39,6 +41,10 @@ "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" }, + "try_pair_later": { + "description": "Assurez-vous que l'appareil est en mode de couplage ou essayez de red\u00e9marrer l'appareil, puis continuez \u00e0 red\u00e9marrer le couplage.", + "title": "Couplage indisponible" + }, "user": { "data": { "device": "Appareil" @@ -48,5 +54,25 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button1": "Bouton 1", + "button10": "Bouton 10", + "button2": "Bouton 2", + "button3": "Bouton 3", + "button4": "Bouton 4", + "button5": "Bouton 5", + "button6": "Bouton 6", + "button7": "Bouton 7", + "button8": "Bouton 8", + "button9": "Bouton 9", + "doorbell": "Sonnette" + }, + "trigger_type": { + "double_press": "\" {subtype} \" appuy\u00e9 deux fois", + "long_press": "\" {subtype} \" enfonc\u00e9 et maintenu", + "single_press": "\" {subtype} \" press\u00e9" + } + }, "title": "Accessoire HomeKit" } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/fr.json b/homeassistant/components/hue/translations/fr.json index 99e82f1a89b..f19c5ec7a34 100644 --- a/homeassistant/components/hue/translations/fr.json +++ b/homeassistant/components/hue/translations/fr.json @@ -49,7 +49,9 @@ "trigger_type": { "remote_button_long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long", "remote_button_short_press": "bouton \"{subtype}\" est press\u00e9", - "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9" + "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", + "remote_double_button_long_press": "Les deux \"{sous-type}\" ont \u00e9t\u00e9 rel\u00e2ch\u00e9s apr\u00e8s un appui long", + "remote_double_button_short_press": "Les deux \" {subtype} \" ont \u00e9t\u00e9 rel\u00e2ch\u00e9s" } }, "options": { diff --git a/homeassistant/components/humidifier/translations/fr.json b/homeassistant/components/humidifier/translations/fr.json index 4b680bdba7f..236c3b93343 100644 --- a/homeassistant/components/humidifier/translations/fr.json +++ b/homeassistant/components/humidifier/translations/fr.json @@ -8,10 +8,12 @@ "turn_on": "Allumer {entity_name}" }, "condition_type": { + "is_mode": "{entity_name} est d\u00e9fini sur un mode sp\u00e9cifique", "is_off": "{entity_name} est d\u00e9sactiv\u00e9", "is_on": "{entity_name} est activ\u00e9" }, "trigger_type": { + "target_humidity_changed": "{nom_de_l'entit\u00e9} changement de l'humidit\u00e9 cible", "turned_off": "{entity_name} s'est \u00e9teint", "turned_on": "{entity_name} s'est allum\u00e9" } diff --git a/homeassistant/components/hunterdouglas_powerview/translations/fr.json b/homeassistant/components/hunterdouglas_powerview/translations/fr.json index a1bd06078c6..e5208ebdd68 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/fr.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/fr.json @@ -19,5 +19,6 @@ "title": "Connectez-vous au concentrateur PowerView" } } - } + }, + "title": "Hunter Douglas PowerView" } \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/fr.json b/homeassistant/components/hvv_departures/translations/fr.json index afc67b1087d..e6560da4047 100644 --- a/homeassistant/components/hvv_departures/translations/fr.json +++ b/homeassistant/components/hvv_departures/translations/fr.json @@ -35,11 +35,14 @@ "step": { "init": { "data": { + "filter": "S\u00e9lectionnez des lignes", "offset": "D\u00e9calage (minutes)", "real_time": "Utiliser des donn\u00e9es en temps r\u00e9el" }, + "description": "Modifier les options de ce capteur de d\u00e9part", "title": "Options" } } - } + }, + "title": "D\u00e9parts HVV" } \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/fr.json b/homeassistant/components/insteon/translations/fr.json index 45b85201a3c..f18df011048 100644 --- a/homeassistant/components/insteon/translations/fr.json +++ b/homeassistant/components/insteon/translations/fr.json @@ -48,14 +48,19 @@ }, "init": { "data": { - "hubv1": "Hub Version 1 (avant 2014)" + "hubv1": "Hub Version 1 (avant 2014)", + "hubv2": "Hub version 2", + "plm": "Modem PowerLink (PLM)" }, + "description": "S\u00e9lectionnez le type de modem Insteon.", "title": "Insteon" }, "plm": { "data": { "device": "Chemin du p\u00e9riph\u00e9rique USB" - } + }, + "description": "Configurez le modem Insteon PowerLink (PLM).", + "title": "Insteon PLM" }, "user": { "data": { @@ -68,21 +73,27 @@ }, "options": { "abort": { + "already_configured": "Une connexion Insteon par modem est d\u00e9j\u00e0 configur\u00e9e", "cannot_connect": "Impossible de se connecter au modem Insteon" }, "error": { "cannot_connect": "\u00c9chec de connexion", + "input_error": "Entr\u00e9es non valides, veuillez v\u00e9rifier vos valeurs.", "select_single": "S\u00e9lectionnez une option" }, "step": { "add_override": { "data": { - "cat": "Cat\u00e9gorie d'appareil (c.-\u00e0-d. 0x10)" + "address": "Adresse de l'appareil (par exemple 1a2b3c)", + "cat": "Cat\u00e9gorie d'appareil (c.-\u00e0-d. 0x10)", + "subcat": "Sous-cat\u00e9gorie de p\u00e9riph\u00e9rique (par exemple 0x0a)" }, + "description": "Ajoutez un remplacement de p\u00e9riph\u00e9rique.", "title": "Insteon" }, "add_x10": { "data": { + "housecode": "Code maison (a - p)", "platform": "Plate-forme", "steps": "Pas de gradateur (pour les appareils d'\u00e9clairage uniquement, par d\u00e9faut 22)", "unitcode": "Code de l'unit\u00e9 (1-16)" @@ -97,6 +108,7 @@ "port": "Port", "username": "Nom d'utilisateur" }, + "description": "Modifiez les informations de connexion Insteon Hub. Vous devez red\u00e9marrer Home Assistant apr\u00e8s avoir effectu\u00e9 cette modification. Cela ne change pas la configuration du Hub lui-m\u00eame. Pour modifier la configuration dans le Hub, utilisez l'application Hub.", "title": "Insteon" }, "init": { diff --git a/homeassistant/components/isy994/translations/fr.json b/homeassistant/components/isy994/translations/fr.json index 102d1fabf05..5afaf2ad830 100644 --- a/homeassistant/components/isy994/translations/fr.json +++ b/homeassistant/components/isy994/translations/fr.json @@ -29,7 +29,8 @@ "data": { "ignore_string": "Ignorer la cha\u00eene", "restore_light_state": "Restaurer la luminosit\u00e9", - "sensor_string": "Node Sensor String" + "sensor_string": "Node Sensor String", + "variable_sensor_string": "Cha\u00eene de capteur variable" }, "description": "D\u00e9finir les options pour l'int\u00e9gration ISY: \n \u2022 Node Sensor String: tout p\u00e9riph\u00e9rique ou dossier contenant \u00abNode Sensor String\u00bb dans le nom sera trait\u00e9 comme un capteur ou un capteur binaire. \n \u2022 Ignore String : tout p\u00e9riph\u00e9rique avec \u00abIgnore String\u00bb dans le nom sera ignor\u00e9. \n \u2022 Variable Sensor String : toute variable contenant \u00abVariable Sensor String\u00bb sera ajout\u00e9e en tant que capteur. \n \u2022 Restaurer la luminosit\u00e9 : si cette option est activ\u00e9e, la luminosit\u00e9 pr\u00e9c\u00e9dente sera restaur\u00e9e lors de l'allumage d'une lumi\u00e8re au lieu de la fonction int\u00e9gr\u00e9e de l'appareil.", "title": "Options ISY994" diff --git a/homeassistant/components/konnected/translations/fr.json b/homeassistant/components/konnected/translations/fr.json index be48b5e15c2..3c020967c8d 100644 --- a/homeassistant/components/konnected/translations/fr.json +++ b/homeassistant/components/konnected/translations/fr.json @@ -32,6 +32,7 @@ "not_konn_panel": "Non reconnu comme appareil Konnected.io" }, "error": { + "bad_host": "URL de substitution de l'h\u00f4te de l'API non valide", "one": "Vide", "other": "Vide" }, @@ -65,6 +66,7 @@ "7": "Zone 7", "out": "OUT" }, + "description": "D\u00e9couverte d\u2019un {model} \u00e0 {host}. S\u00e9lectionnez la configuration de base de chaque E/S ci-dessous - en fonction de l\u2019E/S, il peut permettre des capteurs binaires (contacts ouverts/proches), des capteurs num\u00e9riques (dht et ds18b20) ou des sorties commutables. Vous pourrez configurer des options d\u00e9taill\u00e9es dans les \u00e9tapes suivantes.", "title": "Configurer les E/S" }, "options_io_ext": { @@ -83,8 +85,10 @@ }, "options_misc": { "data": { + "api_host": "Remplacer l'URL de l'h\u00f4te de l'API (facultatif)", "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" + "discovery": "R\u00e9pondre aux demandes de d\u00e9couverte sur votre r\u00e9seau", + "override_api_host": "Remplacer l'URL par d\u00e9faut du panneau h\u00f4te de l'API Home Assistant" }, "description": "Veuillez s\u00e9lectionner le comportement souhat\u00e9 de votre panneau", "title": "Configurer divers" diff --git a/homeassistant/components/lutron_caseta/translations/fr.json b/homeassistant/components/lutron_caseta/translations/fr.json index 02c48b586f2..0674172e975 100644 --- a/homeassistant/components/lutron_caseta/translations/fr.json +++ b/homeassistant/components/lutron_caseta/translations/fr.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Pont Cas\u00e9ta d\u00e9j\u00e0 configur\u00e9.", + "cannot_connect": "Installation annul\u00e9e du pont Cas\u00e9ta en raison d'un \u00e9chec de connexion." + }, "error": { "cannot_connect": "\u00c9chec de la connexion \u00e0 la passerelle Cas\u00e9ta; v\u00e9rifiez la configuration de votre h\u00f4te et de votre certificat." }, diff --git a/homeassistant/components/mikrotik/translations/fr.json b/homeassistant/components/mikrotik/translations/fr.json index 0049c69632f..31fad4b081c 100644 --- a/homeassistant/components/mikrotik/translations/fr.json +++ b/homeassistant/components/mikrotik/translations/fr.json @@ -27,6 +27,7 @@ "device_tracker": { "data": { "arp_ping": "Activer le ping ARP", + "detection_time": "Intervalle de consid\u00e9ration de pr\u00e9sence", "force_dhcp": "Forcer l'analyse \u00e0 l'aide de DHCP" } } diff --git a/homeassistant/components/netatmo/translations/fr.json b/homeassistant/components/netatmo/translations/fr.json index fd4072dc54a..fe8fc74d273 100644 --- a/homeassistant/components/netatmo/translations/fr.json +++ b/homeassistant/components/netatmo/translations/fr.json @@ -3,6 +3,7 @@ "abort": { "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.", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "create_entry": { diff --git a/homeassistant/components/nightscout/translations/fr.json b/homeassistant/components/nightscout/translations/fr.json index 9ed1ea1bfd1..7e8552836a2 100644 --- a/homeassistant/components/nightscout/translations/fr.json +++ b/homeassistant/components/nightscout/translations/fr.json @@ -7,6 +7,7 @@ "cannot_connect": "\u00c9chec de connexion", "unknown": "Erreur inattendue" }, + "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nuheat/translations/fr.json b/homeassistant/components/nuheat/translations/fr.json index da5c3260f2a..f0e912805ed 100644 --- a/homeassistant/components/nuheat/translations/fr.json +++ b/homeassistant/components/nuheat/translations/fr.json @@ -16,6 +16,7 @@ "serial_number": "Num\u00e9ro de s\u00e9rie du thermostat.", "username": "Nom d'utilisateur" }, + "description": "Vous devrez obtenir le num\u00e9ro de s\u00e9rie ou l'identifiant num\u00e9rique de votre thermostat en vous connectant \u00e0 https://MyNuHeat.com et en s\u00e9lectionnant votre (vos) thermostat (s).", "title": "Connectez-vous au NuHeat" } } diff --git a/homeassistant/components/nws/translations/fr.json b/homeassistant/components/nws/translations/fr.json index 86db25ebd11..568179cf9fa 100644 --- a/homeassistant/components/nws/translations/fr.json +++ b/homeassistant/components/nws/translations/fr.json @@ -15,6 +15,7 @@ "longitude": "Longitude", "station": "Code de la station METAR" }, + "description": "Si aucun code de station METAR n'est sp\u00e9cifi\u00e9, la latitude et la longitude seront utilis\u00e9es pour trouver la station la plus proche.", "title": "Se connecter au National Weather Service" } } diff --git a/homeassistant/components/ovo_energy/translations/fr.json b/homeassistant/components/ovo_energy/translations/fr.json index f8bdce3d3a2..b900e75787f 100644 --- a/homeassistant/components/ovo_energy/translations/fr.json +++ b/homeassistant/components/ovo_energy/translations/fr.json @@ -11,6 +11,7 @@ "password": "Mot de passe", "username": "Nom d'utilisateur" }, + "description": "Configurez une instance OVO Energy pour acc\u00e9der \u00e0 votre consommation d'\u00e9nergie.", "title": "Ajouter un compte OVO Energy" } } diff --git a/homeassistant/components/panasonic_viera/translations/fr.json b/homeassistant/components/panasonic_viera/translations/fr.json index 4ee07e94ad4..9f8c9b672e5 100644 --- a/homeassistant/components/panasonic_viera/translations/fr.json +++ b/homeassistant/components/panasonic_viera/translations/fr.json @@ -1,8 +1,14 @@ { "config": { "abort": { + "already_configured": "Ce t\u00e9l\u00e9viseur Panasonic Viera est d\u00e9j\u00e0 configur\u00e9.", + "not_connected": "La connexion \u00e0 distance avec votre t\u00e9l\u00e9viseur Panasonic Viera a \u00e9t\u00e9 perdue. Consultez les journaux pour plus d'informations.", "unknown": "Une erreur inconnue est survenue. Veuillez consulter les journaux pour obtenir plus de d\u00e9tails." }, + "error": { + "invalid_pin_code": "Le code PIN que vous avez entr\u00e9 n'est pas valide", + "not_connected": "Impossible d'\u00e9tablir une connexion \u00e0 distance avec votre t\u00e9l\u00e9viseur Panasonic Viera" + }, "step": { "pairing": { "data": { diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/fr.json b/homeassistant/components/pvpc_hourly_pricing/translations/fr.json index 4048e357751..5386529e43a 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/fr.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/fr.json @@ -9,6 +9,7 @@ "name": "Nom du capteur", "tariff": "Tarif souscrit (1, 2, ou 3 p\u00e9riodes)" }, + "description": "Ce capteur utilise l'API officielle pour obtenir la [tarification horaire de l'\u00e9lectricit\u00e9 (PVPC)] (https://www.esios.ree.es/es/pvpc) en Espagne. \n Pour une explication plus pr\u00e9cise, visitez la [documentation d'int\u00e9gration] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n S\u00e9lectionnez le tarif contract\u00e9 en fonction du nombre de p\u00e9riodes de facturation par jour: \n - 1 p\u00e9riode: normale \n - 2 p\u00e9riodes: discrimination (tarif \u00e0 la nuit) \n - 3 p\u00e9riodes: voiture \u00e9lectrique (tarif \u00e0 la nuit sur 3 p\u00e9riodes)", "title": "S\u00e9lection tarifaire" } } diff --git a/homeassistant/components/roomba/translations/fr.json b/homeassistant/components/roomba/translations/fr.json index a8ff72b2ed5..1ec97dd3842 100644 --- a/homeassistant/components/roomba/translations/fr.json +++ b/homeassistant/components/roomba/translations/fr.json @@ -12,6 +12,7 @@ "host": "Nom d'h\u00f4te ou adresse IP", "password": "Mot de passe" }, + "description": "La r\u00e9cup\u00e9ration du BLID et du mot de passe est actuellement un processus manuel. Veuillez suivre les \u00e9tapes d\u00e9crites dans la documentation \u00e0 l'adresse: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", "title": "Se connecter \u00e0 l'appareil" } } diff --git a/homeassistant/components/rpi_power/translations/fr.json b/homeassistant/components/rpi_power/translations/fr.json new file mode 100644 index 00000000000..7e4fd715ee0 --- /dev/null +++ b/homeassistant/components/rpi_power/translations/fr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Impossible de trouver la classe syst\u00e8me n\u00e9cessaire pour ce composant, assurez-vous que votre noyau est r\u00e9cent et que le mat\u00e9riel est pris en charge", + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "step": { + "confirm": { + "description": "Voulez-vous commencer la configuration ?" + } + } + }, + "title": "V\u00e9rificateur d'alimentation Raspberry Pi" +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/fr.json b/homeassistant/components/simplisafe/translations/fr.json index 9f62eb20823..54f89eb3ab4 100644 --- a/homeassistant/components/simplisafe/translations/fr.json +++ b/homeassistant/components/simplisafe/translations/fr.json @@ -1,17 +1,26 @@ { "config": { "abort": { - "already_configured": "Ce compte SimpliSafe est d\u00e9j\u00e0 utilis\u00e9." + "already_configured": "Ce compte SimpliSafe est d\u00e9j\u00e0 utilis\u00e9.", + "reauth_successful": "SimpliSafe a \u00e9t\u00e9 r\u00e9 authentifi\u00e9 avec succ\u00e8s." }, "error": { "identifier_exists": "Compte d\u00e9j\u00e0 enregistr\u00e9", - "invalid_credentials": "Informations d'identification invalides" + "invalid_credentials": "Informations d'identification invalides", + "still_awaiting_mfa": "En attente de clic sur le message \u00e9lectronique d'authentification multi facteur", + "unknown": "Erreur inattendue" }, "step": { + "mfa": { + "description": "V\u00e9rifiez votre messagerie pour un lien de SimpliSafe. Apr\u00e8s avoir v\u00e9rifi\u00e9 le lien, revenez ici pour terminer l'installation de l'int\u00e9gration.", + "title": "Authentification multi facteur SimpliSafe" + }, "reauth_confirm": { "data": { "password": "Mot de passe" - } + }, + "description": "Votre jeton d'acc\u00e8s a expir\u00e9 ou a \u00e9t\u00e9 r\u00e9voqu\u00e9. Entrez votre mot de passe pour r\u00e9 associer votre compte.", + "title": "Relier le compte SimpliSafe" }, "user": { "data": { diff --git a/homeassistant/components/smappee/translations/fr.json b/homeassistant/components/smappee/translations/fr.json index d1dca6c5895..4bbed6615ca 100644 --- a/homeassistant/components/smappee/translations/fr.json +++ b/homeassistant/components/smappee/translations/fr.json @@ -2,22 +2,33 @@ "config": { "abort": { "already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_configured_local_device": "Le ou les p\u00e9riph\u00e9riques locaux sont d\u00e9j\u00e0 configur\u00e9s. Veuillez les supprimer avant de configurer un appareil cloud.", "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." + "connection_error": "\u00c9chec de la connexion \u00e0 l'appareil Smappee.", + "invalid_mdns": "Appareil non pris en charge pour l'int\u00e9gration Smappee.", + "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )" }, + "flow_title": "Smappee: {name}", "step": { "environment": { "data": { "environment": "Environnement" - } + }, + "description": "Configurez votre Smappee pour qu'il s'int\u00e8gre \u00e0 Home Assistant." }, "local": { "data": { "host": "H\u00f4te" - } + }, + "description": "Entrez l'h\u00f4te pour lancer l'int\u00e9gration locale Smappee" }, "pick_implementation": { "title": "Choisissez la m\u00e9thode d'authentification" + }, + "zeroconf_confirm": { + "description": "Voulez-vous ajouter l'appareil Smappee avec le num\u00e9ro de s\u00e9rie \u00ab {serialnumber} \u00bb \u00e0 Home Assistant?", + "title": "Appareil Smappee d\u00e9couvert" } } } diff --git a/homeassistant/components/smartthings/translations/fr.json b/homeassistant/components/smartthings/translations/fr.json index c355c437689..6051cbbabce 100644 --- a/homeassistant/components/smartthings/translations/fr.json +++ b/homeassistant/components/smartthings/translations/fr.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "invalid_webhook_url": "Home Assistant n'est pas configur\u00e9 correctement pour recevoir les mises \u00e0 jour de SmartThings. L'URL du webhook n'est pas valide: \n > {webhook_url} \n\n Veuillez mettre \u00e0 jour votre configuration en suivant les [instructions] ({component_url}), red\u00e9marrez Home Assistant et r\u00e9essayez.", + "no_available_locations": "Il n'y a pas d'emplacements SmartThings disponibles \u00e0 configurer dans Home Assistant." + }, "error": { "app_setup_error": "Impossible de configurer la SmartApp. Veuillez r\u00e9essayer.", "token_forbidden": "Le jeton n'a pas les port\u00e9es OAuth requises.", @@ -15,12 +19,14 @@ "data": { "access_token": "Jeton d'acc\u00e8s" }, + "description": "Veuillez saisir un [jeton d'acc\u00e8s personnel] {token_url} ( {token_url} ) qui a \u00e9t\u00e9 cr\u00e9\u00e9 conform\u00e9ment aux [instructions] ( {component_url} ). Cela sera utilis\u00e9 pour cr\u00e9er l'int\u00e9gration de Home Assistant dans votre compte SmartThings.", "title": "Entrer un jeton d'acc\u00e8s personnel" }, "select_location": { "data": { "location_id": "Emplacement" }, + "description": "Veuillez s\u00e9lectionner l'emplacement SmartThings que vous souhaitez ajouter \u00e0 Home Assistant. Nous ouvrirons alors une nouvelle fen\u00eatre et vous demanderons de vous connecter et d'autoriser l'installation de l'int\u00e9gration de Home Assistant \u00e0 l'emplacement s\u00e9lectionn\u00e9.", "title": "S\u00e9lectionnez l'emplacement" }, "user": { diff --git a/homeassistant/components/sms/translations/fr.json b/homeassistant/components/sms/translations/fr.json index 25c08a1e7fe..b4c479cfd50 100644 --- a/homeassistant/components/sms/translations/fr.json +++ b/homeassistant/components/sms/translations/fr.json @@ -12,7 +12,8 @@ "user": { "data": { "device": "Appareil" - } + }, + "title": "Se connecter au modem" } } } diff --git a/homeassistant/components/somfy/translations/fr.json b/homeassistant/components/somfy/translations/fr.json index 5df01fe951b..63d9eacc0bc 100644 --- a/homeassistant/components/somfy/translations/fr.json +++ b/homeassistant/components/somfy/translations/fr.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "Vous ne pouvez configurer qu'un seul compte Somfy.", "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration d'url autoriser.", - "missing_configuration": "Le composant Somfy n'est pas configur\u00e9. Veuillez suivre la documentation." + "missing_configuration": "Le composant Somfy n'est pas configur\u00e9. Veuillez suivre la documentation.", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )" }, "create_entry": { "default": "Authentifi\u00e9 avec succ\u00e8s avec Somfy." diff --git a/homeassistant/components/songpal/translations/fr.json b/homeassistant/components/songpal/translations/fr.json index a5f52833f4e..5975bb955fa 100644 --- a/homeassistant/components/songpal/translations/fr.json +++ b/homeassistant/components/songpal/translations/fr.json @@ -11,6 +11,11 @@ "step": { "init": { "description": "Voulez-vous configurer {name} ({host})?" + }, + "user": { + "data": { + "endpoint": "Terminaison" + } } } } diff --git a/homeassistant/components/spider/translations/fr.json b/homeassistant/components/spider/translations/fr.json index 959a28add76..8658343db6a 100644 --- a/homeassistant/components/spider/translations/fr.json +++ b/homeassistant/components/spider/translations/fr.json @@ -12,7 +12,8 @@ "data": { "password": "Mot de passe", "username": "Nom d'utilisateur" - } + }, + "title": "Connectez-vous avec le compte mijn.ithodaalderop.nl" } } } diff --git a/homeassistant/components/spotify/translations/fr.json b/homeassistant/components/spotify/translations/fr.json index 629aa5c681f..251c85920aa 100644 --- a/homeassistant/components/spotify/translations/fr.json +++ b/homeassistant/components/spotify/translations/fr.json @@ -4,6 +4,7 @@ "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.", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", "reauth_account_mismatch": "Le compte Spotify authentifi\u00e9 ne correspond pas au compte requis pour la r\u00e9-authentification." }, "create_entry": { diff --git a/homeassistant/components/squeezebox/translations/fr.json b/homeassistant/components/squeezebox/translations/fr.json index 9e7445a4a32..6119bc34f8d 100644 --- a/homeassistant/components/squeezebox/translations/fr.json +++ b/homeassistant/components/squeezebox/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", + "no_server_found": "Aucun serveur LMS trouv\u00e9." }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -17,7 +18,8 @@ "password": "Mot de passe", "port": "Port", "username": "Username" - } + }, + "title": "Modifier les informations de connexion" }, "user": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/fr.json b/homeassistant/components/synology_dsm/translations/fr.json index 6c8b627a76b..1c411591f1a 100644 --- a/homeassistant/components/synology_dsm/translations/fr.json +++ b/homeassistant/components/synology_dsm/translations/fr.json @@ -39,5 +39,15 @@ "title": "Synology DSM" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Minutes entre les scans", + "timeout": "D\u00e9lai d'expiration (secondes)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tado/translations/fr.json b/homeassistant/components/tado/translations/fr.json index 18196a4bf13..0ebbe4054a1 100644 --- a/homeassistant/components/tado/translations/fr.json +++ b/homeassistant/components/tado/translations/fr.json @@ -25,6 +25,7 @@ "data": { "fallback": "Activer le mode restreint." }, + "description": "Le mode de repli passera au programme intelligent au prochain changement de programme apr\u00e8s avoir ajust\u00e9 manuellement une zone.", "title": "Ajustez les options de Tado." } } diff --git a/homeassistant/components/toon/translations/fr.json b/homeassistant/components/toon/translations/fr.json index c3384f56319..caeed852d0a 100644 --- a/homeassistant/components/toon/translations/fr.json +++ b/homeassistant/components/toon/translations/fr.json @@ -5,7 +5,8 @@ "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.", "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_agreements": "Ce compte n'a pas d'affichages Toon.", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )" }, "step": { "agreement": { diff --git a/homeassistant/components/unifi/translations/fr.json b/homeassistant/components/unifi/translations/fr.json index 21d422ace4d..931b8c6c38c 100644 --- a/homeassistant/components/unifi/translations/fr.json +++ b/homeassistant/components/unifi/translations/fr.json @@ -26,13 +26,16 @@ "step": { "client_control": { "data": { - "block_client": "Clients contr\u00f4l\u00e9s par acc\u00e8s r\u00e9seau" + "block_client": "Clients contr\u00f4l\u00e9s par acc\u00e8s r\u00e9seau", + "poe_clients": "Autoriser le contr\u00f4le POE des clients" }, + "description": "Configurer les contr\u00f4les client \n\n Cr\u00e9ez des interrupteurs pour les num\u00e9ros de s\u00e9rie pour lesquels vous souhaitez contr\u00f4ler l'acc\u00e8s au r\u00e9seau.", "title": "Options UniFi 2/3" }, "device_tracker": { "data": { "detection_time": "Temps en secondes depuis la derni\u00e8re vue avant de consid\u00e9rer comme absent", + "ignore_wired_bug": "D\u00e9sactiver la logique de bogue filaire UniFi", "ssid_filter": "S\u00e9lectionnez les SSID pour suivre les clients sans fil", "track_clients": "Suivre les clients du r\u00e9seau", "track_devices": "Suivre les p\u00e9riph\u00e9riques r\u00e9seau (p\u00e9riph\u00e9riques Ubiquiti)", diff --git a/homeassistant/components/vera/translations/fr.json b/homeassistant/components/vera/translations/fr.json index 9cc6d871dd7..e54613cdb78 100644 --- a/homeassistant/components/vera/translations/fr.json +++ b/homeassistant/components/vera/translations/fr.json @@ -7,8 +7,11 @@ "step": { "user": { "data": { + "exclude": "Identifiants d'appareils Vera \u00e0 exclure de Home Assistant.", + "lights": "Identifiants des interrupteurs vera \u00e0 traiter comme des lumi\u00e8res dans Home Assistant", "vera_controller_url": "URL du contr\u00f4leur" }, + "description": "Fournissez une URL de contr\u00f4leur Vera ci-dessous. Cela devrait ressembler \u00e0 ceci : http://192.168.1.161:3480.", "title": "Configurer le contr\u00f4leur Vera" } } @@ -16,6 +19,11 @@ "options": { "step": { "init": { + "data": { + "exclude": "Identifiants d'appareils Vera \u00e0 exclure de Home Assistant.", + "lights": "Identifiants des interrupteurs vera \u00e0 traiter comme des lumi\u00e8res dans Home Assistant" + }, + "description": "Consultez la documentation de vera pour plus de d\u00e9tails sur les param\u00e8tres facultatifs: https://www.home-assistant.io/integrations/vera/. Remarque: toute modification ici n\u00e9cessitera un red\u00e9marrage du serveur Home Assistant. Pour effacer les valeurs, entrez un espace.", "title": "Options du contr\u00f4leur Vera" } } diff --git a/homeassistant/components/vilfo/translations/fr.json b/homeassistant/components/vilfo/translations/fr.json index 37774921920..b6790d98d39 100644 --- a/homeassistant/components/vilfo/translations/fr.json +++ b/homeassistant/components/vilfo/translations/fr.json @@ -14,6 +14,7 @@ "access_token": "Jeton d'Acc\u00e8s", "host": "Nom d'h\u00f4te ou adresse IP" }, + "description": "Configurez l'int\u00e9gration du routeur Vilfo. Vous avez besoin du nom d'h\u00f4te / IP de votre routeur Vilfo et d'un jeton d'acc\u00e8s API. Pour plus d'informations sur cette int\u00e9gration et comment obtenir ces d\u00e9tails, visitez: https://www.home-assistant.io/integrations/vilfo", "title": "Connectez-vous au routeur Vilfo" } } diff --git a/homeassistant/components/volumio/translations/fr.json b/homeassistant/components/volumio/translations/fr.json index 03844ccf99b..6dee3fb9faf 100644 --- a/homeassistant/components/volumio/translations/fr.json +++ b/homeassistant/components/volumio/translations/fr.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "Impossible de se connecter au Volumio d\u00e9couvert" }, "error": { "cannot_connect": "\u00c9chec de connexion", "unknown": "Erreur inattendue" }, "step": { + "discovery_confirm": { + "description": "Voulez-vous ajouter Volumio (` {name} `) \u00e0 Home Assistant?", + "title": "Volumio d\u00e9couvert" + }, "user": { "data": { "host": "H\u00f4te", diff --git a/homeassistant/components/withings/translations/fr.json b/homeassistant/components/withings/translations/fr.json index 7ddb1049abb..a51efff7276 100644 --- a/homeassistant/components/withings/translations/fr.json +++ b/homeassistant/components/withings/translations/fr.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Configuration mise \u00e0 jour pour le profil.", "authorize_url_timeout": "D\u00e9lai d'expiration g\u00e9n\u00e9rant une URL d'autorisation.", - "missing_configuration": "L'int\u00e9gration Withings n'est pas configur\u00e9e. Veuillez suivre la documentation." + "missing_configuration": "L'int\u00e9gration Withings n'est pas configur\u00e9e. Veuillez suivre la documentation.", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )" }, "create_entry": { "default": "Authentifi\u00e9 avec succ\u00e8s \u00e0 Withings pour le profil s\u00e9lectionn\u00e9." diff --git a/homeassistant/components/wolflink/translations/fr.json b/homeassistant/components/wolflink/translations/fr.json index aa84ec33d8c..6e3348c3647 100644 --- a/homeassistant/components/wolflink/translations/fr.json +++ b/homeassistant/components/wolflink/translations/fr.json @@ -9,11 +9,18 @@ "unknown": "Erreur inattendue" }, "step": { + "device": { + "data": { + "device_name": "Appareil" + }, + "title": "S\u00e9lectionnez l'appareil WOLF" + }, "user": { "data": { "password": "Mot de passe", "username": "Nom d'utilisateur" - } + }, + "title": "Connexion WOLF SmartSet" } } } diff --git a/homeassistant/components/wolflink/translations/sensor.en.json b/homeassistant/components/wolflink/translations/sensor.en.json index ea60e233907..bd505e845ae 100644 --- a/homeassistant/components/wolflink/translations/sensor.en.json +++ b/homeassistant/components/wolflink/translations/sensor.en.json @@ -50,7 +50,7 @@ "parallelbetrieb": "Parallel mode", "partymodus": "Party mode", "perm_cooling": "PermCooling", - "permanent": "Permament", + "permanent": "Permanent", "permanentbetrieb": "Permanent mode", "reduzierter_betrieb": "Limited mode", "rt_abschaltung": "RT shutdown", diff --git a/homeassistant/components/wolflink/translations/sensor.fr.json b/homeassistant/components/wolflink/translations/sensor.fr.json new file mode 100644 index 00000000000..57cd8435f35 --- /dev/null +++ b/homeassistant/components/wolflink/translations/sensor.fr.json @@ -0,0 +1,87 @@ +{ + "state": { + "wolflink__state": { + "1_x_warmwasser": "1 x ECS", + "abgasklappe": "Amortisseur de gaz de combustion", + "absenkbetrieb": "Mode Recul", + "absenkstop": "Arr\u00eat de recul", + "aktiviert": "Activ\u00e9", + "antilegionellenfunktion": "Fonction anti-l\u00e9gionelle", + "at_abschaltung": "Arr\u00eat OT", + "at_frostschutz": "Protection antigel OT", + "aus": "D\u00e9sactiv\u00e9", + "auto": "Auto", + "auto_off_cool": "AutoOffCool", + "auto_on_cool": "AutoOnCool", + "automatik_aus": "Arr\u00eat automatique", + "automatik_ein": "Mise en marche automatique", + "bereit_keine_ladung": "Pr\u00eat, pas de chargement", + "betrieb_ohne_brenner": "Travaille sans br\u00fbleur", + "cooling": "Refroidissement", + "deaktiviert": "Inactif", + "dhw_prior": "Priorit\u00e9 ECS", + "eco": "\u00c9co", + "ein": "Activ\u00e9", + "estrichtrocknung": "S\u00e9chage de chape", + "externe_deaktivierung": "D\u00e9sactivation externe", + "fernschalter_ein": "Contr\u00f4le \u00e0 distance activ\u00e9", + "frost_heizkreis": "Gel du circuit de chauffage", + "frost_warmwasser": "Gel ECS", + "frostschutz": "Protection antigel", + "gasdruck": "Pression du gaz", + "glt_betrieb": "Mode BMS", + "gradienten_uberwachung": "Surveillance de gradient", + "heizbetrieb": "Mode chauffage", + "heizgerat_mit_speicher": "Chaudi\u00e8re \u00e0 cylindre", + "heizung": "En chauffe", + "initialisierung": "Initialisation", + "kalibration": "\u00c9talonnage", + "kalibration_heizbetrieb": "Calibrage du mode de chauffage", + "kalibration_kombibetrieb": "\u00c9talonnage du mode Combi", + "kalibration_warmwasserbetrieb": "Calibrage ECS", + "kaskadenbetrieb": "Fonctionnement en cascade", + "kombibetrieb": "Mode Combi", + "kombigerat": "Chaudi\u00e8re combi", + "kombigerat_mit_solareinbindung": "Chaudi\u00e8re mixte avec int\u00e9gration solaire", + "mindest_kombizeit": "Temps combin\u00e9 minimum", + "nachlauf_heizkreispumpe": "Pompe du circuit de chauffage en marche", + "nachspulen": "Apr\u00e8s rin\u00e7age", + "nur_heizgerat": "Chaudi\u00e8re seulement", + "parallelbetrieb": "Mode parall\u00e8le", + "partymodus": "Mode festif", + "perm_cooling": "Refroidissement permanent", + "permanent": "Permanent", + "permanentbetrieb": "Mode permanent", + "reduzierter_betrieb": "Mode limit\u00e9", + "rt_abschaltung": "Arr\u00eat RT", + "rt_frostschutz": "Protection antigel RT", + "ruhekontakt": "Contact de repos", + "schornsteinfeger": "Test d'\u00e9missions", + "smart_grid": "SmartGrid", + "smart_home": "SmartHome", + "softstart": "D\u00e9marrage progressif", + "solarbetrieb": "Mode solaire", + "sparbetrieb": "Mode \u00e9conomie", + "sparen": "\u00c9conomie", + "spreizung_hoch": "dT trop large", + "spreizung_kf": "Spread KF", + "stabilisierung": "Stabilisation", + "standby": "En veille", + "start": "D\u00e9marrer", + "storung": "Faute", + "taktsperre": "Anti-cycle", + "telefonfernschalter": "Commutateur \u00e0 distance t\u00e9l\u00e9phonique", + "test": "Test", + "tpw": "TPW", + "urlaubsmodus": "Mode vacances", + "ventilprufung": "Test de valve", + "vorspulen": "Rin\u00e7age d'entr\u00e9e", + "warmwasser": "ECS", + "warmwasser_schnellstart": "D\u00e9marrage rapide ECS", + "warmwasserbetrieb": "Mode ECS", + "warmwassernachlauf": "ECS en marche", + "warmwasservorrang": "Priorit\u00e9 ECS", + "zunden": "Allumage" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/ca.json b/homeassistant/components/zoneminder/translations/ca.json new file mode 100644 index 00000000000..cdbe838c1f1 --- /dev/null +++ b/homeassistant/components/zoneminder/translations/ca.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "auth_fail": "Nom d'usuari i/o contrasenya incorrectes.", + "connection_error": "No s'ha pogut connectar al servidor ZoneMinder." + }, + "create_entry": { + "default": "S'ha afegit el servidor ZoneMinder." + }, + "error": { + "auth_fail": "Nom d'usuari i/o contrasenya incorrectes.", + "connection_error": "No s'ha pogut connectar al servidor ZoneMinder." + }, + "flow_title": "ZoneMinder", + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3 i port (ex: 10.10.0.4:8010)", + "password": "Contrasenya", + "path": "Ruta de ZM", + "path_zms": "Ruta de ZMS", + "ssl": "Utilitza SSL per a les connexions a ZoneMinder", + "username": "Nom d'usuari", + "verify_ssl": "Verifica el certificat SSL" + }, + "title": "Afegeix un servidor ZoneMinder." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/en.json b/homeassistant/components/zoneminder/translations/en.json new file mode 100644 index 00000000000..3bf249c0786 --- /dev/null +++ b/homeassistant/components/zoneminder/translations/en.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "auth_fail": "Username or password is incorrect.", + "connection_error": "Failed to connect to a ZoneMinder server." + }, + "create_entry": { + "default": "ZoneMinder server added." + }, + "error": { + "auth_fail": "Username or password is incorrect.", + "connection_error": "Failed to connect to a ZoneMinder server." + }, + "flow_title": "ZoneMinder", + "step": { + "user": { + "data": { + "host": "Host and Port (ex 10.10.0.4:8010)", + "password": "Password", + "path": "ZM Path", + "path_zms": "ZMS Path", + "ssl": "Use SSL for connections to ZoneMinder", + "username": "Username", + "verify_ssl": "Verify SSL Certificate" + }, + "title": "Add ZoneMinder Server." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/es.json b/homeassistant/components/zoneminder/translations/es.json new file mode 100644 index 00000000000..b7fa166864a --- /dev/null +++ b/homeassistant/components/zoneminder/translations/es.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "auth_fail": "Nombre de usuario o contrase\u00f1a incorrectos.", + "connection_error": "No se pudo conectar con un servidor ZoneMinder." + }, + "create_entry": { + "default": "Servidor ZoneMinder a\u00f1adido." + }, + "error": { + "auth_fail": "Nombre de usuario o contrase\u00f1a incorrectos.", + "connection_error": "No se pudo conectar con un servidor ZoneMinder." + }, + "flow_title": "ZoneMinder", + "step": { + "user": { + "data": { + "host": "Host y Puerto (ej 10.10.0.4:8010)", + "password": "Contrase\u00f1a", + "path": "Ruta ZM", + "path_zms": "Ruta ZMS", + "ssl": "Usar SSL para conexiones a ZoneMinder", + "username": "Usuario", + "verify_ssl": "Verificar certificado SSL" + }, + "title": "A\u00f1adir Servidor ZoneMinder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/fr.json b/homeassistant/components/zoneminder/translations/fr.json new file mode 100644 index 00000000000..0919811fa2b --- /dev/null +++ b/homeassistant/components/zoneminder/translations/fr.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "auth_fail": "L'identifiant ou le mot de passe est incorrect.", + "connection_error": "\u00c9chec de la connexion \u00e0 un serveur ZoneMinder." + }, + "create_entry": { + "default": "Serveur Zoneminder ajout\u00e9." + }, + "error": { + "auth_fail": "L'identifiant ou le mot de passe est incorrect.", + "connection_error": "\u00c9chec de la connexion \u00e0 un serveur ZoneMinder." + }, + "flow_title": "ZoneMinder", + "step": { + "user": { + "data": { + "host": "H\u00f4te et port (ex 10.10.0.4:8010)", + "password": "Mot de passe", + "path": "Chemin ZM", + "path_zms": "Chemin ZMS", + "ssl": "Utiliser SSL pour les connexions \u00e0 ZoneMinder", + "username": "Nom d'utilisateur", + "verify_ssl": "V\u00e9rifier le certificat SSL" + }, + "title": "Ajouter le serveur ZoneMinder." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/nl.json b/homeassistant/components/zoneminder/translations/nl.json new file mode 100644 index 00000000000..e6e487320ac --- /dev/null +++ b/homeassistant/components/zoneminder/translations/nl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "verify_ssl": "Verifieer SSLcertificaat" + }, + "title": "Voeg ZoneMinder server toe." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/no.json b/homeassistant/components/zoneminder/translations/no.json new file mode 100644 index 00000000000..f711b6eaf06 --- /dev/null +++ b/homeassistant/components/zoneminder/translations/no.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "auth_fail": "Brukernavn eller passord er feil.", + "connection_error": "Kunne ikke koble til en ZoneMinder-server." + }, + "create_entry": { + "default": "ZoneMinder-serveren er lagt til." + }, + "error": { + "auth_fail": "Brukernavn eller passord er feil.", + "connection_error": "Kunne ikke koble til en ZoneMinder-server." + }, + "flow_title": "ZoneMinder", + "step": { + "user": { + "data": { + "host": "Vert og port (ex 10.10.0.4:8010)", + "password": "Passord", + "path": "ZM-bane", + "path_zms": "ZMS-bane", + "ssl": "Bruk SSL for tilkoblinger til ZoneMinder", + "username": "Brukernavn", + "verify_ssl": "Bekreft SSL-sertifikat" + }, + "title": "Legg til ZoneMinder Server." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/ru.json b/homeassistant/components/zoneminder/translations/ru.json new file mode 100644 index 00000000000..e7ac29e58ab --- /dev/null +++ b/homeassistant/components/zoneminder/translations/ru.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "auth_fail": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 ZoneMinder." + }, + "create_entry": { + "default": "\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u0441\u0435\u0440\u0432\u0435\u0440 ZoneMinder." + }, + "error": { + "auth_fail": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 ZoneMinder." + }, + "flow_title": "ZoneMinder", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442 \u0438 \u043f\u043e\u0440\u0442 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 10.10.0.4:8010)", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "path": "\u041f\u0443\u0442\u044c \u043a ZM", + "path_zms": "\u041f\u0443\u0442\u044c \u043a ZMS", + "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", + "username": "\u041b\u043e\u0433\u0438\u043d", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "title": "ZoneMinder" + } + } + } +} \ No newline at end of file From 100d2369d5ec3df5fb05f52f306464e03b276c52 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 17 Sep 2020 21:13:40 -0400 Subject: [PATCH 217/514] Use async_on_remove for vizio listeners (#40185) --- homeassistant/components/vizio/media_player.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index ab5386c151b..ac688241682 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -143,7 +143,6 @@ class VizioDevice(MediaPlayerEntity): ) -> None: """Initialize Vizio device.""" self._config_entry = config_entry - self._async_unsub_listeners = [] self._apps_coordinator = apps_coordinator self._name = name @@ -312,14 +311,14 @@ class VizioDevice(MediaPlayerEntity): 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( + self.async_on_remove( self._config_entry.add_update_listener( self._async_send_update_options_signal ) ) # Register callback for update event - self._async_unsub_listeners.append( + self.async_on_remove( async_dispatcher_connect( self.hass, self._config_entry.entry_id, self._async_update_options ) @@ -333,17 +332,10 @@ class VizioDevice(MediaPlayerEntity): self.async_write_ha_state() if self._device_class == DEVICE_CLASS_TV: - self._async_unsub_listeners.append( + self.async_on_remove( 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: - listener() - - self._async_unsub_listeners.clear() - @property def available(self) -> bool: """Return the availabiliity of the device.""" From 689f1519c0cf2b9e927b2b7013598e1ab81c77f4 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 18 Sep 2020 03:16:29 +0200 Subject: [PATCH 218/514] Add cgtobi to sonos code owners (#40204) * Add myself to Sonos code owners * Run hassfest --- CODEOWNERS | 1 + homeassistant/components/sonos/manifest.json | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 8804380fc72..b33246766d5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -400,6 +400,7 @@ homeassistant/components/soma/* @ratsept homeassistant/components/somfy/* @tetienne homeassistant/components/sonarr/* @ctalkington homeassistant/components/songpal/* @rytilahti @shenxn +homeassistant/components/sonos/* @cgtobi homeassistant/components/spaceapi/* @fabaff homeassistant/components/speedtestdotnet/* @rohankapoorcom @engrbm87 homeassistant/components/spider/* @peternijssen diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index efad23ee1f2..daded59cadf 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -9,5 +9,7 @@ "st": "urn:schemas-upnp-org:device:ZonePlayer:1" } ], - "codeowners": [] + "codeowners": [ + "@cgtobi" + ] } From 4efefd7fba69473e63526edc260b3ae9cd0a86bd Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 18 Sep 2020 03:17:22 +0200 Subject: [PATCH 219/514] Add cgtobi to kodi code owners (#40202) * Add myself to Kodi code owners * Run hassfest --- CODEOWNERS | 2 +- homeassistant/components/kodi/manifest.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b33246766d5..613617e9942 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -226,7 +226,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/* @OnFreund +homeassistant/components/kodi/* @OnFreund @cgtobi homeassistant/components/konnected/* @heythisisnate @kit-klein homeassistant/components/lametric/* @robbiet480 homeassistant/components/launch_library/* @ludeeus diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index da4daf85ada..a9df9718f8c 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -4,7 +4,8 @@ "documentation": "https://www.home-assistant.io/integrations/kodi", "requirements": ["pykodi==0.2.0"], "codeowners": [ - "@OnFreund" + "@OnFreund", + "@cgtobi" ], "zeroconf": ["_xbmc-jsonrpc-h._tcp.local."], "config_flow": true From ed4ab403deaed9e8c95e0db728477fcb012bf4fa Mon Sep 17 00:00:00 2001 From: Julius Mittenzwei Date: Fri, 18 Sep 2020 03:18:55 +0200 Subject: [PATCH 220/514] Upgrade pyvlx to 0.2.17 (#40182) --- homeassistant/components/velux/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/velux/manifest.json b/homeassistant/components/velux/manifest.json index 73306bca7b5..0fdbfb64999 100644 --- a/homeassistant/components/velux/manifest.json +++ b/homeassistant/components/velux/manifest.json @@ -2,6 +2,6 @@ "domain": "velux", "name": "Velux", "documentation": "https://www.home-assistant.io/integrations/velux", - "requirements": ["pyvlx==0.2.16"], + "requirements": ["pyvlx==0.2.17"], "codeowners": ["@Julius2342"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1683ef87d61..90a2affdf11 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1837,7 +1837,7 @@ pyvesync==1.1.0 pyvizio==0.1.56 # homeassistant.components.velux -pyvlx==0.2.16 +pyvlx==0.2.17 # homeassistant.components.volumio pyvolumio==0.1.2 From d0a5feda4475fbe48364f8cf84156c9a5e9e3abe Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 18 Sep 2020 07:18:56 -0400 Subject: [PATCH 221/514] Fix Vizio async_unload_entry bug (#40210) --- homeassistant/components/vizio/__init__.py | 2 +- tests/components/vizio/test_init.py | 27 +++++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index 25960da72cf..a7a9c404f74 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -94,7 +94,7 @@ async def async_unload_entry( and entry.data[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV for entry in hass.config_entries.async_entries(DOMAIN) ): - hass.data[DOMAIN].pop(CONF_APPS) + hass.data[DOMAIN].pop(CONF_APPS, None) if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py index 33764de8696..cd611662597 100644 --- a/tests/components/vizio/test_init.py +++ b/tests/components/vizio/test_init.py @@ -6,7 +6,7 @@ from homeassistant.components.vizio.const import DOMAIN from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component -from .const import MOCK_USER_VALID_TV_CONFIG, UNIQUE_ID +from .const import MOCK_SPEAKER_CONFIG, MOCK_USER_VALID_TV_CONFIG, UNIQUE_ID from tests.common import MockConfigEntry @@ -24,12 +24,12 @@ async def test_setup_component( assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1 -async def test_load_and_unload( +async def test_tv_load_and_unload( hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture, ) -> None: - """Test loading and unloading entry.""" + """Test loading and unloading TV entry.""" config_entry = MockConfigEntry( domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG, unique_id=UNIQUE_ID ) @@ -43,3 +43,24 @@ async def test_load_and_unload( await hass.async_block_till_done() assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0 assert DOMAIN not in hass.data + + +async def test_speaker_load_and_unload( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_update: pytest.fixture, +) -> None: + """Test loading and unloading speaker entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, unique_id=UNIQUE_ID + ) + config_entry.add_to_hass(hass) + 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 await config_entry.async_unload(hass) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0 + assert DOMAIN not in hass.data From 2d9019d4b2befa3947e1fbb5218d04d4977109ee Mon Sep 17 00:00:00 2001 From: Tomasz Date: Fri, 18 Sep 2020 14:32:33 +0200 Subject: [PATCH 222/514] Fix shelly sensor names (#39854) --- homeassistant/components/shelly/binary_sensor.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index c9a13249aa8..1460c62f153 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -20,31 +20,31 @@ SENSORS = { name="Overheating", device_class=DEVICE_CLASS_PROBLEM ), ("device", "overpower"): BlockAttributeDescription( - name="Over Power", device_class=DEVICE_CLASS_PROBLEM + name="Overpowering", device_class=DEVICE_CLASS_PROBLEM ), ("light", "overpower"): BlockAttributeDescription( - name="Over Power", device_class=DEVICE_CLASS_PROBLEM + name="Overpowering", device_class=DEVICE_CLASS_PROBLEM ), ("relay", "overpower"): BlockAttributeDescription( - name="Over Power", device_class=DEVICE_CLASS_PROBLEM + name="Overpowering", device_class=DEVICE_CLASS_PROBLEM ), ("sensor", "dwIsOpened"): BlockAttributeDescription( name="Door", device_class=DEVICE_CLASS_OPENING ), ("sensor", "flood"): BlockAttributeDescription( - name="flood", device_class=DEVICE_CLASS_MOISTURE + name="Flood", device_class=DEVICE_CLASS_MOISTURE ), ("sensor", "gas"): BlockAttributeDescription( - name="gas", + 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 + name="Smoke", device_class=DEVICE_CLASS_SMOKE ), ("sensor", "vibration"): BlockAttributeDescription( - name="vibration", device_class=DEVICE_CLASS_VIBRATION + name="Vibration", device_class=DEVICE_CLASS_VIBRATION ), } From 3f514da285b55320bcd19694031b06313c9aac3c Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 18 Sep 2020 16:24:14 +0300 Subject: [PATCH 223/514] Fix kodi.call_method (#40236) --- homeassistant/components/kodi/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index f13d5301625..a1b9987b5c9 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -714,7 +714,7 @@ class KodiEntity(MediaPlayerEntity): _LOGGER.debug("Run API method %s, kwargs=%s", method, kwargs) result_ok = False try: - result = self._kodi.call_method(method, **kwargs) + result = await self._kodi.call_method(method, **kwargs) result_ok = True except jsonrpc_base.jsonrpc.ProtocolError as exc: result = exc.args[2]["error"] From 4cd876f1593f01efc0acdfaf999c830e6513ef8e Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 18 Sep 2020 16:28:02 +0300 Subject: [PATCH 224/514] Fix coolmaster.info (#40240) --- 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 58bd51fca4d..85bd3b1893f 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.1"], + "requirements": ["pycoolmasternet-async==0.1.2"], "codeowners": ["@OnFreund"] } diff --git a/requirements_all.txt b/requirements_all.txt index 90a2affdf11..95c2ac5c745 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1277,7 +1277,7 @@ pycocotools==2.0.1 pycomfoconnect==0.3 # homeassistant.components.coolmaster -pycoolmasternet-async==0.1.1 +pycoolmasternet-async==0.1.2 # homeassistant.components.avri pycountry==19.8.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 38e922b1b43..e6a0fc8811a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -619,7 +619,7 @@ pybotvac==0.0.17 pychromecast==7.2.1 # homeassistant.components.coolmaster -pycoolmasternet-async==0.1.1 +pycoolmasternet-async==0.1.2 # homeassistant.components.avri pycountry==19.8.18 From ff3fd63eeac454c1c7c19fd4507ea09dfa65f081 Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 18 Sep 2020 16:28:39 +0300 Subject: [PATCH 225/514] Handle systems without groups (#40238) --- 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 80b132b0fb2..7f13af252f3 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.3.0" + "pyrisco==0.3.1" ], "codeowners": [ "@OnFreund" diff --git a/requirements_all.txt b/requirements_all.txt index 95c2ac5c745..5ea8f2343d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1595,7 +1595,7 @@ pyrecswitch==1.0.2 pyrepetier==3.0.5 # homeassistant.components.risco -pyrisco==0.3.0 +pyrisco==0.3.1 # homeassistant.components.sabnzbd pysabnzbd==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6a0fc8811a..1511a855683 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -772,7 +772,7 @@ pyps4-2ndscreen==1.1.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.3.0 +pyrisco==0.3.1 # homeassistant.components.acer_projector # homeassistant.components.zha From 976d8f7abe9dbf12c08254f7b8c07caf235783a4 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Fri, 18 Sep 2020 10:29:26 -0300 Subject: [PATCH 226/514] Handle an unsupported device in the Broadlink config flow (#40242) --- .../components/broadlink/config_flow.py | 16 ++++++++- .../components/broadlink/strings.json | 1 + .../components/broadlink/translations/en.json | 1 + tests/components/broadlink/__init__.py | 10 ++++++ .../components/broadlink/test_config_flow.py | 35 +++++++++++++++++++ 5 files changed, 62 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index 284a9bffe19..4dfc80c6fe9 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -11,7 +11,7 @@ from broadlink.exceptions import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE from homeassistant.helpers import config_validation as cv @@ -20,6 +20,7 @@ from .const import ( # pylint: disable=unused-import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN, + DOMAINS_AND_TYPES, ) from .helpers import format_mac @@ -36,6 +37,19 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_set_device(self, device, raise_on_progress=True): """Define a device for the config flow.""" + supported_types = { + device_type + for _, device_types in DOMAINS_AND_TYPES + for device_type in device_types + } + if device.type not in supported_types: + LOGGER.error( + "Unsupported device: %s. If it worked before, please open " + "an issue at https://github.com/home-assistant/core/issues", + hex(device.devtype), + ) + raise data_entry_flow.AbortFlow("not_supported") + await self.async_set_unique_id( device.mac.hex(), raise_on_progress=raise_on_progress ) diff --git a/homeassistant/components/broadlink/strings.json b/homeassistant/components/broadlink/strings.json index 44cb1801ede..d17c639469a 100644 --- a/homeassistant/components/broadlink/strings.json +++ b/homeassistant/components/broadlink/strings.json @@ -35,6 +35,7 @@ "already_in_progress": "There is already a configuration flow in progress for this device", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_host": "Invalid hostname or IP address", + "not_supported": "Device not supported", "unknown": "[%key:common::config_flow::error::unknown%]" }, "error": { diff --git a/homeassistant/components/broadlink/translations/en.json b/homeassistant/components/broadlink/translations/en.json index fa3feb88008..bd8dfd0c403 100644 --- a/homeassistant/components/broadlink/translations/en.json +++ b/homeassistant/components/broadlink/translations/en.json @@ -5,6 +5,7 @@ "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", + "not_supported": "Device not supported", "unknown": "Unexpected error" }, "error": { diff --git a/tests/components/broadlink/__init__.py b/tests/components/broadlink/__init__.py index 4051f94c0d3..87a23d7074c 100644 --- a/tests/components/broadlink/__init__.py +++ b/tests/components/broadlink/__init__.py @@ -56,6 +56,16 @@ BROADLINK_DEVICES = { 20025, 5, ), + "Kitchen": ( # Not supported. + "192.168.0.64", + "34ea34b61d2c", + "LB1", + "Broadlink", + "SmartBulb", + 0x504E, + 57, + 5, + ), } diff --git a/tests/components/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py index 2bb00c347c7..a7660b03da5 100644 --- a/tests/components/broadlink/test_config_flow.py +++ b/tests/components/broadlink/test_config_flow.py @@ -175,6 +175,25 @@ async def test_flow_user_device_not_found(hass): assert result["errors"] == {"base": "cannot_connect"} +async def test_flow_user_device_not_supported(hass): + """Test we handle a device not supported in the user step.""" + device = get_device("Kitchen") + mock_api = device.get_mock_api() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": device.host}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "not_supported" + + async def test_flow_user_network_unreachable(hass): """Test we handle a network unreachable in the user step.""" result = await hass.config_entries.flow.async_init( @@ -634,6 +653,22 @@ async def test_flow_import_device_not_found(hass): assert result["reason"] == "cannot_connect" +async def test_flow_import_device_not_supported(hass): + """Test we handle a device not supported in the import step.""" + device = get_device("Kitchen") + mock_api = device.get_mock_api() + + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"host": device.host}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "not_supported" + + async def test_flow_import_invalid_ip_address(hass): """Test we handle an invalid IP address in the import step.""" with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.EINVAL, None)): From ddbcfe83dd26fc6c7e193134e91405fed9dc1cf4 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Fri, 18 Sep 2020 15:29:40 +0200 Subject: [PATCH 227/514] Catch TypeError in strptime() template helper (#40226) --- homeassistant/helpers/template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 79f372e3ff1..bef6323d10c 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -908,7 +908,7 @@ def strptime(string, fmt): """Parse a time string to datetime.""" try: return datetime.strptime(string, fmt) - except (ValueError, AttributeError): + except (ValueError, AttributeError, TypeError): return string From 27f11a10223a93006db47db84c05b1f6255f279a Mon Sep 17 00:00:00 2001 From: Markus Bong <2Fake1987@gmail.com> Date: Fri, 18 Sep 2020 15:30:42 +0200 Subject: [PATCH 228/514] Use default values in advanced options in devolo home control (#40216) --- homeassistant/components/devolo_home_control/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py index 93b2cfc11e5..e596a7628f6 100644 --- a/homeassistant/components/devolo_home_control/config_flow.py +++ b/homeassistant/components/devolo_home_control/config_flow.py @@ -38,8 +38,8 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.data_schema = { vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_MYDEVOLO): str, - vol.Required(CONF_HOMECONTROL): str, + vol.Required(CONF_MYDEVOLO, default=DEFAULT_MYDEVOLO): str, + vol.Required(CONF_HOMECONTROL, default=DEFAULT_MPRM): str, } if user_input is None: return self._show_form(user_input) From 2f7b6bfa2d5423564045f8ac76ec280ee0b3e3d3 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Fri, 18 Sep 2020 10:31:25 -0300 Subject: [PATCH 229/514] Fix RM mini 3 update manager (#40215) --- homeassistant/components/broadlink/updater.py | 22 +++++++++++++++++++ tests/components/broadlink/__init__.py | 3 +++ tests/components/broadlink/test_device.py | 4 ++-- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py index bd124d1e1ac..8b6f1316f52 100644 --- a/homeassistant/components/broadlink/updater.py +++ b/homeassistant/components/broadlink/updater.py @@ -1,12 +1,15 @@ """Support for fetching data from Broadlink devices.""" from abc import ABC, abstractmethod from datetime import timedelta +from functools import partial import logging +import broadlink as blk from broadlink.exceptions import ( AuthorizationError, BroadlinkException, CommandNotSupportedError, + DeviceOfflineError, StorageError, ) @@ -18,6 +21,9 @@ _LOGGER = logging.getLogger(__name__) def get_update_manager(device): """Return an update manager for a given Broadlink device.""" + if device.api.model.startswith("RM mini"): + return BroadlinkRMMini3UpdateManager(device) + update_managers = { "A1": BroadlinkA1UpdateManager, "MP1": BroadlinkMP1UpdateManager, @@ -95,6 +101,22 @@ class BroadlinkMP1UpdateManager(BroadlinkUpdateManager): return await self.device.async_request(self.device.api.check_power) +class BroadlinkRMMini3UpdateManager(BroadlinkUpdateManager): + """Manages updates for Broadlink RM mini 3 devices.""" + + async def async_fetch_data(self): + """Fetch data from the device.""" + hello = partial( + blk.discover, + discover_ip_address=self.device.api.host[0], + timeout=self.device.api.timeout, + ) + devices = await self.device.hass.async_add_executor_job(hello) + if not devices: + raise DeviceOfflineError("The device is offline") + return {} + + class BroadlinkRMUpdateManager(BroadlinkUpdateManager): """Manages updates for Broadlink RM2 and RM4 devices.""" diff --git a/tests/components/broadlink/__init__.py b/tests/components/broadlink/__init__.py index 87a23d7074c..86756c922f1 100644 --- a/tests/components/broadlink/__init__.py +++ b/tests/components/broadlink/__init__.py @@ -95,6 +95,9 @@ class BroadlinkDevice: with patch( "homeassistant.components.broadlink.device.blk.gendevice", return_value=mock_api, + ), patch( + "homeassistant.components.broadlink.updater.blk.discover", + return_value=[mock_api], ): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/broadlink/test_device.py b/tests/components/broadlink/test_device.py index a226c5e484f..1e68921e9bd 100644 --- a/tests/components/broadlink/test_device.py +++ b/tests/components/broadlink/test_device.py @@ -164,7 +164,7 @@ async def test_device_setup_update_authorization_error(hass): async def test_device_setup_update_authentication_error(hass): """Test we handle an authentication error in the update step.""" - device = get_device("Living Room") + device = get_device("Garage") mock_api = device.get_mock_api() mock_api.check_sensors.side_effect = blke.AuthorizationError() mock_api.auth.side_effect = (None, blke.AuthenticationError()) @@ -190,7 +190,7 @@ async def test_device_setup_update_authentication_error(hass): async def test_device_setup_update_broadlink_exception(hass): """Test we handle a Broadlink exception in the update step.""" - device = get_device("Living Room") + device = get_device("Garage") mock_api = device.get_mock_api() mock_api.check_sensors.side_effect = blke.BroadlinkException() From 0a0d44a0b597250a5bd56d45ec67d2d2383ba595 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Fri, 18 Sep 2020 09:32:38 -0400 Subject: [PATCH 230/514] Disable async on Apprise (#40213) --- homeassistant/components/apprise/notify.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/apprise/notify.py b/homeassistant/components/apprise/notify.py index 0c8c5b26eec..f999da94531 100644 --- a/homeassistant/components/apprise/notify.py +++ b/homeassistant/components/apprise/notify.py @@ -29,8 +29,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the Apprise notification service.""" - # Create our object - a_obj = apprise.Apprise() + # Create our Apprise Asset Object + asset = apprise.AppriseAsset(async_mode=False) + + # Create our Apprise Instance (reference our asset) + a_obj = apprise.Apprise(asset=asset) if config.get(CONF_FILE): # Sourced from a Configuration File From 3612df91e1586e3a1a686573acf84b6468ba3c50 Mon Sep 17 00:00:00 2001 From: MeIchthys <10717998+meichthys@users.noreply.github.com> Date: Fri, 18 Sep 2020 09:34:17 -0400 Subject: [PATCH 231/514] Fix Nextcloud sensors becoming unavailable (#40212) --- homeassistant/components/nextcloud/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py index 12c17e6081d..ff94fd708db 100644 --- a/homeassistant/components/nextcloud/__init__.py +++ b/homeassistant/components/nextcloud/__init__.py @@ -100,7 +100,6 @@ def setup(hass, config): _LOGGER.error("Nextcloud setup failed - Check configuration") hass.data[DOMAIN] = get_data_points(ncm.data) - hass.data[DOMAIN]["instance"] = conf[CONF_URL] def nextcloud_update(event_time): """Update data from nextcloud api.""" @@ -111,6 +110,7 @@ def setup(hass, config): return False hass.data[DOMAIN] = get_data_points(ncm.data) + hass.data[DOMAIN]["instance"] = conf[CONF_URL] # Update sensors on time interval track_time_interval(hass, nextcloud_update, conf[CONF_SCAN_INTERVAL]) From 7f072a5ca8910be2990d48d931978c3ea9ccf7f8 Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 18 Sep 2020 18:30:46 +0300 Subject: [PATCH 232/514] Fix Kodi discovery title (#40247) --- homeassistant/components/kodi/config_flow.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index f10dcbb2d28..067ee8be476 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -116,6 +116,9 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } ) + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context.update({"title_placeholders": {CONF_NAME: self._name}}) + try: await validate_http(self.hass, self._get_data()) await validate_ws(self.hass, self._get_data()) @@ -129,8 +132,6 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _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): From 2858c8dcc798aa356d70ae2f220946872bce5b92 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Fri, 18 Sep 2020 08:39:12 -0700 Subject: [PATCH 233/514] Fix high CPU usage in vera integration. (#40249) --- homeassistant/components/vera/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index a6afcce65b3..b41d289e6b3 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -3,6 +3,6 @@ "name": "Vera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/vera", - "requirements": ["pyvera==0.3.9"], + "requirements": ["pyvera==0.3.10"], "codeowners": ["@vangorra"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5ea8f2343d2..aac425355d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1825,7 +1825,7 @@ pyuptimerobot==0.0.5 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.3.9 +pyvera==0.3.10 # homeassistant.components.versasense pyversasense==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1511a855683..f950be78f68 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -857,7 +857,7 @@ pytraccar==0.9.0 pytradfri[async]==7.0.2 # homeassistant.components.vera -pyvera==0.3.9 +pyvera==0.3.10 # homeassistant.components.vesync pyvesync==1.1.0 From b25bb789168d567bd0c3d3e4c13612806f71ad30 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 18 Sep 2020 19:12:10 +0200 Subject: [PATCH 234/514] Updated frontend to 20200918.0 (#40253) --- 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 96d2a51cb09..4368bff8d0f 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==20200917.1"], + "requirements": ["home-assistant-frontend==20200918.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e5978245bac..0f88f88469b 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==20200917.1 +home-assistant-frontend==20200918.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 aac425355d3..57eb1507a87 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==20200917.1 +home-assistant-frontend==20200918.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f950be78f68..bc254b5871e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -373,7 +373,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200917.1 +home-assistant-frontend==20200918.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 46b86f4a2f879a0e8a386732d80bac2dac57efe0 Mon Sep 17 00:00:00 2001 From: timkoers Date: Fri, 18 Sep 2020 19:33:37 +0200 Subject: [PATCH 235/514] Add UniFi Uptime sensor (#40058) * Added UniFi Uptime sensor Added the UniFi uptime data as a sensor. Untested. * Update sensor.py Updated code as a result of the tests. * Changed timestamp format and device class Converted state to iso timestamp and changed device class to DEVICE_CLASS_TIMESTAMP. * Updated unit of measurement to None * Added import * Update homeassistant/components/unifi/sensor.py Co-authored-by: Martin Hjelmare * Removed whitespace * Added the uptime sensors option to the config flow * All the unit tests should be there now * Whoops * Fixed translation * Properly formatted the code * Flake8 really has angel eyes * Black should also be satisfied now * Should have satisfied all static code analysis tools * Fixed add uptime sensor function * Fixed overintendation * Fixed unit tests * Made a spelling mistake during editing of unit tests * Test verifies if utc time is correct * Converted to iso format * Converted unit test to iso format * Unit test sensor json had the wrong uptime name * Added options_updated handler * Fixed remove sensors unit test * Update homeassistant/components/unifi/sensor.py Co-authored-by: Robert Svensson * Update homeassistant/components/unifi/sensor.py Co-authored-by: Robert Svensson * Update test_device_tracker.py Removed uptime from the devices * Fixed black formatting issue * I think the code coverage should be good now * Trying to add the sensors again * Using signals to hopefully trigger the controller to add them again * Forgot import * Sorted components * fixed isort comments * Removed CLASS and DEVICE_CLASS * Added TYPE again * Removed double underscores Co-authored-by: Martin Hjelmare Co-authored-by: Robert Svensson --- homeassistant/components/unifi/config_flow.py | 7 +- homeassistant/components/unifi/const.py | 2 + homeassistant/components/unifi/controller.py | 9 ++ homeassistant/components/unifi/sensor.py | 60 +++++++++++++- homeassistant/components/unifi/strings.json | 3 +- .../components/unifi/translations/nl.json | 3 +- tests/components/unifi/test_config_flow.py | 8 +- tests/components/unifi/test_controller.py | 3 + tests/components/unifi/test_sensor.py | 83 +++++++++++++++++-- 9 files changed, 163 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 6115821b000..e40fb30a62c 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -16,6 +16,7 @@ import homeassistant.helpers.config_validation as cv from .const import ( CONF_ALLOW_BANDWIDTH_SENSORS, + CONF_ALLOW_UPTIME_SENSORS, CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, @@ -312,7 +313,11 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): vol.Optional( CONF_ALLOW_BANDWIDTH_SENSORS, default=self.controller.option_allow_bandwidth_sensors, - ): bool + ): bool, + vol.Optional( + CONF_ALLOW_UPTIME_SENSORS, + default=self.controller.option_allow_uptime_sensors, + ): bool, } ), ) diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index 803a892647f..42d160f2dea 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -12,6 +12,7 @@ CONF_SITE_ID = "site" UNIFI_WIRELESS_CLIENTS = "unifi_wireless_clients" CONF_ALLOW_BANDWIDTH_SENSORS = "allow_bandwidth_sensors" +CONF_ALLOW_UPTIME_SENSORS = "allow_uptime_sensors" CONF_BLOCK_CLIENT = "block_client" CONF_DETECTION_TIME = "detection_time" CONF_IGNORE_WIRED_BUG = "ignore_wired_bug" @@ -22,6 +23,7 @@ CONF_TRACK_WIRED_CLIENTS = "track_wired_clients" CONF_SSID_FILTER = "ssid_filter" DEFAULT_ALLOW_BANDWIDTH_SENSORS = False +DEFAULT_ALLOW_UPTIME_SENSORS = False DEFAULT_IGNORE_WIRED_BUG = False DEFAULT_POE_CLIENTS = True DEFAULT_TRACK_CLIENTS = True diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 7c30a34f58f..6fc5b3d9ed7 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -33,6 +33,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( CONF_ALLOW_BANDWIDTH_SENSORS, + CONF_ALLOW_UPTIME_SENSORS, CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, @@ -45,6 +46,7 @@ from .const import ( CONF_TRACK_WIRED_CLIENTS, CONTROLLER_ID, DEFAULT_ALLOW_BANDWIDTH_SENSORS, + DEFAULT_ALLOW_UPTIME_SENSORS, DEFAULT_DETECTION_TIME, DEFAULT_IGNORE_WIRED_BUG, DEFAULT_POE_CLIENTS, @@ -184,6 +186,13 @@ class UniFiController: CONF_ALLOW_BANDWIDTH_SENSORS, DEFAULT_ALLOW_BANDWIDTH_SENSORS ) + @property + def option_allow_uptime_sensors(self): + """Config entry option to allow uptime sensors.""" + return self.config_entry.options.get( + CONF_ALLOW_UPTIME_SENSORS, DEFAULT_ALLOW_UPTIME_SENSORS + ) + @callback def async_unifi_signalling_callback(self, signal, data): """Handle messages back from UniFi library.""" diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 8fdb0ac1461..59aff09811f 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -1,10 +1,11 @@ """Support for bandwidth sensors with UniFi clients.""" import logging -from homeassistant.components.sensor import DOMAIN +from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP, DOMAIN from homeassistant.const import DATA_MEGABYTES from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +import homeassistant.util.dt as dt_util from .const import DOMAIN as UNIFI_DOMAIN from .unifi_client import UniFiClient @@ -13,6 +14,7 @@ LOGGER = logging.getLogger(__name__) RX_SENSOR = "rx" TX_SENSOR = "tx" +UPTIME_SENSOR = "uptime" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -22,7 +24,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up sensors for UniFi integration.""" controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - controller.entities[DOMAIN] = {RX_SENSOR: set(), TX_SENSOR: set()} + controller.entities[DOMAIN] = { + RX_SENSOR: set(), + TX_SENSOR: set(), + UPTIME_SENSOR: set(), + } @callback def items_added( @@ -30,7 +36,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -> None: """Update the values of the controller.""" if controller.option_allow_bandwidth_sensors: - add_entities(controller, async_add_entities, clients) + add_bandwith_entities(controller, async_add_entities, clients) + + if controller.option_allow_uptime_sensors: + add_uptime_entities(controller, async_add_entities, clients) for signal in (controller.signal_update, controller.signal_options_update): controller.listeners.append(async_dispatcher_connect(hass, signal, items_added)) @@ -39,7 +48,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback -def add_entities(controller, async_add_entities, clients): +def add_bandwith_entities(controller, async_add_entities, clients): """Add new sensor entities from the controller.""" sensors = [] @@ -55,6 +64,22 @@ def add_entities(controller, async_add_entities, clients): async_add_entities(sensors) +@callback +def add_uptime_entities(controller, async_add_entities, clients): + """Add new sensor entities from the controller.""" + sensors = [] + + for mac in clients: + if mac in controller.entities[DOMAIN][UniFiUpTimeSensor.TYPE]: + continue + + client = controller.api.clients[mac] + sensors.append(UniFiUpTimeSensor(client, controller)) + + if sensors: + async_add_entities(sensors) + + class UniFiBandwidthSensor(UniFiClient): """UniFi bandwidth sensor base class.""" @@ -100,3 +125,30 @@ class UniFiTxBandwidthSensor(UniFiBandwidthSensor): if self._is_wired: return self.client.wired_tx_bytes / 1000000 return self.client.tx_bytes / 1000000 + + +class UniFiUpTimeSensor(UniFiClient): + """UniFi uptime sensor.""" + + DOMAIN = DOMAIN + TYPE = UPTIME_SENSOR + + @property + def device_class(self) -> str: + """Return device class.""" + return DEVICE_CLASS_TIMESTAMP + + @property + def name(self) -> str: + """Return the name of the client.""" + return f"{super().name} {self.TYPE.capitalize()}" + + @property + def state(self) -> int: + """Return the uptime of the client.""" + return dt_util.utc_from_timestamp(float(self.client.uptime)).isoformat() + + async def options_updated(self) -> None: + """Config entry options are updated, remove entity if option is disabled.""" + if not self.controller.option_allow_uptime_sensors: + await self.remove_item({self.client.mac}) diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index 95d273278bd..ba0b3952dd0 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -54,7 +54,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Bandwidth usage sensors for network clients" + "allow_bandwidth_sensors": "Bandwidth usage sensors for network clients", + "allow_uptime_sensors": "Uptime sensors for network clients" }, "description": "Configure statistics sensors", "title": "UniFi options 3/3" diff --git a/homeassistant/components/unifi/translations/nl.json b/homeassistant/components/unifi/translations/nl.json index 37a6148d377..aeeaba60f0f 100644 --- a/homeassistant/components/unifi/translations/nl.json +++ b/homeassistant/components/unifi/translations/nl.json @@ -58,7 +58,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Maak bandbreedtegebruiksensoren voor netwerkclients" + "allow_bandwidth_sensors": "Maak bandbreedtegebruiksensoren voor netwerkclients", + "allow_uptime_sensors": "Maak uptime-sensoren voor netwerkclients" }, "description": "Configureer statistische sensoren", "title": "UniFi-opties 3/3" diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index a1af12dfb76..aec6ebed664 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -4,6 +4,7 @@ import aiounifi from homeassistant import data_entry_flow from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, + CONF_ALLOW_UPTIME_SENSORS, CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, @@ -341,7 +342,11 @@ async def test_advanced_option_flow(hass): assert result["step_id"] == "statistics_sensors" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_ALLOW_BANDWIDTH_SENSORS: True} + result["flow_id"], + user_input={ + CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: True, + }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -355,6 +360,7 @@ async def test_advanced_option_flow(hass): CONF_POE_CLIENTS: False, CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]], CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: True, } diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 5600f454336..5fee4a85f9a 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -13,6 +13,7 @@ from homeassistant.components.unifi.const import ( CONF_CONTROLLER, CONF_SITE_ID, DEFAULT_ALLOW_BANDWIDTH_SENSORS, + DEFAULT_ALLOW_UPTIME_SENSORS, DEFAULT_DETECTION_TIME, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, @@ -49,6 +50,7 @@ CONTROLLER_HOST = { "sw_port": 1, "wired-rx_bytes": 1234000000, "wired-tx_bytes": 5678000000, + "uptime": 1562600160, } CONTROLLER_DATA = { @@ -175,6 +177,7 @@ async def test_controller_setup(hass): assert controller.site_role == SITES[controller.site_name]["role"] assert controller.option_allow_bandwidth_sensors == DEFAULT_ALLOW_BANDWIDTH_SENSORS + assert controller.option_allow_uptime_sensors == DEFAULT_ALLOW_UPTIME_SENSORS assert isinstance(controller.option_block_clients, list) assert controller.option_track_clients == DEFAULT_TRACK_CLIENTS assert controller.option_track_devices == DEFAULT_TRACK_DEVICES diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 2d5fbe96e0f..690b9d77899 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -8,10 +8,12 @@ from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, + CONF_ALLOW_UPTIME_SENSORS, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, DOMAIN as UNIFI_DOMAIN, ) +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component from .test_controller import setup_unifi_integration @@ -29,6 +31,7 @@ CLIENTS = [ "sw_port": 1, "wired-rx_bytes": 1234000000, "wired-tx_bytes": 5678000000, + "uptime": 1600094505, }, { "hostname": "Wireless client hostname", @@ -42,6 +45,7 @@ CLIENTS = [ "sw_port": 2, "rx_bytes": 1234000000, "tx_bytes": 5678000000, + "uptime": 1600094505, }, ] @@ -61,7 +65,10 @@ 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}, + options={ + CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: True, + }, ) assert len(controller.mock_requests) == 4 @@ -74,6 +81,7 @@ async def test_sensors(hass): hass, options={ CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: True, CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False, }, @@ -81,7 +89,7 @@ async def test_sensors(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6 wired_client_rx = hass.states.get("sensor.wired_client_name_rx") assert wired_client_rx.state == "1234.0" @@ -89,16 +97,23 @@ async def test_sensors(hass): wired_client_tx = hass.states.get("sensor.wired_client_name_tx") assert wired_client_tx.state == "5678.0" + wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime") + assert wired_client_uptime.state == "2020-09-14T14:41:45+00:00" + wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx") assert wireless_client_rx.state == "1234.0" wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") assert wireless_client_tx.state == "5678.0" + wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime") + assert wireless_client_uptime.state == "2020-09-14T14:41:45+00:00" + clients = deepcopy(CLIENTS) clients[0]["is_wired"] = False clients[1]["rx_bytes"] = 2345000000 clients[1]["tx_bytes"] = 6789000000 + clients[1]["uptime"] = 1600180860 event = {"meta": {"message": MESSAGE_CLIENT}, "data": clients} controller.api.message_handler(event) @@ -110,9 +125,15 @@ async def test_sensors(hass): wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") assert wireless_client_tx.state == "6789.0" + wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime") + assert wireless_client_uptime.state == "2020-09-15T14:41:00+00:00" + hass.config_entries.async_update_entry( controller.config_entry, - options={CONF_ALLOW_BANDWIDTH_SENSORS: False}, + options={ + CONF_ALLOW_BANDWIDTH_SENSORS: False, + CONF_ALLOW_UPTIME_SENSORS: False, + }, ) await hass.async_block_till_done() @@ -122,9 +143,18 @@ async def test_sensors(hass): wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") assert wireless_client_tx is None + wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime") + assert wired_client_uptime is None + + wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime") + assert wireless_client_uptime is None + hass.config_entries.async_update_entry( controller.config_entry, - options={CONF_ALLOW_BANDWIDTH_SENSORS: True}, + options={ + CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: True, + }, ) await hass.async_block_till_done() @@ -134,15 +164,42 @@ async def test_sensors(hass): wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") assert wireless_client_tx.state == "6789.0" + wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime") + assert wireless_client_uptime.state == "2020-09-15T14:41:00+00:00" + + wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime") + assert wired_client_uptime.state == "2020-09-14T14:41:45+00:00" + + # Try to add the sensors again, using a signal + clients_connected = set() + devices_connected = set() + + clients_connected.add(clients[0]["mac"]) + clients_connected.add(clients[1]["mac"]) + + async_dispatcher_send( + hass, + controller.signal_update, + clients_connected, + devices_connected, + ) + + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6 + 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}, + options={ + CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: True, + }, clients_response=CLIENTS, ) - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6 assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 wired_client_rx = hass.states.get("sensor.wired_client_name_rx") @@ -150,11 +207,17 @@ async def test_remove_sensors(hass): wired_client_tx = hass.states.get("sensor.wired_client_name_tx") assert wired_client_tx is not None + wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime") + assert wired_client_uptime is not None + wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx") assert wireless_client_rx is not None wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") assert wireless_client_tx is not None + wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime") + assert wireless_client_uptime is not None + controller.api.websocket._data = { "meta": {"message": MESSAGE_CLIENT_REMOVED}, "data": [CLIENTS[0]], @@ -162,7 +225,7 @@ async def test_remove_sensors(hass): controller.api.session_handler(SIGNAL_DATA) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 wired_client_rx = hass.states.get("sensor.wired_client_name_rx") @@ -170,7 +233,13 @@ async def test_remove_sensors(hass): wired_client_tx = hass.states.get("sensor.wired_client_name_tx") assert wired_client_tx is None + wired_client_uptime = hass.states.get("sensor.wired_client_name_uptime") + assert wired_client_uptime is None + wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx") assert wireless_client_rx is not None wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") assert wireless_client_tx is not None + + wireless_client_uptime = hass.states.get("sensor.wireless_client_name_uptime") + assert wireless_client_uptime is not None From 1ba098508ce15a837ef00b5a92deb6d9ce2c5318 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Fri, 18 Sep 2020 20:19:54 +0200 Subject: [PATCH 236/514] Update velbus to 2.0.45 (#40256) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 455aa98b34c..42e8a3307dc 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["python-velbus==2.0.44"], + "requirements": ["python-velbus==2.0.45"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"] } diff --git a/requirements_all.txt b/requirements_all.txt index 57eb1507a87..2506954e8a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1776,7 +1776,7 @@ python-telnet-vlc==1.0.4 python-twitch-client==0.6.0 # homeassistant.components.velbus -python-velbus==2.0.44 +python-velbus==2.0.45 # homeassistant.components.vlc python-vlc==1.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc254b5871e..b2fc5359e50 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -842,7 +842,7 @@ python-tado==0.8.1 python-twitch-client==0.6.0 # homeassistant.components.velbus -python-velbus==2.0.44 +python-velbus==2.0.45 # homeassistant.components.awair python_awair==0.1.1 From cf89b8764b7950eac29523d3b1bd0b5ebd6d1c04 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Fri, 18 Sep 2020 11:43:36 -0700 Subject: [PATCH 237/514] Bump hangups to 0.4.11 (#40258) --- homeassistant/components/hangouts/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hangouts/manifest.json b/homeassistant/components/hangouts/manifest.json index 80eed48cde9..a2605124dc4 100644 --- a/homeassistant/components/hangouts/manifest.json +++ b/homeassistant/components/hangouts/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hangouts", "requirements": [ - "hangups==0.4.10" + "hangups==0.4.11" ], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 2506954e8a7..b328dc716da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -717,7 +717,7 @@ ha-philipsjs==0.0.8 habitipy==0.2.0 # homeassistant.components.hangouts -hangups==0.4.10 +hangups==0.4.11 # homeassistant.components.cloud hass-nabucasa==0.37.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b2fc5359e50..e580617ac15 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -352,7 +352,7 @@ griddypower==0.1.0 ha-ffmpeg==2.0 # homeassistant.components.hangouts -hangups==0.4.10 +hangups==0.4.11 # homeassistant.components.cloud hass-nabucasa==0.37.0 From 62c4e072f5e21ccd057e343c700d2f81a0a50b2e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 19 Sep 2020 00:07:12 +0000 Subject: [PATCH 238/514] [ci skip] Translation update --- .../alarmdecoder/translations/et.json | 49 +++++++++++++++++++ .../alarmdecoder/translations/sv.json | 27 ++++++++++ .../components/august/translations/et.json | 7 +++ .../binary_sensor/translations/et.json | 4 +- .../components/broadlink/translations/ca.json | 1 + .../components/broadlink/translations/et.json | 7 +++ .../components/broadlink/translations/fr.json | 1 + .../components/broadlink/translations/ru.json | 1 + .../components/broadlink/translations/sv.json | 7 +++ .../broadlink/translations/zh-Hant.json | 1 + .../components/climate/translations/et.json | 15 ++++++ .../components/dsmr/translations/fr.json | 4 ++ .../components/gogogate2/translations/fr.json | 2 +- .../homekit_controller/translations/fr.json | 10 ++-- .../homekit_controller/translations/sv.json | 15 ++++++ .../homematicip_cloud/translations/et.json | 1 + .../homematicip_cloud/translations/fr.json | 2 +- .../openweathermap/translations/et.json | 35 +++++++++++++ .../openweathermap/translations/sv.json | 34 +++++++++++++ .../components/plugwise/translations/et.json | 11 +++++ .../components/remote/translations/sv.json | 14 ++++++ .../components/rpi_power/translations/et.json | 14 ++++++ .../components/sharkiq/translations/et.json | 12 +++++ .../components/sharkiq/translations/sv.json | 15 ++++++ .../synology_dsm/translations/sv.json | 3 +- .../components/unifi/translations/ca.json | 3 +- .../components/unifi/translations/en.json | 3 +- .../components/unifi/translations/nl.json | 3 +- .../components/unifi/translations/ru.json | 3 +- .../unifi/translations/zh-Hant.json | 3 +- .../components/vacuum/translations/et.json | 4 +- .../wolflink/translations/sensor.no.json | 2 +- .../wolflink/translations/sensor.zh-Hant.json | 2 +- .../xiaomi_aqara/translations/fr.json | 2 +- .../components/yeelight/translations/sv.json | 11 +++++ .../zoneminder/translations/et.json | 29 +++++++++++ .../zoneminder/translations/sv.json | 17 +++++++ .../zoneminder/translations/zh-Hant.json | 30 ++++++++++++ .../components/zwave/translations/et.json | 4 +- 39 files changed, 385 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/alarmdecoder/translations/et.json create mode 100644 homeassistant/components/alarmdecoder/translations/sv.json create mode 100644 homeassistant/components/august/translations/et.json create mode 100644 homeassistant/components/broadlink/translations/et.json create mode 100644 homeassistant/components/broadlink/translations/sv.json create mode 100644 homeassistant/components/openweathermap/translations/et.json create mode 100644 homeassistant/components/openweathermap/translations/sv.json create mode 100644 homeassistant/components/plugwise/translations/et.json create mode 100644 homeassistant/components/rpi_power/translations/et.json create mode 100644 homeassistant/components/sharkiq/translations/et.json create mode 100644 homeassistant/components/sharkiq/translations/sv.json create mode 100644 homeassistant/components/yeelight/translations/sv.json create mode 100644 homeassistant/components/zoneminder/translations/et.json create mode 100644 homeassistant/components/zoneminder/translations/sv.json create mode 100644 homeassistant/components/zoneminder/translations/zh-Hant.json diff --git a/homeassistant/components/alarmdecoder/translations/et.json b/homeassistant/components/alarmdecoder/translations/et.json new file mode 100644 index 00000000000..c88a63eadb6 --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/et.json @@ -0,0 +1,49 @@ +{ + "config": { + "step": { + "user": { + "data": { + "protocol": "Protokoll" + } + } + } + }, + "options": { + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternatiivne \u00f6\u00f6re\u017eiim", + "auto_bypass": "Automaatne m\u00f6\u00f6daviik valvestamisel", + "code_arm_required": "Valvestamise kood" + }, + "title": "Seadista AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Muuda" + }, + "description": "Mida Te soovite muuta?", + "title": "Seadista AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF silmus", + "zone_name": "Tsooni nimi", + "zone_relayaddr": "Relee aadress", + "zone_relaychan": "Relee kanalinumber", + "zone_rfid": "RF jada\u00fchendus", + "zone_type": "Tsooni t\u00fc\u00fcp" + }, + "description": "Sisestage tsooni {zone_number} \u00fcksikasjad. Tsooni {zone_number} kustutamiseks j\u00e4tke tsooni nimi t\u00fchjaks.", + "title": "Seadista AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "Tsooni number" + }, + "description": "Sisestage tsooni number mida soovite lisada, muuta v\u00f5i eemaldada.", + "title": "Seadista AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/sv.json b/homeassistant/components/alarmdecoder/translations/sv.json new file mode 100644 index 00000000000..6c9f0dbcb43 --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "step": { + "protocol": { + "data": { + "device_path": "Enhetsv\u00e4g" + }, + "title": "Konfigurera anslutningsinst\u00e4llningar" + }, + "user": { + "data": { + "protocol": "Protokoll" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "edit_select": "Redigera" + }, + "description": "Vad vill du redigera?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/et.json b/homeassistant/components/august/translations/et.json new file mode 100644 index 00000000000..af21b9d8204 --- /dev/null +++ b/homeassistant/components/august/translations/et.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "Taasautentimine \u00f5nnestus" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/et.json b/homeassistant/components/binary_sensor/translations/et.json index a9da1be9ee2..c496976f114 100644 --- a/homeassistant/components/binary_sensor/translations/et.json +++ b/homeassistant/components/binary_sensor/translations/et.json @@ -41,8 +41,8 @@ "on": "M\u00e4rg" }, "motion": { - "off": "Puudub", - "on": "Tuvastatud" + "off": "Liikumine puudub", + "on": "Liikumine tuvastatud" }, "occupancy": { "off": "Puudub", diff --git a/homeassistant/components/broadlink/translations/ca.json b/homeassistant/components/broadlink/translations/ca.json index 3e642b0f6b5..4d2fa7a8373 100644 --- a/homeassistant/components/broadlink/translations/ca.json +++ b/homeassistant/components/broadlink/translations/ca.json @@ -5,6 +5,7 @@ "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", + "not_supported": "Dispositiu no compatible", "unknown": "Error inesperat" }, "error": { diff --git a/homeassistant/components/broadlink/translations/et.json b/homeassistant/components/broadlink/translations/et.json new file mode 100644 index 00000000000..fc7f3424b62 --- /dev/null +++ b/homeassistant/components/broadlink/translations/et.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "Seadet ei toetata" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/fr.json b/homeassistant/components/broadlink/translations/fr.json index 2bf2477f615..1d80059fb7a 100644 --- a/homeassistant/components/broadlink/translations/fr.json +++ b/homeassistant/components/broadlink/translations/fr.json @@ -5,6 +5,7 @@ "already_in_progress": "Il y a d\u00e9j\u00e0 un processus de configuration en cours pour cet appareil", "cannot_connect": "\u00c9chec de connexion", "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", + "not_supported": "Dispositif non pris en charge", "unknown": "Erreur inattendue" }, "error": { diff --git a/homeassistant/components/broadlink/translations/ru.json b/homeassistant/components/broadlink/translations/ru.json index f7d0cfab3d1..542efe753f9 100644 --- a/homeassistant/components/broadlink/translations/ru.json +++ b/homeassistant/components/broadlink/translations/ru.json @@ -5,6 +5,7 @@ "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.", + "not_supported": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { diff --git a/homeassistant/components/broadlink/translations/sv.json b/homeassistant/components/broadlink/translations/sv.json new file mode 100644 index 00000000000..38d02e42d90 --- /dev/null +++ b/homeassistant/components/broadlink/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "Enheten st\u00f6ds inte" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/zh-Hant.json b/homeassistant/components/broadlink/translations/zh-Hant.json index 741e8beb2f7..6bb8d54bd16 100644 --- a/homeassistant/components/broadlink/translations/zh-Hant.json +++ b/homeassistant/components/broadlink/translations/zh-Hant.json @@ -5,6 +5,7 @@ "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", + "not_supported": "\u8a2d\u5099\u4e0d\u652f\u63f4", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/climate/translations/et.json b/homeassistant/components/climate/translations/et.json index 1c4a6a5ff11..4cba2fc5e41 100644 --- a/homeassistant/components/climate/translations/et.json +++ b/homeassistant/components/climate/translations/et.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "set_hvac_mode": "Kliimaseadme {entity_name} re\u017eiimi muutmine", + "set_preset_mode": "Olemi {entity_name} eelseadistuse muutmine" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} on seatud kindlale kliimaseadme re\u017eiimile", + "is_preset_mode": "{entity_name} on seatud kindlale eelseadistatud re\u017eiimile" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} m\u00f5\u00f5detud niiskus muutus", + "current_temperature_changed": "{entity_name} m\u00f5\u00f5detud temperatuur muutus", + "hvac_mode_changed": "{entity_name} kliimasedame re\u017eiim on muudetud" + } + }, "state": { "_": { "auto": "Automaatne", diff --git a/homeassistant/components/dsmr/translations/fr.json b/homeassistant/components/dsmr/translations/fr.json index c4bc0d48b1a..ea382532a71 100644 --- a/homeassistant/components/dsmr/translations/fr.json +++ b/homeassistant/components/dsmr/translations/fr.json @@ -2,6 +2,10 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "step": { + "one": "", + "other": "Autre" } } } \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/fr.json b/homeassistant/components/gogogate2/translations/fr.json index 478e7e8ccf8..79f216738c4 100644 --- a/homeassistant/components/gogogate2/translations/fr.json +++ b/homeassistant/components/gogogate2/translations/fr.json @@ -15,7 +15,7 @@ "username": "Nom d'utilisateur" }, "description": "Fournissez les informations requises ci-dessous.", - "title": "Configurer GogoGate2" + "title": "Configurer GogoGate2 ou iSmartGate" } } } diff --git a/homeassistant/components/homekit_controller/translations/fr.json b/homeassistant/components/homekit_controller/translations/fr.json index c0e0600210b..9634fb784f7 100644 --- a/homeassistant/components/homekit_controller/translations/fr.json +++ b/homeassistant/components/homekit_controller/translations/fr.json @@ -20,7 +20,7 @@ "unable_to_pair": "Impossible d'appairer, veuillez r\u00e9essayer.", "unknown_error": "L'appareil a signal\u00e9 une erreur inconnue. L'appairage a \u00e9chou\u00e9." }, - "flow_title": "Accessoire HomeKit: {name}", + "flow_title": "{name} via le protocole accessoire HomeKit", "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.", @@ -34,8 +34,8 @@ "data": { "pairing_code": "Code d\u2019appairage" }, - "description": "Entrez votre code de jumelage HomeKit (au format XXX-XX-XXX) pour utiliser cet accessoire.", - "title": "Appairer avec l'accessoire HomeKit" + "description": "Le contr\u00f4leur HomeKit communique avec {name} sur le r\u00e9seau local en utilisant une connexion crypt\u00e9e s\u00e9curis\u00e9e sans contr\u00f4leur HomeKit s\u00e9par\u00e9 ou iCloud. Entrez votre code d'appariement HomeKit (au format XXX-XX-XXX) pour utiliser cet accessoire. Ce code se trouve g\u00e9n\u00e9ralement sur l'appareil lui-m\u00eame ou dans l'emballage.", + "title": "Couplage avec un appareil via le protocole 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.", @@ -49,8 +49,8 @@ "data": { "device": "Appareil" }, - "description": "S\u00e9lectionnez l'appareil avec lequel vous voulez appairer", - "title": "Appairer avec l'accessoire HomeKit" + "description": "Le contr\u00f4leur HomeKit communique sur le r\u00e9seau local \u00e0 l'aide d'une connexion crypt\u00e9e s\u00e9curis\u00e9e sans contr\u00f4leur HomeKit s\u00e9par\u00e9 ou iCloud. S\u00e9lectionnez l'appareil avec lequel vous souhaitez vous associer:", + "title": "S\u00e9lection de l'appareil" } } }, diff --git a/homeassistant/components/homekit_controller/translations/sv.json b/homeassistant/components/homekit_controller/translations/sv.json index 0c57e09b09b..e57d61dcdb6 100644 --- a/homeassistant/components/homekit_controller/translations/sv.json +++ b/homeassistant/components/homekit_controller/translations/sv.json @@ -36,5 +36,20 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button1": "Knapp 1", + "button10": "Knapp 10", + "button2": "Knapp 2", + "button3": "Knapp 3", + "button4": "Knapp 4", + "button5": "Knapp 5", + "button6": "Knapp 6", + "button7": "Knapp 7", + "button8": "Knapp 8", + "button9": "Knapp 9", + "doorbell": "D\u00f6rrklocka" + } + }, "title": "HomeKit-tillbeh\u00f6r" } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/et.json b/homeassistant/components/homematicip_cloud/translations/et.json index 92f07d401e6..4f97508c045 100644 --- a/homeassistant/components/homematicip_cloud/translations/et.json +++ b/homeassistant/components/homematicip_cloud/translations/et.json @@ -1,6 +1,7 @@ { "config": { "error": { + "invalid_pin": "Vale PIN-kood. Palun proovige uuesti.", "invalid_sgtin_or_pin": "Vale PIN, palun proovige uuesti" }, "step": { diff --git a/homeassistant/components/homematicip_cloud/translations/fr.json b/homeassistant/components/homematicip_cloud/translations/fr.json index 585334b3118..0c5f54d588a 100644 --- a/homeassistant/components/homematicip_cloud/translations/fr.json +++ b/homeassistant/components/homematicip_cloud/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "invalid_pin": "Code PIN invalide, veuillez r\u00e9essayer.", - "invalid_sgtin_or_pin": "Code PIN invalide, veuillez r\u00e9essayer.", + "invalid_sgtin_or_pin": "Code SGTIN ou 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/openweathermap/translations/et.json b/homeassistant/components/openweathermap/translations/et.json new file mode 100644 index 00000000000..3620f1a363e --- /dev/null +++ b/homeassistant/components/openweathermap/translations/et.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Nende koordinaatidele on OpenWeatherMapi sidumine juba tehtud." + }, + "error": { + "auth": "API v\u00f5ti on vale.", + "connection": "OWM API-ga ei saa \u00fchendust luua" + }, + "step": { + "user": { + "data": { + "api_key": "OpenWeatherMapi API v\u00f5ti", + "language": "Keel", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "mode": "Re\u017eiim", + "name": "Sidumise nimi" + }, + "description": "Seadistage OpenWeatherMapi sidumine. API-v\u00f5tme loomiseks minge aadressile https://openweathermap.org/appid", + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Keel", + "mode": "Re\u017eiim" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/sv.json b/homeassistant/components/openweathermap/translations/sv.json new file mode 100644 index 00000000000..a6fe05a8346 --- /dev/null +++ b/homeassistant/components/openweathermap/translations/sv.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "OpenWeatherMap-integrationen f\u00f6r dessa koordinater \u00e4r redan konfigurerad." + }, + "error": { + "auth": "API-nyckeln \u00e4r inte korrekt.", + "connection": "Kan inte ansluta till OWM API" + }, + "step": { + "user": { + "data": { + "api_key": "OpenWeatherMap API-nyckel", + "language": "Spr\u00e5k", + "latitude": "Latitud", + "longitude": "Longitud", + "mode": "L\u00e4ge", + "name": "Integrationens namn" + }, + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Spr\u00e5k", + "mode": "L\u00e4ge" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/et.json b/homeassistant/components/plugwise/translations/et.json new file mode 100644 index 00000000000..4b7c8bf1d8d --- /dev/null +++ b/homeassistant/components/plugwise/translations/et.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "scan_interval": "P\u00e4ringute intervall (sekundites)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/remote/translations/sv.json b/homeassistant/components/remote/translations/sv.json index ea82df41e75..1b6584c5bf8 100644 --- a/homeassistant/components/remote/translations/sv.json +++ b/homeassistant/components/remote/translations/sv.json @@ -1,4 +1,18 @@ { + "device_automation": { + "action_type": { + "turn_off": "St\u00e4ng av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u00e4r avst\u00e4ngd", + "is_on": "{entity_name} \u00e4r p\u00e5" + }, + "trigger_type": { + "turned_off": "{entity_name} st\u00e4ngdes av", + "turned_on": "{entity_name} slogs p\u00e5" + } + }, "state": { "_": { "off": "Av", diff --git a/homeassistant/components/rpi_power/translations/et.json b/homeassistant/components/rpi_power/translations/et.json new file mode 100644 index 00000000000..350e09ca86f --- /dev/null +++ b/homeassistant/components/rpi_power/translations/et.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ei leia selle komponendi jaoks vajalikku s\u00fcsteemiklassi. Veenduge, et teie kernel on v\u00e4rske ja riistvara on toetatud", + "single_instance_allowed": "Seadistused on juba tehtud. Korraga saab olla ainult \u00fcks konfiguratsioon." + }, + "step": { + "confirm": { + "description": "Kas alustame paigaldusega?" + } + } + }, + "title": "Raspberry Pi toiteallika kontroll" +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/et.json b/homeassistant/components/sharkiq/translations/et.json new file mode 100644 index 00000000000..ff5b447a315 --- /dev/null +++ b/homeassistant/components/sharkiq/translations/et.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "reauth": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/sv.json b/homeassistant/components/sharkiq/translations/sv.json new file mode 100644 index 00000000000..75f4175c9af --- /dev/null +++ b/homeassistant/components/sharkiq/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "reauth": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/sv.json b/homeassistant/components/synology_dsm/translations/sv.json index 6aaee8b44aa..690e622ecd2 100644 --- a/homeassistant/components/synology_dsm/translations/sv.json +++ b/homeassistant/components/synology_dsm/translations/sv.json @@ -29,7 +29,8 @@ "step": { "init": { "data": { - "scan_interval": "Minuter mellan skanningar" + "scan_interval": "Minuter mellan skanningar", + "timeout": "Timeout (sekunder)" } } } diff --git a/homeassistant/components/unifi/translations/ca.json b/homeassistant/components/unifi/translations/ca.json index de2a8ffc562..8fe38c706ba 100644 --- a/homeassistant/components/unifi/translations/ca.json +++ b/homeassistant/components/unifi/translations/ca.json @@ -54,7 +54,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Sensors d'utilitzaci\u00f3 d'ample de banda per a clients de la xarxa" + "allow_bandwidth_sensors": "Sensors d'utilitzaci\u00f3 d'ample de banda per a clients de la xarxa", + "allow_uptime_sensors": "Sensors de temps d'activitat per a clients de xarxa" }, "description": "Configuraci\u00f3 dels sensors d'estad\u00edstiques", "title": "Opcions d'UniFi 3/3" diff --git a/homeassistant/components/unifi/translations/en.json b/homeassistant/components/unifi/translations/en.json index 691f4fb6b01..72dec2e7709 100644 --- a/homeassistant/components/unifi/translations/en.json +++ b/homeassistant/components/unifi/translations/en.json @@ -54,7 +54,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Bandwidth usage sensors for network clients" + "allow_bandwidth_sensors": "Bandwidth usage sensors for network clients", + "allow_uptime_sensors": "Uptime sensors for network clients" }, "description": "Configure statistics sensors", "title": "UniFi options 3/3" diff --git a/homeassistant/components/unifi/translations/nl.json b/homeassistant/components/unifi/translations/nl.json index aeeaba60f0f..37a6148d377 100644 --- a/homeassistant/components/unifi/translations/nl.json +++ b/homeassistant/components/unifi/translations/nl.json @@ -58,8 +58,7 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Maak bandbreedtegebruiksensoren voor netwerkclients", - "allow_uptime_sensors": "Maak uptime-sensoren voor netwerkclients" + "allow_bandwidth_sensors": "Maak bandbreedtegebruiksensoren voor netwerkclients" }, "description": "Configureer statistische sensoren", "title": "UniFi-opties 3/3" diff --git a/homeassistant/components/unifi/translations/ru.json b/homeassistant/components/unifi/translations/ru.json index 550867b682b..8276b41e33b 100644 --- a/homeassistant/components/unifi/translations/ru.json +++ b/homeassistant/components/unifi/translations/ru.json @@ -62,7 +62,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "\u0421\u0435\u043d\u0441\u043e\u0440\u044b \u043f\u043e\u043b\u043e\u0441\u044b \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432" + "allow_bandwidth_sensors": "\u0421\u0435\u043d\u0441\u043e\u0440\u044b \u043f\u043e\u043b\u043e\u0441\u044b \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432", + "allow_uptime_sensors": "\u0421\u0435\u043d\u0441\u043e\u0440\u044b \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0438", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 UniFi. \u0428\u0430\u0433 3" diff --git a/homeassistant/components/unifi/translations/zh-Hant.json b/homeassistant/components/unifi/translations/zh-Hant.json index 7ba51c9b621..bd43b062786 100644 --- a/homeassistant/components/unifi/translations/zh-Hant.json +++ b/homeassistant/components/unifi/translations/zh-Hant.json @@ -54,7 +54,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "\u7db2\u8def\u5ba2\u6236\u7aef\u983b\u5bec\u7528\u91cf\u611f\u61c9\u5668" + "allow_bandwidth_sensors": "\u7db2\u8def\u5ba2\u6236\u7aef\u983b\u5bec\u7528\u91cf\u611f\u61c9\u5668", + "allow_uptime_sensors": "\u7db2\u8def\u5ba2\u6236\u7aef\u4e0a\u7dda\u6642\u9593\u611f\u6e2c\u5668" }, "description": "\u8a2d\u5b9a\u7d71\u8a08\u6578\u64da\u611f\u61c9\u5668", "title": "UniFi \u9078\u9805 3/3" diff --git a/homeassistant/components/vacuum/translations/et.json b/homeassistant/components/vacuum/translations/et.json index 56976340c5b..7d221f9be58 100644 --- a/homeassistant/components/vacuum/translations/et.json +++ b/homeassistant/components/vacuum/translations/et.json @@ -7,8 +7,8 @@ "idle": "Ootel", "off": "V\u00e4ljas", "on": "Sees", - "paused": "Peatatud", - "returning": "P\u00f6\u00f6rdun tagasi dokki" + "paused": "Pausil", + "returning": "P\u00f6\u00f6rdun tagasi laadimisjaama" } }, "title": "T\u00fchjenda" diff --git a/homeassistant/components/wolflink/translations/sensor.no.json b/homeassistant/components/wolflink/translations/sensor.no.json index fcd93f0b01b..75aa91bcf9c 100644 --- a/homeassistant/components/wolflink/translations/sensor.no.json +++ b/homeassistant/components/wolflink/translations/sensor.no.json @@ -50,7 +50,7 @@ "parallelbetrieb": "Parallell modus", "partymodus": "Festmodus", "perm_cooling": "PermKj\u00f8ling", - "permanent": "permament", + "permanent": "Permanent", "permanentbetrieb": "Permanent modus", "reduzierter_betrieb": "Begrenset modus", "rt_abschaltung": "RT-avstengning", diff --git a/homeassistant/components/wolflink/translations/sensor.zh-Hant.json b/homeassistant/components/wolflink/translations/sensor.zh-Hant.json index c2be9263bcf..d9c90824742 100644 --- a/homeassistant/components/wolflink/translations/sensor.zh-Hant.json +++ b/homeassistant/components/wolflink/translations/sensor.zh-Hant.json @@ -50,7 +50,7 @@ "parallelbetrieb": "\u4e26\u884c\u6a21\u5f0f", "partymodus": "\u6d3e\u5c0d\u6a21\u5f0f", "perm_cooling": "PermCooling", - "permanent": "\u6c38\u4e45", + "permanent": "\u56fa\u5b9a", "permanentbetrieb": "\u6c38\u4e45\u6a21\u5f0f", "reduzierter_betrieb": "\u9650\u5236\u6a21\u5f0f", "rt_abschaltung": "RT \u95dc\u6a5f", diff --git a/homeassistant/components/xiaomi_aqara/translations/fr.json b/homeassistant/components/xiaomi_aqara/translations/fr.json index a46dc756390..c5e03cc5c14 100644 --- a/homeassistant/components/xiaomi_aqara/translations/fr.json +++ b/homeassistant/components/xiaomi_aqara/translations/fr.json @@ -7,7 +7,7 @@ }, "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_host": "Adresse IP non valide, voir https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "Interface r\u00e9seau non valide", "invalid_key": "Cl\u00e9 de passerelle non valide", "invalid_mac": "Adresse MAC non valide", diff --git a/homeassistant/components/yeelight/translations/sv.json b/homeassistant/components/yeelight/translations/sv.json new file mode 100644 index 00000000000..9fdd341e941 --- /dev/null +++ b/homeassistant/components/yeelight/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "pick_device": { + "data": { + "device": "Enhet" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/et.json b/homeassistant/components/zoneminder/translations/et.json new file mode 100644 index 00000000000..1dcaa5626b0 --- /dev/null +++ b/homeassistant/components/zoneminder/translations/et.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "auth_fail": "Kasutajanimi v\u00f5i salas\u00f5na on vale.", + "connection_error": "ZoneMinderi serveriga \u00fchenduse loomine nurjus." + }, + "create_entry": { + "default": "ZoneMinderi server on lisatud." + }, + "error": { + "auth_fail": "Vale kasutajanimi v\u00f5i salas\u00f5na", + "connection_error": "ZoneMinderi serveriga \u00fchenduse loomine nurjus." + }, + "flow_title": "ZoneMinder", + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "path": "ZM aadress", + "path_zms": "ZMS-i aadress", + "ssl": "Kasutage ZoneMinderiga \u00fchenduse loomiseks SSL-i", + "username": "Kasutajanimi", + "verify_ssl": "Kontrollige SSL-sertifikaati" + }, + "title": "Lisa ZoneMinderi server." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/sv.json b/homeassistant/components/zoneminder/translations/sv.json new file mode 100644 index 00000000000..37fd73d32f0 --- /dev/null +++ b/homeassistant/components/zoneminder/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "auth_fail": "Anv\u00e4ndarnamn eller l\u00f6senord \u00e4r felaktigt." + }, + "error": { + "auth_fail": "Anv\u00e4ndarnamn eller l\u00f6senord \u00e4r felaktigt." + }, + "step": { + "user": { + "data": { + "verify_ssl": "Verifiera SSL-certifikat" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/zh-Hant.json b/homeassistant/components/zoneminder/translations/zh-Hant.json new file mode 100644 index 00000000000..5d7c96c2ad2 --- /dev/null +++ b/homeassistant/components/zoneminder/translations/zh-Hant.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "auth_fail": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4\u3002", + "connection_error": "ZoneMinder \u4f3a\u670d\u5668\u9023\u7dda\u5931\u6557\u3002" + }, + "create_entry": { + "default": "ZoneMinder \u4f3a\u670d\u5668\u5df2\u65b0\u589e\u3002" + }, + "error": { + "auth_fail": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4\u3002", + "connection_error": "ZoneMinder \u4f3a\u670d\u5668\u9023\u7dda\u5931\u6557\u3002" + }, + "flow_title": "ZoneMinder", + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\uff08\u4f8b\u5982 10.10.0.4:8010\uff09", + "password": "\u5bc6\u78bc", + "path": "ZM \u8def\u5f91", + "path_zms": "ZMS \u8def\u5f91", + "ssl": "\u4f7f\u7528 SSL/TLS \u9023\u7dda\u81f3 ZoneMinder", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + }, + "title": "\u65b0\u589e ZoneMinder \u4f3a\u670d\u5668\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/et.json b/homeassistant/components/zwave/translations/et.json index e33b5e32827..07d5d386548 100644 --- a/homeassistant/components/zwave/translations/et.json +++ b/homeassistant/components/zwave/translations/et.json @@ -7,8 +7,8 @@ "sleeping": "Ootel" }, "query_stage": { - "dead": "Surnud ({query_stage})", - "initializing": "L\u00e4htestan ( {query_stage} )" + "dead": "Surnud", + "initializing": "L\u00e4htestan" } } } \ No newline at end of file From 052e8f09834e7568583b0e334b7e1adbd8e35698 Mon Sep 17 00:00:00 2001 From: twdkeule <11250711+twdkeule@users.noreply.github.com> Date: Sat, 19 Sep 2020 04:42:03 +0200 Subject: [PATCH 239/514] Add Influxdb precision option (#38454) --- homeassistant/components/influxdb/__init__.py | 7 +- homeassistant/components/influxdb/const.py | 2 + tests/components/influxdb/test_init.py | 107 +++++++++++++++++- 3 files changed, 111 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index db49e119235..45d3a4f5a25 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -57,6 +57,7 @@ from .const import ( CONF_PASSWORD, CONF_PATH, CONF_PORT, + CONF_PRECISION, CONF_RETRY_COUNT, CONF_SSL, CONF_TAGS, @@ -307,13 +308,13 @@ def get_influx_connection(conf, test_write=False, test_read=False): kwargs = { CONF_TIMEOUT: TIMEOUT, } + precision = conf.get(CONF_PRECISION) if conf[CONF_API_VERSION] == API_VERSION_2: kwargs[CONF_URL] = conf[CONF_URL] kwargs[CONF_TOKEN] = conf[CONF_TOKEN] kwargs[INFLUX_CONF_ORG] = conf[CONF_ORG] bucket = conf.get(CONF_BUCKET) - influx = InfluxDBClientV2(**kwargs) query_api = influx.query_api() initial_write_mode = SYNCHRONOUS if test_write else ASYNCHRONOUS @@ -322,7 +323,7 @@ def get_influx_connection(conf, test_write=False, test_read=False): def write_v2(json): """Write data to V2 influx.""" try: - write_api.write(bucket=bucket, record=json) + write_api.write(bucket=bucket, record=json, write_precision=precision) except (urllib3.exceptions.HTTPError, OSError) as exc: raise ConnectionError(CONNECTION_ERROR % exc) from exc except ApiException as exc: @@ -393,7 +394,7 @@ def get_influx_connection(conf, test_write=False, test_read=False): def write_v1(json): """Write data to V1 influx.""" try: - influx.write_points(json) + influx.write_points(json, time_precision=precision) except ( requests.exceptions.RequestException, exceptions.InfluxDBServerError, diff --git a/homeassistant/components/influxdb/const.py b/homeassistant/components/influxdb/const.py index c1b5ce3a591..029e4d482e8 100644 --- a/homeassistant/components/influxdb/const.py +++ b/homeassistant/components/influxdb/const.py @@ -29,6 +29,7 @@ CONF_COMPONENT_CONFIG_GLOB = "component_config_glob" CONF_COMPONENT_CONFIG_DOMAIN = "component_config_domain" CONF_RETRY_COUNT = "max_retries" CONF_IGNORE_ATTRIBUTES = "ignore_attributes" +CONF_PRECISION = "precision" CONF_LANGUAGE = "language" CONF_QUERIES = "queries" @@ -136,6 +137,7 @@ COMPONENT_CONFIG_SCHEMA_CONNECTION = { vol.Optional(CONF_PATH): cv.string, vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_SSL): cv.boolean, + vol.Optional(CONF_PRECISION): vol.In(["ms", "s", "us", "ns"]), # Connection config for V1 API only. vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index ca4e56ff54d..edb85e7b98d 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -62,9 +62,11 @@ def mock_client_fixture(request): def get_mock_call_fixture(request): """Get version specific lambda to make write API call mock.""" if request.param == influxdb.API_VERSION_2: - return lambda body: call(bucket=DEFAULT_BUCKET, record=body) + return lambda body, precision=None: call( + bucket=DEFAULT_BUCKET, record=body, write_precision=precision + ) # pylint: disable=unnecessary-lambda - return lambda body: call(body) + return lambda body, precision=None: call(body, time_precision=precision) def _get_write_api_mock_v1(mock_influx_client): @@ -1474,3 +1476,104 @@ async def test_invalid_inputs_error( == 1 ) sleep.assert_not_called() + + +@pytest.mark.parametrize( + "mock_client, config_ext, get_write_api, get_mock_call, precision", + [ + ( + influxdb.DEFAULT_API_VERSION, + BASE_V1_CONFIG, + _get_write_api_mock_v1, + influxdb.DEFAULT_API_VERSION, + "ns", + ), + ( + influxdb.API_VERSION_2, + BASE_V2_CONFIG, + _get_write_api_mock_v2, + influxdb.API_VERSION_2, + "ns", + ), + ( + influxdb.DEFAULT_API_VERSION, + BASE_V1_CONFIG, + _get_write_api_mock_v1, + influxdb.DEFAULT_API_VERSION, + "us", + ), + ( + influxdb.API_VERSION_2, + BASE_V2_CONFIG, + _get_write_api_mock_v2, + influxdb.API_VERSION_2, + "us", + ), + ( + influxdb.DEFAULT_API_VERSION, + BASE_V1_CONFIG, + _get_write_api_mock_v1, + influxdb.DEFAULT_API_VERSION, + "ms", + ), + ( + influxdb.API_VERSION_2, + BASE_V2_CONFIG, + _get_write_api_mock_v2, + influxdb.API_VERSION_2, + "ms", + ), + ( + influxdb.DEFAULT_API_VERSION, + BASE_V1_CONFIG, + _get_write_api_mock_v1, + influxdb.DEFAULT_API_VERSION, + "s", + ), + ( + influxdb.API_VERSION_2, + BASE_V2_CONFIG, + _get_write_api_mock_v2, + influxdb.API_VERSION_2, + "s", + ), + ], + indirect=["mock_client", "get_mock_call"], +) +async def test_precision( + hass, mock_client, config_ext, get_write_api, get_mock_call, precision +): + """Test the precision setup.""" + config = { + "precision": precision, + } + config.update(config_ext) + handler_method = await _setup(hass, mock_client, config, get_write_api) + + value = "1.9" + attrs = { + "unit_of_measurement": "foobars", + } + state = MagicMock( + state=value, + domain="fake", + entity_id="fake.entity-id", + object_id="entity", + attributes=attrs, + ) + event = MagicMock(data={"new_state": state}, time_fired=12345) + body = [ + { + "measurement": "foobars", + "tags": {"domain": "fake", "entity_id": "entity"}, + "time": 12345, + "fields": {"value": float(value)}, + } + ] + handler_method(event) + hass.data[influxdb.DOMAIN].block_till_done() + + write_api = get_write_api(mock_client) + assert write_api.call_count == 1 + assert write_api.call_args == get_mock_call(body, precision) + write_api.reset_mock() From 94dfb66824b05d6092bc6240d5c1721da106a27f Mon Sep 17 00:00:00 2001 From: Greg Badros Date: Fri, 18 Sep 2020 19:48:19 -0700 Subject: [PATCH 240/514] Make tplink SmartStrip communication more robust (#40281) --- homeassistant/components/tplink/common.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py index 7ecced32341..6c9d1f8b2e8 100644 --- a/homeassistant/components/tplink/common.py +++ b/homeassistant/components/tplink/common.py @@ -119,7 +119,14 @@ def get_static_devices(config_data) -> SmartDevices: elif type_ == CONF_SWITCH: switches.append(SmartPlug(host)) elif type_ == CONF_STRIP: - for plug in SmartStrip(host).plugs.values(): + try: + ss_host = SmartStrip(host) + except SmartDeviceException as sde: + _LOGGER.error( + "Failed to setup SmartStrip at %s: %s; not retrying", host, sde + ) + continue + for plug in ss_host.plugs.values(): switches.append(plug) # Dimmers need to be defined as smart plugs to work correctly. elif type_ == CONF_DIMMER: From f563068ce69270f72dc98ab61153a356f73c0dd3 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Fri, 18 Sep 2020 23:22:19 -0500 Subject: [PATCH 241/514] Add config flow to canary (#40055) * Create config_flow.py * Update config_flow.py * work on config flow * Update test_config_flow.py * Update __init__.py * Update camera.py * Update test_config_flow.py * Update test_config_flow.py * Update config_flow.py * Update conftest.py * Update test_config_flow.py * Update test_init.py * Update test_init.py * Apply suggestions from code review * Update camera.py * Update test_init.py * Update camera.py * Update __init__.py * Update test_init.py * Update test_init.py * Update __init__.py * Update __init__.py * Apply suggestions from code review * Update __init__.py * Update test_init.py * Update __init__.py * Update __init__.py * Update config_flow.py * Update test_config_flow.py * Update config_flow.py * Update config_flow.py --- homeassistant/components/canary/__init__.py | 123 ++++++++++++++---- .../components/canary/alarm_control_panel.py | 21 ++- homeassistant/components/canary/camera.py | 41 ++++-- .../components/canary/config_flow.py | 121 +++++++++++++++++ homeassistant/components/canary/const.py | 14 ++ homeassistant/components/canary/manifest.json | 3 +- homeassistant/components/canary/sensor.py | 23 +++- homeassistant/components/canary/strings.json | 31 +++++ homeassistant/generated/config_flows.py | 1 + tests/components/canary/__init__.py | 71 +++++++++- tests/components/canary/conftest.py | 24 ++++ .../canary/test_alarm_control_panel.py | 8 +- tests/components/canary/test_config_flow.py | 121 +++++++++++++++++ tests/components/canary/test_init.py | 87 ++++++++++--- tests/components/canary/test_sensor.py | 6 +- 15 files changed, 612 insertions(+), 83 deletions(-) create mode 100644 homeassistant/components/canary/config_flow.py create mode 100644 homeassistant/components/canary/const.py create mode 100644 homeassistant/components/canary/strings.json create mode 100644 tests/components/canary/test_config_flow.py diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index 0a4ae770006..c0245b5b9d0 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -1,4 +1,5 @@ """Support for Canary devices.""" +import asyncio from datetime import timedelta import logging @@ -6,20 +7,26 @@ from canary.api import Api from requests import ConnectTimeout, HTTPError import voluptuous as vol +from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME -from homeassistant.helpers import discovery +from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle +from .const import ( + CONF_FFMPEG_ARGUMENTS, + DATA_CANARY, + DATA_UNDO_UPDATE_LISTENER, + DEFAULT_FFMPEG_ARGUMENTS, + DEFAULT_TIMEOUT, + DOMAIN, +) + _LOGGER = logging.getLogger(__name__) -NOTIFICATION_ID = "canary_notification" -NOTIFICATION_TITLE = "Canary Setup" - -DOMAIN = "canary" -DATA_CANARY = "canary" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) -DEFAULT_TIMEOUT = 10 CONFIG_SCHEMA = vol.Schema( { @@ -34,33 +41,101 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -CANARY_COMPONENTS = ["alarm_control_panel", "camera", "sensor"] +PLATFORMS = ["alarm_control_panel", "camera", "sensor"] -def setup(hass, config): - """Set up the Canary component.""" - conf = config[DOMAIN] - username = conf[CONF_USERNAME] - password = conf[CONF_PASSWORD] - timeout = conf[CONF_TIMEOUT] +async def async_setup(hass: HomeAssistantType, config: dict) -> bool: + """Set up the Canary integration.""" + hass.data.setdefault(DOMAIN, {}) + + if hass.config_entries.async_entries(DOMAIN): + return True + + ffmpeg_arguments = DEFAULT_FFMPEG_ARGUMENTS + if CAMERA_DOMAIN in config: + camera_config = next( + (item for item in config[CAMERA_DOMAIN] if item["platform"] == DOMAIN), + None, + ) + + if camera_config: + ffmpeg_arguments = camera_config.get( + CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS + ) + + if DOMAIN in config: + if ffmpeg_arguments != DEFAULT_FFMPEG_ARGUMENTS: + config[DOMAIN][CONF_FFMPEG_ARGUMENTS] = ffmpeg_arguments + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config[DOMAIN], + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Set up Canary from a config entry.""" + if not entry.options: + options = { + CONF_FFMPEG_ARGUMENTS: entry.data.get( + CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS + ), + CONF_TIMEOUT: entry.data.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), + } + hass.config_entries.async_update_entry(entry, options=options) try: - hass.data[DATA_CANARY] = CanaryData(username, password, timeout) - except (ConnectTimeout, HTTPError) as ex: - _LOGGER.error("Unable to connect to Canary service: %s", str(ex)) - hass.components.persistent_notification.create( - f"Error: {ex}
You will need to restart hass after fixing.", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, + canary_data = CanaryData( + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), ) - return False + except (ConnectTimeout, HTTPError) as error: + _LOGGER.error("Unable to connect to Canary service: %s", str(error)) + raise ConfigEntryNotReady from error - for component in CANARY_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, config) + undo_listener = entry.add_update_listener(_async_update_listener) + + hass.data[DOMAIN][entry.entry_id] = { + DATA_CANARY: canary_data, + DATA_UNDO_UPDATE_LISTENER: undo_listener, + } + + 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: 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 + ] + ) + ) + + if unload_ok: + hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]() + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + class CanaryData: """Get the latest data and update the states.""" diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index 0677480815b..8d2b01fd5da 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -1,5 +1,6 @@ """Support for Canary alarm.""" import logging +from typing import Callable, List from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT @@ -9,24 +10,32 @@ from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_HOME, SUPPORT_ALARM_ARM_NIGHT, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, ) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType -from . import DATA_CANARY +from . import CanaryData +from .const import DATA_CANARY, DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Canary alarms.""" - data = hass.data[DATA_CANARY] - devices = [CanaryAlarm(data, location.location_id) for location in data.locations] +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up Canary alarm control panels based on a config entry.""" + data: CanaryData = hass.data[DOMAIN][entry.entry_id][DATA_CANARY] + alarms = [CanaryAlarm(data, location.location_id) for location in data.locations] - add_entities(devices, True) + async_add_entities(alarms, True) class CanaryAlarm(AlarmControlPanelEntity): diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 4f8370fb09c..5263f852621 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta import logging +from typing import Callable, List from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG, ImageFrame @@ -9,47 +10,59 @@ import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.config_entries import ConfigEntry from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle -from . import DATA_CANARY, DEFAULT_TIMEOUT +from . import CanaryData +from .const import ( + CONF_FFMPEG_ARGUMENTS, + DATA_CANARY, + DEFAULT_FFMPEG_ARGUMENTS, + DEFAULT_TIMEOUT, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) -CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" -DEFAULT_ARGUMENTS = "-pred 1" - MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string} + {vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_FFMPEG_ARGUMENTS): cv.string} ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Canary sensors.""" - if discovery_info is not None: - return +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up Canary sensors based on a config entry.""" + data: CanaryData = hass.data[DOMAIN][entry.entry_id][DATA_CANARY] - data = hass.data[DATA_CANARY] - devices = [] + ffmpeg_arguments = entry.options.get( + CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS + ) + cameras = [] for location in data.locations: for device in location.devices: if device.is_online: - devices.append( + cameras.append( CanaryCamera( hass, data, location, device, DEFAULT_TIMEOUT, - config[CONF_FFMPEG_ARGUMENTS], + ffmpeg_arguments, ) ) - add_entities(devices, True) + async_add_entities(cameras, True) class CanaryCamera(Camera): diff --git a/homeassistant/components/canary/config_flow.py b/homeassistant/components/canary/config_flow.py new file mode 100644 index 00000000000..dc2822d836a --- /dev/null +++ b/homeassistant/components/canary/config_flow.py @@ -0,0 +1,121 @@ +"""Config flow for Canary.""" +import logging +from typing import Any, Dict, Optional + +from canary.api import Api +from requests import ConnectTimeout, HTTPError +import voluptuous as vol + +from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL, ConfigFlow, OptionsFlow +from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME +from homeassistant.core import callback +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from .const import CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS, DEFAULT_TIMEOUT +from .const import DOMAIN # pylint: disable=unused-import + +_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. + """ + # constructor does login call + Api( + data[CONF_USERNAME], + data[CONF_PASSWORD], + data.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), + ) + + return True + + +class CanaryConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Canary.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_CLOUD_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return CanaryOptionsFlowHandler(config_entry) + + async def async_step_import( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Handle a flow initiated by configuration file.""" + 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") + + errors = {} + default_username = "" + + if user_input is not None: + if CONF_TIMEOUT not in user_input: + user_input[CONF_TIMEOUT] = DEFAULT_TIMEOUT + + default_username = user_input[CONF_USERNAME] + + try: + await self.hass.async_add_executor_job( + validate_input, self.hass, user_input + ) + except (ConnectTimeout, HTTPError): + 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_USERNAME], + data=user_input, + ) + + data_schema = { + vol.Required(CONF_USERNAME, default=default_username): str, + vol.Required(CONF_PASSWORD): str, + } + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema(data_schema), + errors=errors or {}, + ) + + +class CanaryOptionsFlowHandler(OptionsFlow): + """Handle Canary 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 Canary options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = { + vol.Optional( + CONF_FFMPEG_ARGUMENTS, + default=self.config_entry.options.get( + CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS + ), + ): str, + vol.Optional( + CONF_TIMEOUT, + default=self.config_entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), + ): int, + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) diff --git a/homeassistant/components/canary/const.py b/homeassistant/components/canary/const.py new file mode 100644 index 00000000000..4a4da9a3c8d --- /dev/null +++ b/homeassistant/components/canary/const.py @@ -0,0 +1,14 @@ +"""Constants for the Canary integration.""" + +DOMAIN = "canary" + +# Configuration +CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" + +# Data +DATA_CANARY = "canary" +DATA_UNDO_UPDATE_LISTENER = "undo_update_listener" + +# Defaults +DEFAULT_FFMPEG_ARGUMENTS = "-pred 1" +DEFAULT_TIMEOUT = 10 diff --git a/homeassistant/components/canary/manifest.json b/homeassistant/components/canary/manifest.json index e383cb7514b..b4598d64087 100644 --- a/homeassistant/components/canary/manifest.json +++ b/homeassistant/components/canary/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/canary", "requirements": ["py-canary==0.5.0"], "dependencies": ["ffmpeg"], - "codeowners": [] + "codeowners": [], + "config_flow": true } diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 1af2b5ad135..e3e6549e88f 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -1,6 +1,9 @@ """Support for Canary sensors.""" +from typing import Callable, List + from canary.api import SensorType +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, @@ -10,8 +13,10 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType -from . import DATA_CANARY +from . import CanaryData +from .const import DATA_CANARY, DOMAIN SENSOR_VALUE_PRECISION = 2 ATTR_AIR_QUALITY = "air_quality" @@ -38,10 +43,14 @@ STATE_AIR_QUALITY_ABNORMAL = "abnormal" STATE_AIR_QUALITY_VERY_ABNORMAL = "very_abnormal" -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Canary sensors.""" - data = hass.data[DATA_CANARY] - devices = [] +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up Canary sensors based on a config entry.""" + data: CanaryData = hass.data[DOMAIN][entry.entry_id][DATA_CANARY] + sensors = [] for location in data.locations: for device in location.devices: @@ -49,11 +58,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_type = device.device_type for sensor_type in SENSOR_TYPES: if device_type.get("name") in sensor_type[4]: - devices.append( + sensors.append( CanarySensor(data, sensor_type, location, device) ) - add_entities(devices, True) + async_add_entities(sensors, True) class CanarySensor(Entity): diff --git a/homeassistant/components/canary/strings.json b/homeassistant/components/canary/strings.json new file mode 100644 index 00000000000..504a5dc2ac1 --- /dev/null +++ b/homeassistant/components/canary/strings.json @@ -0,0 +1,31 @@ +{ + "config": { + "flow_title": "Canary: {name}", + "step": { + "user": { + "title": "Connect to Canary", + "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%]" + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Arguments passed to ffmpeg for cameras", + "timeout": "Request Timeout (seconds)" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 045c5c26285..fae053ac1a1 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -31,6 +31,7 @@ FLOWS = [ "broadlink", "brother", "bsblan", + "canary", "cast", "cert_expiry", "control4", diff --git a/tests/components/canary/__init__.py b/tests/components/canary/__init__.py index e8effcb4c3f..9d0e488d516 100644 --- a/tests/components/canary/__init__.py +++ b/tests/components/canary/__init__.py @@ -1,8 +1,73 @@ -"""Tests for the canary component.""" +"""Tests for the Canary integration.""" from unittest.mock import MagicMock, PropertyMock from canary.api import SensorType +from homeassistant.components.canary.const import ( + CONF_FFMPEG_ARGUMENTS, + DEFAULT_FFMPEG_ARGUMENTS, + DEFAULT_TIMEOUT, + DOMAIN, +) +from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME +from homeassistant.helpers.typing import HomeAssistantType + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +ENTRY_CONFIG = { + CONF_PASSWORD: "test-password", + CONF_USERNAME: "test-username", +} + +ENTRY_OPTIONS = { + CONF_FFMPEG_ARGUMENTS: DEFAULT_FFMPEG_ARGUMENTS, + CONF_TIMEOUT: DEFAULT_TIMEOUT, +} + +USER_INPUT = { + CONF_PASSWORD: "test-password", + CONF_USERNAME: "test-username", +} + +YAML_CONFIG = { + CONF_PASSWORD: "test-password", + CONF_USERNAME: "test-username", + CONF_TIMEOUT: 5, +} + + +def _patch_async_setup(return_value=True): + return patch( + "homeassistant.components.canary.async_setup", + return_value=return_value, + ) + + +def _patch_async_setup_entry(return_value=True): + return patch( + "homeassistant.components.canary.async_setup_entry", + return_value=return_value, + ) + + +async def init_integration( + hass: HomeAssistantType, + *, + data: dict = ENTRY_CONFIG, + options: dict = ENTRY_OPTIONS, + skip_entry_setup: bool = False, +) -> MockConfigEntry: + """Set up the Canary integration in Home Assistant.""" + entry = MockConfigEntry(domain=DOMAIN, data=data, options=options) + entry.add_to_hass(hass) + + if not skip_entry_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry + def mock_device(device_id, name, is_online=True, device_type_name=None): """Mock Canary Device class.""" @@ -13,6 +78,7 @@ def mock_device(device_id, name, is_online=True, device_type_name=None): type(device).device_type = PropertyMock( return_value={"id": 1, "name": device_type_name} ) + return device @@ -27,6 +93,7 @@ def mock_location( type(location).is_private = PropertyMock(return_value=is_private) type(location).devices = PropertyMock(return_value=devices or []) type(location).mode = PropertyMock(return_value=mode) + return location @@ -36,6 +103,7 @@ def mock_mode(mode_id, name): type(mode).mode_id = PropertyMock(return_value=mode_id) type(mode).name = PropertyMock(return_value=name) type(mode).resource_url = PropertyMock(return_value=f"/v1/modes/{mode_id}") + return mode @@ -44,4 +112,5 @@ def mock_reading(sensor_type, sensor_value): reading = MagicMock() type(reading).sensor_type = SensorType(sensor_type) type(reading).value = PropertyMock(return_value=sensor_value) + return reading diff --git a/tests/components/canary/conftest.py b/tests/components/canary/conftest.py index 41873e0c25f..0127865f6a1 100644 --- a/tests/components/canary/conftest.py +++ b/tests/components/canary/conftest.py @@ -32,3 +32,27 @@ def canary(hass): instance.set_location_mode = MagicMock(return_value=None) yield mock_canary + + +@fixture +def canary_config_flow(hass): + """Mock the CanaryApi for easier config flow testing.""" + with patch.object(Api, "login", return_value=True), patch( + "homeassistant.components.canary.config_flow.Api" + ) as mock_canary: + instance = mock_canary.return_value = Api( + "test-username", + "test-password", + 1, + ) + + instance.login = MagicMock(return_value=True) + instance.get_entries = MagicMock(return_value=[]) + instance.get_locations = MagicMock(return_value=[]) + instance.get_location = MagicMock(return_value=None) + instance.get_modes = MagicMock(return_value=[]) + instance.get_readings = MagicMock(return_value=[]) + instance.get_latest_readings = MagicMock(return_value=[]) + instance.set_location_mode = MagicMock(return_value=None) + + yield mock_canary diff --git a/tests/components/canary/test_alarm_control_panel.py b/tests/components/canary/test_alarm_control_panel.py index 87522d6ad95..f1b8fc3396e 100644 --- a/tests/components/canary/test_alarm_control_panel.py +++ b/tests/components/canary/test_alarm_control_panel.py @@ -42,9 +42,7 @@ async def test_alarm_control_panel(hass, canary) -> None: instance.get_locations.return_value = [mocked_location] config = {DOMAIN: {"username": "test-username", "password": "test-password"}} - with patch( - "homeassistant.components.canary.CANARY_COMPONENTS", ["alarm_control_panel"] - ): + with patch("homeassistant.components.canary.PLATFORMS", ["alarm_control_panel"]): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() @@ -126,9 +124,7 @@ async def test_alarm_control_panel_services(hass, canary) -> None: instance.get_locations.return_value = [mocked_location] config = {DOMAIN: {"username": "test-username", "password": "test-password"}} - with patch( - "homeassistant.components.canary.CANARY_COMPONENTS", ["alarm_control_panel"] - ): + with patch("homeassistant.components.canary.PLATFORMS", ["alarm_control_panel"]): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/canary/test_config_flow.py b/tests/components/canary/test_config_flow.py new file mode 100644 index 00000000000..2bd6ae6443d --- /dev/null +++ b/tests/components/canary/test_config_flow.py @@ -0,0 +1,121 @@ +"""Test the Canary config flow.""" +from requests import ConnectTimeout, HTTPError + +from homeassistant.components.canary.const import ( + CONF_FFMPEG_ARGUMENTS, + DEFAULT_FFMPEG_ARGUMENTS, + DEFAULT_TIMEOUT, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_TIMEOUT +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 USER_INPUT, _patch_async_setup, _patch_async_setup_entry, init_integration + + +async def test_user_form(hass, canary_config_flow): + """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_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"] == "test-username" + assert result["data"] == {**USER_INPUT, CONF_TIMEOUT: DEFAULT_TIMEOUT} + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_form_cannot_connect(hass, canary_config_flow): + """Test we handle errors that should trigger the cannot connect error.""" + canary_config_flow.side_effect = HTTPError() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + 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"} + + canary_config_flow.side_effect = ConnectTimeout() + + 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, canary_config_flow): + """Test we handle unexpected exception.""" + canary_config_flow.side_effect = Exception() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + 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, canary_config_flow): + """Test that configuring more than one instance is rejected.""" + await init_integration(hass, skip_entry_setup=True) + + 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 = await init_integration(hass, skip_entry_setup=True) + assert entry.options[CONF_FFMPEG_ARGUMENTS] == DEFAULT_FFMPEG_ARGUMENTS + assert entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT + + 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_FFMPEG_ARGUMENTS: "-v", CONF_TIMEOUT: 7}, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_FFMPEG_ARGUMENTS] == "-v" + assert result["data"][CONF_TIMEOUT] == 7 diff --git a/tests/components/canary/test_init.py b/tests/components/canary/test_init.py index ab0d8e5ab7a..f548a007505 100644 --- a/tests/components/canary/test_init.py +++ b/tests/components/canary/test_init.py @@ -1,37 +1,82 @@ """The tests for the Canary component.""" -from requests import HTTPError +from requests import ConnectTimeout -from homeassistant.components.canary import DOMAIN +from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.canary.const import CONF_FFMPEG_ARGUMENTS, DOMAIN +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.setup import async_setup_component +from . import YAML_CONFIG, init_integration + from tests.async_mock import patch -async def test_setup_with_valid_config(hass, canary) -> None: - """Test setup with valid YAML.""" - await async_setup_component(hass, "persistent_notification", {}) - config = {DOMAIN: {"username": "test-username", "password": "test-password"}} - +async def test_import_from_yaml(hass, canary) -> None: + """Test import from YAML.""" with patch( - "homeassistant.components.canary.alarm_control_panel.setup_platform", - return_value=True, - ), patch( - "homeassistant.components.canary.camera.setup_platform", - return_value=True, - ), patch( - "homeassistant.components.canary.sensor.setup_platform", + "homeassistant.components.canary.async_setup_entry", return_value=True, ): - assert await async_setup_component(hass, DOMAIN, config) + 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 -async def test_setup_with_http_error(hass, canary) -> None: - """Test setup with HTTP error.""" - await async_setup_component(hass, "persistent_notification", {}) - config = {DOMAIN: {"username": "test-username", "password": "test-password"}} + assert entries[0].data[CONF_USERNAME] == "test-username" + assert entries[0].data[CONF_PASSWORD] == "test-password" + assert entries[0].data[CONF_TIMEOUT] == 5 - canary.side_effect = HTTPError() - assert not await async_setup_component(hass, DOMAIN, config) +async def test_import_from_yaml_ffmpeg(hass, canary) -> None: + """Test import from YAML with ffmpeg arguments.""" + with patch( + "homeassistant.components.canary.async_setup_entry", + return_value=True, + ): + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: YAML_CONFIG, + CAMERA_DOMAIN: [{"platform": DOMAIN, CONF_FFMPEG_ARGUMENTS: "-v"}], + }, + ) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + + assert entries[0].data[CONF_USERNAME] == "test-username" + assert entries[0].data[CONF_PASSWORD] == "test-password" + assert entries[0].data[CONF_TIMEOUT] == 5 + assert entries[0].data.get(CONF_FFMPEG_ARGUMENTS) == "-v" + + +async def test_unload_entry(hass, canary): + """Test successful unload of entry.""" + entry = await init_integration(hass) + + assert entry + 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, canary): + """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" + canary.side_effect = ConnectTimeout() + + entry = await init_integration(hass) + assert entry + assert entry.state == ENTRY_STATE_SETUP_RETRY diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index 8d785a6ced5..b5c8ecb6837 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -42,7 +42,7 @@ async def test_sensors_pro(hass, canary) -> None: ] config = {DOMAIN: {"username": "test-username", "password": "test-password"}} - with patch("homeassistant.components.canary.CANARY_COMPONENTS", ["sensor"]): + with patch("homeassistant.components.canary.PLATFORMS", ["sensor"]): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() @@ -101,7 +101,7 @@ async def test_sensors_attributes_pro(hass, canary) -> None: ] config = {DOMAIN: {"username": "test-username", "password": "test-password"}} - with patch("homeassistant.components.canary.CANARY_COMPONENTS", ["sensor"]): + with patch("homeassistant.components.canary.PLATFORMS", ["sensor"]): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() @@ -155,7 +155,7 @@ async def test_sensors_flex(hass, canary) -> None: ] config = {DOMAIN: {"username": "test-username", "password": "test-password"}} - with patch("homeassistant.components.canary.CANARY_COMPONENTS", ["sensor"]): + with patch("homeassistant.components.canary.PLATFORMS", ["sensor"]): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() From e330468a137b4d61cea0b4338c43cff216987ee4 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 19 Sep 2020 09:26:08 +0200 Subject: [PATCH 242/514] Use pressure constants in code base (#40262) --- homeassistant/components/ambient_station/__init__.py | 5 +++-- homeassistant/components/bloomsky/sensor.py | 6 ++++-- homeassistant/components/bom/sensor.py | 3 ++- homeassistant/components/buienradar/sensor.py | 3 ++- homeassistant/components/darksky/sensor.py | 11 ++++++----- homeassistant/components/envirophat/sensor.py | 10 ++++++++-- homeassistant/components/homematic/sensor.py | 3 ++- homeassistant/components/isy994/const.py | 6 ++++-- homeassistant/components/luftdaten/__init__.py | 5 +++-- homeassistant/components/opentherm_gw/const.py | 8 ++++++-- homeassistant/components/point/sensor.py | 3 ++- homeassistant/components/wunderground/sensor.py | 3 ++- homeassistant/components/xiaomi_aqara/sensor.py | 3 ++- homeassistant/components/zamg/sensor.py | 5 +++-- homeassistant/components/zha/sensor.py | 3 ++- tests/components/zha/test_sensor.py | 5 +++-- tests/testing_config/custom_components/test/sensor.py | 4 ++-- 17 files changed, 56 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index ac8ae18a657..8658c3d04b9 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -19,6 +19,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, PERCENTAGE, POWER_WATT, + PRESSURE_INHG, SPEED_MILES_PER_HOUR, TEMP_FAHRENHEIT, ) @@ -143,8 +144,8 @@ TYPE_WINDSPEEDMPH = "windspeedmph" TYPE_YEARLYRAININ = "yearlyrainin" SENSOR_TYPES = { TYPE_24HOURRAININ: ("24 Hr Rain", "in", TYPE_SENSOR, None), - TYPE_BAROMABSIN: ("Abs Pressure", "inHg", TYPE_SENSOR, "pressure"), - TYPE_BAROMRELIN: ("Rel Pressure", "inHg", TYPE_SENSOR, "pressure"), + TYPE_BAROMABSIN: ("Abs Pressure", PRESSURE_INHG, TYPE_SENSOR, "pressure"), + TYPE_BAROMRELIN: ("Rel Pressure", PRESSURE_INHG, TYPE_SENSOR, "pressure"), TYPE_BATT10: ("Battery 10", None, TYPE_BINARY_SENSOR, "battery"), TYPE_BATT1: ("Battery 1", None, TYPE_BINARY_SENSOR, "battery"), TYPE_BATT2: ("Battery 2", None, TYPE_BINARY_SENSOR, "battery"), diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index 812efe7697e..eaa03ef2f3b 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -8,6 +8,8 @@ from homeassistant.const import ( AREA_SQUARE_METERS, CONF_MONITORED_CONDITIONS, PERCENTAGE, + PRESSURE_INHG, + PRESSURE_MBAR, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -32,7 +34,7 @@ SENSOR_TYPES = [ SENSOR_UNITS_IMPERIAL = { "Temperature": TEMP_FAHRENHEIT, "Humidity": PERCENTAGE, - "Pressure": "inHg", + "Pressure": PRESSURE_INHG, "Luminance": f"cd/{AREA_SQUARE_METERS}", "Voltage": "mV", } @@ -41,7 +43,7 @@ SENSOR_UNITS_IMPERIAL = { SENSOR_UNITS_METRIC = { "Temperature": TEMP_CELSIUS, "Humidity": PERCENTAGE, - "Pressure": "mbar", + "Pressure": PRESSURE_MBAR, "Luminance": f"cd/{AREA_SQUARE_METERS}", "Voltage": "mV", } diff --git a/homeassistant/components/bom/sensor.py b/homeassistant/components/bom/sensor.py index 56406b29e82..848be51cc8f 100644 --- a/homeassistant/components/bom/sensor.py +++ b/homeassistant/components/bom/sensor.py @@ -22,6 +22,7 @@ from homeassistant.const import ( LENGTH_KILOMETERS, LENGTH_METERS, PERCENTAGE, + PRESSURE_MBAR, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) @@ -66,7 +67,7 @@ SENSOR_TYPES = { "gust_kt": ["Wind Gust kt", "kt"], "air_temp": ["Air Temp C", TEMP_CELSIUS], "dewpt": ["Dew Point C", TEMP_CELSIUS], - "press": ["Pressure mb", "mbar"], + "press": ["Pressure mb", PRESSURE_MBAR], "press_qnh": ["Pressure qnh", "qnh"], "press_msl": ["Pressure msl", "msl"], "press_tend": ["Pressure Tend", None], diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index ba0063ebb85..d1d1f0cf632 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -31,6 +31,7 @@ from homeassistant.const import ( IRRADIATION_WATTS_PER_SQUARE_METER, LENGTH_KILOMETERS, PERCENTAGE, + PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, TIME_HOURS, @@ -78,7 +79,7 @@ SENSOR_TYPES = { "windforce": ["Wind force", "Bft", "mdi:weather-windy"], "winddirection": ["Wind direction", None, "mdi:compass-outline"], "windazimuth": ["Wind direction azimuth", DEGREE, "mdi:compass-outline"], - "pressure": ["Pressure", "hPa", "mdi:gauge"], + "pressure": ["Pressure", PRESSURE_HPA, "mdi:gauge"], "visibility": ["Visibility", LENGTH_KILOMETERS, None], "windgust": ["Wind gust", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy"], "precipitation": ["Precipitation", f"mm/{TIME_HOURS}", "mdi:weather-pouring"], diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index e167f16b4e4..48ceffbac50 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ( LENGTH_CENTIMETERS, LENGTH_KILOMETERS, PERCENTAGE, + PRESSURE_MBAR, SPEED_KILOMETERS_PER_HOUR, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR, @@ -219,11 +220,11 @@ SENSOR_TYPES = { ], "pressure": [ "Pressure", - "mbar", - "mbar", - "mbar", - "mbar", - "mbar", + PRESSURE_MBAR, + PRESSURE_MBAR, + PRESSURE_MBAR, + PRESSURE_MBAR, + PRESSURE_MBAR, "mdi:gauge", ["currently", "hourly", "daily"], ], diff --git a/homeassistant/components/envirophat/sensor.py b/homeassistant/components/envirophat/sensor.py index 4813fd47a92..873d9935ee8 100644 --- a/homeassistant/components/envirophat/sensor.py +++ b/homeassistant/components/envirophat/sensor.py @@ -6,7 +6,13 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_DISPLAY_OPTIONS, CONF_NAME, TEMP_CELSIUS, VOLT +from homeassistant.const import ( + CONF_DISPLAY_OPTIONS, + CONF_NAME, + PRESSURE_HPA, + TEMP_CELSIUS, + VOLT, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -30,7 +36,7 @@ SENSOR_TYPES = { "magnetometer_y": ["magnetometer_y", " ", "mdi:magnet"], "magnetometer_z": ["magnetometer_z", " ", "mdi:magnet"], "temperature": ["temperature", TEMP_CELSIUS, "mdi:thermometer"], - "pressure": ["pressure", "hPa", "mdi:gauge"], + "pressure": ["pressure", PRESSURE_HPA, "mdi:gauge"], "voltage_0": ["voltage_0", VOLT, "mdi:flash"], "voltage_1": ["voltage_1", VOLT, "mdi:flash"], "voltage_2": ["voltage_2", VOLT, "mdi:flash"], diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 51a88bd1207..121b3af5d81 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -11,6 +11,7 @@ from homeassistant.const import ( FREQUENCY_HERTZ, PERCENTAGE, POWER_WATT, + PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, VOLT, @@ -59,7 +60,7 @@ HM_UNIT_HA_CAST = { "WIND_DIRECTION": DEGREE, "WIND_DIRECTION_RANGE": DEGREE, "SUNSHINEDURATION": "#", - "AIR_PRESSURE": "hPa", + "AIR_PRESSURE": PRESSURE_HPA, "FREQUENCY": FREQUENCY_HERTZ, "VALUE": "#", "VALVE_STATE": PERCENTAGE, diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 375f4e4aa73..75c7b3b9de1 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -60,7 +60,9 @@ from homeassistant.const import ( MASS_POUNDS, PERCENTAGE, POWER_WATT, + PRESSURE_HPA, PRESSURE_INHG, + PRESSURE_MBAR, SERVICE_LOCK, SERVICE_UNLOCK, SPEED_KILOMETERS_PER_HOUR, @@ -411,8 +413,8 @@ UOM_FRIENDLY_NAME = { "113": "", # raw 3-byte signed value "114": "", # raw 4-byte signed value "116": LENGTH_MILES, - "117": "mbar", - "118": "hPa", + "117": PRESSURE_MBAR, + "118": PRESSURE_HPA, "119": f"{POWER_WATT}{TIME_HOURS}", "120": f"{LENGTH_INCHES}/{TIME_DAYS}", } diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index 9d184969139..91e9c96d429 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_SENSORS, CONF_SHOW_ON_MAP, PERCENTAGE, + PRESSURE_PA, TEMP_CELSIUS, ) from homeassistant.core import callback @@ -44,8 +45,8 @@ TOPIC_UPDATE = f"{DOMAIN}_data_update" SENSORS = { SENSOR_TEMPERATURE: ["Temperature", "mdi:thermometer", TEMP_CELSIUS], 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_PRESSURE: ["Pressure", "mdi:arrow-down-bold", PRESSURE_PA], + SENSOR_PRESSURE_AT_SEALEVEL: ["Pressure at sealevel", "mdi:download", PRESSURE_PA], SENSOR_PM10: [ "PM10", "mdi:thought-bubble", diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index a32a2199760..3ff1577c436 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -5,6 +5,7 @@ from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, PERCENTAGE, + PRESSURE_BAR, TEMP_CELSIUS, TIME_HOURS, TIME_MINUTES, @@ -39,7 +40,6 @@ SERVICE_SET_MAX_MOD = "set_max_modulation" SERVICE_SET_OAT = "set_outside_temperature" SERVICE_SET_SB_TEMP = "set_setback_temperature" -UNIT_BAR = "bar" UNIT_KW = "kW" UNIT_L_MIN = f"L/{TIME_MINUTES}" @@ -152,7 +152,11 @@ SENSOR_INFO = { "Room Setpoint {}", ], 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_CH_WATER_PRESS: [ + None, + PRESSURE_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: [ DEVICE_CLASS_TEMPERATURE, diff --git a/homeassistant/components/point/sensor.py b/homeassistant/components/point/sensor.py index 4ac8f0c1832..9436877e434 100644 --- a/homeassistant/components/point/sensor.py +++ b/homeassistant/components/point/sensor.py @@ -7,6 +7,7 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, + PRESSURE_HPA, TEMP_CELSIUS, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -21,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_PRESSURE: (None, 0, PRESSURE_HPA), DEVICE_CLASS_HUMIDITY: (None, 1, PERCENTAGE), DEVICE_CLASS_SOUND: ("mdi:ear-hearing", 1, "dBa"), } diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py index 2d9c4f5c9c1..6699c2f7e1d 100644 --- a/homeassistant/components/wunderground/sensor.py +++ b/homeassistant/components/wunderground/sensor.py @@ -24,6 +24,7 @@ from homeassistant.const import ( LENGTH_KILOMETERS, LENGTH_MILES, PERCENTAGE, + PRESSURE_INHG, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR, TEMP_CELSIUS, @@ -406,7 +407,7 @@ SENSOR_TYPES = { "Precipitation Today", "precip_today_string", "mdi:umbrella" ), "pressure_in": WUCurrentConditionsSensorConfig( - "Pressure", "pressure_in", "mdi:gauge", "inHg", device_class="pressure" + "Pressure", "pressure_in", "mdi:gauge", PRESSURE_INHG, device_class="pressure" ), "pressure_mb": WUCurrentConditionsSensorConfig( "Pressure", "pressure_mb", "mdi:gauge", "mb", device_class="pressure" diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py index 576e99eab4b..ed1792a5fdb 100644 --- a/homeassistant/components/xiaomi_aqara/sensor.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -11,6 +11,7 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, PERCENTAGE, POWER_WATT, + PRESSURE_HPA, TEMP_CELSIUS, ) @@ -24,7 +25,7 @@ SENSOR_TYPES = { "humidity": [PERCENTAGE, None, DEVICE_CLASS_HUMIDITY], "illumination": ["lm", None, DEVICE_CLASS_ILLUMINANCE], "lux": ["lx", None, DEVICE_CLASS_ILLUMINANCE], - "pressure": ["hPa", None, DEVICE_CLASS_PRESSURE], + "pressure": [PRESSURE_HPA, None, DEVICE_CLASS_PRESSURE], "bed_activity": ["μm", None, None], "load_power": [POWER_WATT, None, DEVICE_CLASS_POWER], } diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 9372be58493..4852e874672 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -21,6 +21,7 @@ from homeassistant.const import ( DEGREE, LENGTH_METERS, PERCENTAGE, + PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, __version__, @@ -42,8 +43,8 @@ DEFAULT_NAME = "zamg" 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), + "pressure": ("Pressure", PRESSURE_HPA, "LDstat hPa", float), + "pressure_sealevel": ("Pressure at Sea Level", PRESSURE_HPA, "LDred hPa", float), "humidity": ("Humidity", PERCENTAGE, "RF %", int), "wind_speed": ( "Wind Speed", diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 6e2878f371b..215299ca34f 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, POWER_WATT, + PRESSURE_HPA, STATE_UNKNOWN, TEMP_CELSIUS, ) @@ -266,7 +267,7 @@ class Pressure(Sensor): SENSOR_ATTR = "measured_value" _device_class = DEVICE_CLASS_PRESSURE _decimals = 0 - _unit = "hPa" + _unit = PRESSURE_HPA @STRICT_MATCH(channel_names=CHANNEL_TEMPERATURE) diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 7cba51b3e5c..1c7d85c4528 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_METRIC, PERCENTAGE, POWER_WATT, + PRESSURE_HPA, STATE_UNAVAILABLE, STATE_UNKNOWN, TEMP_CELSIUS, @@ -48,10 +49,10 @@ async def async_test_temperature(hass, cluster, entity_id): async def async_test_pressure(hass, cluster, entity_id): """Test pressure sensor.""" await send_attributes_report(hass, cluster, {1: 1, 0: 1000, 2: 10000}) - assert_state(hass, entity_id, "1000", "hPa") + assert_state(hass, entity_id, "1000", PRESSURE_HPA) await send_attributes_report(hass, cluster, {0: 1000, 20: -1, 16: 10000}) - assert_state(hass, entity_id, "1000", "hPa") + assert_state(hass, entity_id, "1000", PRESSURE_HPA) async def async_test_illuminance(hass, cluster, entity_id): diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index 8d0cdf6ac93..7cb8bb070b1 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 PERCENTAGE +from homeassistant.const import PERCENTAGE, PRESSURE_HPA from tests.common import MockEntity @@ -18,7 +18,7 @@ UNITS_OF_MEASUREMENT = { sensor.DEVICE_CLASS_SIGNAL_STRENGTH: "dB", # signal strength (dB/dBm) sensor.DEVICE_CLASS_TEMPERATURE: "C", # temperature (C/F) sensor.DEVICE_CLASS_TIMESTAMP: "hh:mm:ss", # timestamp (ISO8601) - sensor.DEVICE_CLASS_PRESSURE: "hPa", # pressure (hPa/mbar) + sensor.DEVICE_CLASS_PRESSURE: 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) From 399661a7f41f4afecda15124510150c4ba8bf307 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 19 Sep 2020 11:48:56 +0200 Subject: [PATCH 243/514] Add missing integration quality scale to image integration (#40289) --- homeassistant/components/image/manifest.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json index 4fc9c2d1d05..246ea387140 100644 --- a/homeassistant/components/image/manifest.json +++ b/homeassistant/components/image/manifest.json @@ -4,9 +4,7 @@ "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/image", "requirements": ["pillow==7.2.0"], - "ssdp": [], - "zeroconf": [], - "homekit": {}, "dependencies": ["http"], - "codeowners": ["@home-assistant/core"] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } From b175adae5373633d9e7ca496a553c5efaa1d84a2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 19 Sep 2020 11:49:53 +0200 Subject: [PATCH 244/514] Add missing integration quality scale to tags integration (#40291) --- homeassistant/components/tag/manifest.json | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/tag/manifest.json b/homeassistant/components/tag/manifest.json index d330fdaf3f8..f2d3a4133b8 100644 --- a/homeassistant/components/tag/manifest.json +++ b/homeassistant/components/tag/manifest.json @@ -1,12 +1,7 @@ { "domain": "tag", - "name": "Tag", - "config_flow": false, + "name": "Tags", "documentation": "https://www.home-assistant.io/integrations/tag", - "requirements": [], - "ssdp": [], - "zeroconf": [], - "homekit": {}, - "dependencies": [], - "codeowners": ["@balloob", "@dmulcahey"] + "codeowners": ["@balloob", "@dmulcahey"], + "quality_scale": "internal" } From 58321843049e376ff9b7f1fe2edbbacab87f9e08 Mon Sep 17 00:00:00 2001 From: Tom Date: Sat, 19 Sep 2020 12:08:16 +0200 Subject: [PATCH 245/514] Add port to plugwise (#40017) --- homeassistant/components/plugwise/__init__.py | 13 +++++++-- .../components/plugwise/config_flow.py | 13 +++++++-- .../components/plugwise/strings.json | 5 ++-- tests/components/plugwise/test_config_flow.py | 29 ++++++++++++++++++- 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py index 0e55c3e715c..f7986f91540 100644 --- a/homeassistant/components/plugwise/__init__.py +++ b/homeassistant/components/plugwise/__init__.py @@ -10,7 +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.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -21,7 +21,13 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import COORDINATOR, DEFAULT_SCAN_INTERVAL, DOMAIN, UNDO_UPDATE_LISTENER +from .const import ( + COORDINATOR, + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + UNDO_UPDATE_LISTENER, +) CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) @@ -39,9 +45,12 @@ async def async_setup(hass: HomeAssistant, config: dict): 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[CONF_HOST], password=entry.data[CONF_PASSWORD], + port=entry.data.get(CONF_PORT, DEFAULT_PORT), + timeout=30, websession=websession, ) diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 689bfb68f22..14405062231 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -5,12 +5,16 @@ 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, CONF_SCAN_INTERVAL +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, 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 DEFAULT_SCAN_INTERVAL, DOMAIN # pylint:disable=unused-import +from .const import ( # pylint:disable=unused-import + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) @@ -27,6 +31,7 @@ def _base_schema(discovery_info): if not discovery_info: base_schema[vol.Required(CONF_HOST)] = str + base_schema[vol.Optional(CONF_PORT, default=DEFAULT_PORT)] = int base_schema[vol.Required(CONF_PASSWORD)] = str @@ -40,9 +45,11 @@ async def validate_input(hass: core.HomeAssistant, data): Data has the keys from _base_schema() with values provided by the user. """ websession = async_get_clientsession(hass, verify_ssl=False) + api = Smile( host=data[CONF_HOST], password=data[CONF_PASSWORD], + port=data[CONF_PORT], timeout=30, websession=websession, ) @@ -83,6 +90,7 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { CONF_HOST: discovery_info[CONF_HOST], + CONF_PORT: discovery_info.get(CONF_PORT, DEFAULT_PORT), "name": _name, } return await self.async_step_user() @@ -95,6 +103,7 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self.discovery_info: user_input[CONF_HOST] = self.discovery_info[CONF_HOST] + user_input[CONF_PORT] = self.discovery_info.get(CONF_PORT, DEFAULT_PORT) for entry in self._async_current_entries(): if entry.data.get(CONF_HOST) == user_input[CONF_HOST]: diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index 7dc8542698b..0abcd780255 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -13,10 +13,11 @@ "step": { "user": { "title": "Connect to the Smile", - "description": "Details", + "description": "Please enter:", "data": { + "password": "Smile ID", "host": "Smile IP address", - "password": "Smile ID" + "port": "Smile port number" } } }, diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index e0f4993df55..9b67f06e469 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -3,7 +3,11 @@ from Plugwise_Smile.Smile import Smile import pytest from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.plugwise.const import DEFAULT_SCAN_INTERVAL, DOMAIN +from homeassistant.components.plugwise.const import ( + DEFAULT_PORT, + 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 @@ -13,8 +17,11 @@ from tests.common import MockConfigEntry TEST_HOST = "1.1.1.1" TEST_HOSTNAME = "smileabcdef" TEST_PASSWORD = "test_password" +TEST_PORT = 81 + TEST_DISCOVERY = { "host": TEST_HOST, + "port": DEFAULT_PORT, "hostname": f"{TEST_HOSTNAME}.local.", "server": f"{TEST_HOSTNAME}.local.", "properties": { @@ -68,6 +75,7 @@ async def test_form(hass): assert result2["data"] == { "host": TEST_HOST, "password": TEST_PASSWORD, + "port": DEFAULT_PORT, } assert len(mock_setup.mock_calls) == 1 @@ -106,6 +114,7 @@ async def test_zeroconf_form(hass): assert result2["data"] == { "host": TEST_HOST, "password": TEST_PASSWORD, + "port": DEFAULT_PORT, } assert len(mock_setup.mock_calls) == 1 @@ -176,6 +185,24 @@ async def test_form_cannot_connect(hass, mock_smile): assert result2["errors"] == {"base": "cannot_connect"} +async def test_form_cannot_connect_port(hass, mock_smile): + """Test we handle cannot connect to port error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_smile.connect.side_effect = Smile.ConnectionFailedError + mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": TEST_HOST, "password": TEST_PASSWORD, "port": TEST_PORT}, + ) + + 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( From 9bd28306f626a996e23e33da90e63492213462c8 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 19 Sep 2020 12:14:51 +0200 Subject: [PATCH 246/514] Correct modbus switch to return correct coil (#40190) --- homeassistant/components/modbus/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 8037d926ef1..c238e105659 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -171,7 +171,7 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity): return self._available = True - return bool(result.bits[0]) + return bool(result.bits[coil]) def _write_coil(self, coil, value): """Write coil using the Modbus hub slave.""" From 467a001e1fde7b07cb2e3953c5a2d195e5ff4a70 Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Sat, 19 Sep 2020 13:08:14 +0200 Subject: [PATCH 247/514] Fix error creating duplicate ConfigEntry upon import for rfxtrx (#40296) --- homeassistant/components/rfxtrx/config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 287e1ec4baf..596f1d0b5e9 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -20,4 +20,5 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if entry and import_config.items() != entry.data.items(): self.hass.config_entries.async_update_entry(entry, data=import_config) return self.async_abort(reason="already_configured") + self._abort_if_unique_id_configured() return self.async_create_entry(title="RFXTRX", data=import_config) From 0c8b530aa22f8d0a5a0ddc06c98ffd612f6216be Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sat, 19 Sep 2020 10:09:40 -0500 Subject: [PATCH 248/514] Add device info to canary camera and sensors (#40053) * add device info to canary sensors * Update test_sensor.py * Update sensor.py * Update sensor.py * Update test_sensor.py * Create const.py * Update sensor.py * Update test_sensor.py * Update sensor.py * Update test_sensor.py * Update camera.py * Update camera.py * Update sensor.py * Update camera.py * Update camera.py --- homeassistant/components/canary/camera.py | 18 ++++++++++++++++-- homeassistant/components/canary/const.py | 2 ++ homeassistant/components/canary/sensor.py | 14 +++++++++++++- tests/components/canary/test_sensor.py | 20 ++++++++++++++++++-- 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 5263f852621..bbf1284c602 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -24,6 +24,7 @@ from .const import ( DEFAULT_FFMPEG_ARGUMENTS, DEFAULT_TIMEOUT, DOMAIN, + MANUFACTURER, ) _LOGGER = logging.getLogger(__name__) @@ -77,18 +78,31 @@ class CanaryCamera(Camera): self._data = data self._location = location self._device = device + self._device_id = device.device_id + self._device_name = device.name + self._device_type_name = device.device_type["name"] self._timeout = timeout self._live_stream_session = None @property def name(self): """Return the name of this device.""" - return self._device.name + return self._device_name @property def unique_id(self): """Return the unique ID of this camera.""" - return str(self._device.device_id) + return str(self._device_id) + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, str(self._device_id))}, + "name": self._device_name, + "model": self._device_type_name, + "manufacturer": MANUFACTURER, + } @property def is_recording(self): diff --git a/homeassistant/components/canary/const.py b/homeassistant/components/canary/const.py index 4a4da9a3c8d..e6e3dbb73c9 100644 --- a/homeassistant/components/canary/const.py +++ b/homeassistant/components/canary/const.py @@ -2,6 +2,8 @@ DOMAIN = "canary" +MANUFACTURER = "Canary Connect, Inc" + # Configuration CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index e3e6549e88f..acf44457cbf 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from . import CanaryData -from .const import DATA_CANARY, DOMAIN +from .const import DATA_CANARY, DOMAIN, MANUFACTURER SENSOR_VALUE_PRECISION = 2 ATTR_AIR_QUALITY = "air_quality" @@ -73,6 +73,8 @@ class CanarySensor(Entity): self._data = data self._sensor_type = sensor_type self._device_id = device.device_id + self._device_name = device.name + self._device_type_name = device.device_type["name"] self._sensor_value = None sensor_type_name = sensor_type[0].replace("_", " ").title() @@ -93,6 +95,16 @@ class CanarySensor(Entity): """Return the unique ID of this sensor.""" return f"{self._device_id}_{self._sensor_type[0]}" + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, str(self._device_id))}, + "name": self._device_name, + "model": self._device_type_name, + "manufacturer": MANUFACTURER, + } + @property def unit_of_measurement(self): """Return the unit of measurement.""" diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index b5c8ecb6837..13b82c9a996 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -1,5 +1,5 @@ """The tests for the Canary sensor platform.""" -from homeassistant.components.canary import DOMAIN +from homeassistant.components.canary.const import DOMAIN, MANUFACTURER from homeassistant.components.canary.sensor import ( ATTR_AIR_QUALITY, STATE_AIR_QUALITY_ABNORMAL, @@ -20,7 +20,7 @@ from homeassistant.setup import async_setup_component from . import mock_device, mock_location, mock_reading from tests.async_mock import patch -from tests.common import mock_registry +from tests.common import mock_device_registry, mock_registry async def test_sensors_pro(hass, canary) -> None: @@ -28,6 +28,8 @@ async def test_sensors_pro(hass, canary) -> None: await async_setup_component(hass, "persistent_notification", {}) registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + online_device_at_home = mock_device(20, "Dining Room", True, "Canary Pro") instance = canary.return_value @@ -82,6 +84,12 @@ async def test_sensors_pro(hass, canary) -> None: assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == data[2] assert state.state == data[1] + device = device_registry.async_get_device({(DOMAIN, "20")}, set()) + assert device + assert device.manufacturer == MANUFACTURER + assert device.name == "Dining Room" + assert device.model == "Canary Pro" + async def test_sensors_attributes_pro(hass, canary) -> None: """Test the creation and values of the sensors attributes for Canary Pro.""" @@ -142,6 +150,8 @@ async def test_sensors_flex(hass, canary) -> None: await async_setup_component(hass, "persistent_notification", {}) registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + online_device_at_home = mock_device(20, "Dining Room", True, "Canary Flex") instance = canary.return_value @@ -187,3 +197,9 @@ async def test_sensors_flex(hass, canary) -> None: assert state assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == data[2] assert state.state == data[1] + + device = device_registry.async_get_device({(DOMAIN, "20")}, set()) + assert device + assert device.manufacturer == MANUFACTURER + assert device.name == "Dining Room" + assert device.model == "Canary Flex" From 711ca6aff5cbf8b67675976d9290f12d89019afb Mon Sep 17 00:00:00 2001 From: Ian Duffy <1243435+imduffy15@users.noreply.github.com> Date: Sat, 19 Sep 2020 16:14:20 +0100 Subject: [PATCH 249/514] Add kodi browse media for channels (#40277) * Add kodi browse media for channels * Rename "TV Channels" to "Channels" --- homeassistant/components/kodi/browse_media.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index c7df170b5c9..c174cf28406 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -5,6 +5,7 @@ from homeassistant.components.media_player import BrowseError, BrowseMedia from homeassistant.components.media_player.const import ( MEDIA_CLASS_ALBUM, MEDIA_CLASS_ARTIST, + MEDIA_CLASS_CHANNEL, MEDIA_CLASS_DIRECTORY, MEDIA_CLASS_EPISODE, MEDIA_CLASS_MOVIE, @@ -15,6 +16,7 @@ from homeassistant.components.media_player.const import ( MEDIA_CLASS_TV_SHOW, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, + MEDIA_TYPE_CHANNEL, MEDIA_TYPE_EPISODE, MEDIA_TYPE_MOVIE, MEDIA_TYPE_PLAYLIST, @@ -45,6 +47,7 @@ CHILD_TYPE_MEDIA_CLASS = { MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, MEDIA_TYPE_TRACK: MEDIA_CLASS_TRACK, MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW, + MEDIA_TYPE_CHANNEL: MEDIA_CLASS_CHANNEL, MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE, } @@ -147,6 +150,15 @@ async def build_item_response(media_library, payload): season["seasondetails"].get("thumbnail") ) title = season["seasondetails"]["label"] + elif search_type == MEDIA_TYPE_CHANNEL: + media = await media_library._server.PVR.GetChannels( + { + "channelgroupid": "alltv", + "properties": ["thumbnail", "channeltype", "channel", "broadcastnow"], + } + ) + media = media.get("channels") + title = "Channels" if media is None: return None @@ -227,9 +239,18 @@ def item_payload(item, media_library): media_content_id = f"{item['tvshowid']}" can_play = False can_expand = True + elif "channelid" in item: + media_content_type = MEDIA_TYPE_CHANNEL + media_content_id = f"{item['channelid']}" + broadcasting = item.get("broadcastnow") + if broadcasting: + show = broadcasting.get("title") + title = f"{title} - {show}" + can_play = True + can_expand = False else: # this case is for the top folder of each type - # possible content types: album, artist, movie, library_music, tvshow + # possible content types: album, artist, movie, library_music, tvshow, channel media_class = MEDIA_CLASS_DIRECTORY media_content_type = item["type"] media_content_id = "" @@ -274,6 +295,7 @@ def library_payload(media_library): "library_music": "Music", MEDIA_TYPE_MOVIE: "Movies", MEDIA_TYPE_TVSHOW: "TV shows", + MEDIA_TYPE_CHANNEL: "Channels", } for item in [{"label": name, "type": type_} for type_, name in library.items()]: library_info.children.append( From 7de1fe7416410f51d6e7280f1552cfd77b301ffd Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 19 Sep 2020 17:30:22 +0200 Subject: [PATCH 250/514] Use percentage constant in more integrations (#40165) --- homeassistant/components/growatt_server/sensor.py | 5 +++-- homeassistant/components/numato/__init__.py | 4 ++-- tests/components/nut/test_sensor.py | 6 +++--- tests/testing_config/custom_components/test/sensor.py | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 384b35a0aff..e6ed422db0f 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -22,6 +22,7 @@ from homeassistant.const import ( ELECTRICAL_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, FREQUENCY_HERTZ, + PERCENTAGE, POWER_WATT, TEMP_CELSIUS, VOLT, @@ -231,7 +232,7 @@ STORAGE_SENSOR_TYPES = { ), "storage_battery_percentage": ( "Battery percentage", - "%", + PERCENTAGE, "capacity", {"device_class": DEVICE_CLASS_BATTERY}, ), @@ -339,7 +340,7 @@ STORAGE_SENSOR_TYPES = { ), "storage_load_percentage": ( "Load percentage", - "%", + PERCENTAGE, "loadPercent", {"device_class": DEVICE_CLASS_BATTERY, "round": 2}, ), diff --git a/homeassistant/components/numato/__init__.py b/homeassistant/components/numato/__init__.py index 8efc56e12fe..0f088cd179d 100644 --- a/homeassistant/components/numato/__init__.py +++ b/homeassistant/components/numato/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_SWITCHES, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + PERCENTAGE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform @@ -31,7 +32,6 @@ CONF_DST_UNIT = "unit" DEFAULT_INVERT_LOGIC = False DEFAULT_SRC_RANGE = [0, 1024] DEFAULT_DST_RANGE = [0.0, 100.0] -DEFAULT_UNIT = "%" DEFAULT_DEV = [f"/dev/ttyACM{i}" for i in range(10)] PORT_RANGE = range(1, 8) # ports 0-7 are ADC capable @@ -82,7 +82,7 @@ ADC_SCHEMA = vol.Schema( vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_SRC_RANGE, default=DEFAULT_SRC_RANGE): int_range, vol.Optional(CONF_DST_RANGE, default=DEFAULT_DST_RANGE): float_range, - vol.Optional(CONF_DST_UNIT, default=DEFAULT_UNIT): cv.string, + vol.Optional(CONF_DST_UNIT, default=PERCENTAGE): cv.string, } ) diff --git a/tests/components/nut/test_sensor.py b/tests/components/nut/test_sensor.py index 42fdc09e2b1..4950d467d6a 100644 --- a/tests/components/nut/test_sensor.py +++ b/tests/components/nut/test_sensor.py @@ -72,7 +72,7 @@ async def test_5e850i(hass): "device_class": "battery", "friendly_name": "Ups1 Battery Charge", "state": "Online", - "unit_of_measurement": "%", + "unit_of_measurement": PERCENTAGE, } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears @@ -97,7 +97,7 @@ async def test_5e650i(hass): "device_class": "battery", "friendly_name": "Ups1 Battery Charge", "state": "Online Battery Charging", - "unit_of_measurement": "%", + "unit_of_measurement": PERCENTAGE, } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears @@ -125,7 +125,7 @@ async def test_backupsses600m1(hass): "device_class": "battery", "friendly_name": "Ups1 Battery Charge", "state": "Online", - "unit_of_measurement": "%", + "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/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index 7cb8bb070b1..d9ed47844af 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -22,7 +22,7 @@ UNITS_OF_MEASUREMENT = { 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_POWER_FACTOR: PERCENTAGE, # power factor (no unit, min: -1.0, max: 1.0) sensor.DEVICE_CLASS_VOLTAGE: "V", # voltage (V) } From b088830382e0cd18d6a6d4cedcfe915ca6a5b967 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 19 Sep 2020 17:34:54 +0200 Subject: [PATCH 251/514] Add and use currency cent constant (#40261) --- homeassistant/components/griddy/sensor.py | 4 ++-- homeassistant/components/isy994/const.py | 3 ++- homeassistant/components/nsw_fuel_station/sensor.py | 4 ++-- homeassistant/const.py | 1 + 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/griddy/sensor.py b/homeassistant/components/griddy/sensor.py index acdcefee527..7a155586fac 100644 --- a/homeassistant/components/griddy/sensor.py +++ b/homeassistant/components/griddy/sensor.py @@ -1,7 +1,7 @@ """Support for August sensors.""" import logging -from homeassistant.const import ENERGY_KILO_WATT_HOUR +from homeassistant.const import CURRENCY_CENT, ENERGY_KILO_WATT_HOUR from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_LOADZONE, DOMAIN @@ -29,7 +29,7 @@ class GriddyPriceSensor(CoordinatorEntity): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return f"¢/{ENERGY_KILO_WATT_HOUR}" + return f"{CURRENCY_CENT}/{ENERGY_KILO_WATT_HOUR}" @property def name(self): diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 75c7b3b9de1..7724420f391 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -46,6 +46,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.const import ( AREA_SQUARE_METERS, CONCENTRATION_PARTS_PER_MILLION, + CURRENCY_CENT, CURRENCY_DOLLAR, DEGREE, ENERGY_KILO_WATT_HOUR, @@ -401,7 +402,7 @@ UOM_FRIENDLY_NAME = { UOM_DOUBLE_TEMP: UOM_DOUBLE_TEMP, "102": "kWs", "103": CURRENCY_DOLLAR, - "104": "¢", + "104": CURRENCY_CENT, "105": LENGTH_INCHES, "106": f"mm/{TIME_DAYS}", "107": "", # raw 1-byte unsigned value diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py index 5e9a9835bf4..b6c0d1a5d9b 100644 --- a/homeassistant/components/nsw_fuel_station/sensor.py +++ b/homeassistant/components/nsw_fuel_station/sensor.py @@ -7,7 +7,7 @@ from nsw_fuel import FuelCheckClient, FuelCheckError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CURRENCY_CENT, VOLUME_LITERS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -179,7 +179,7 @@ class StationPriceSensor(Entity): @property def unit_of_measurement(self) -> str: """Return the units of measurement.""" - return "¢/L" + return f"{CURRENCY_CENT}/{VOLUME_LITERS}" def update(self): """Update current conditions.""" diff --git a/homeassistant/const.py b/homeassistant/const.py index c35437283cc..f3bb26d3ef0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -384,6 +384,7 @@ DEGREE = "°" # Currency units CURRENCY_EURO = "€" CURRENCY_DOLLAR = "$" +CURRENCY_CENT = "¢" # Temperature units TEMP_CELSIUS = f"{DEGREE}C" From e300cf3747d2f3a6181d6c9465224c55adcb49a3 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 19 Sep 2020 17:59:46 +0200 Subject: [PATCH 252/514] Add binary_sensor for cloud connectivity to HomematicIP Cloud (#39675) * Add binary_sensor for cloud connectivity to HomematicIP Cloud * Fix Test * Remove device_class * Switch from _device to _home * Switch from _device to _home for sensor --- .../homematicip_cloud/binary_sensor.py | 40 ++++++++++++++++++- .../components/homematicip_cloud/sensor.py | 4 +- .../homematicip_cloud/test_binary_sensor.py | 23 +++++++++++ .../homematicip_cloud/test_device.py | 2 +- .../homematicip_cloud/test_sensor.py | 4 +- 5 files changed, 67 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 50e8360675b..440dc31788f 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -82,7 +82,7 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP Cloud binary sensor from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] - entities = [] + entities = [HomematicipCloudConnectionSensor(hap)] for device in hap.home.devices: if isinstance(device, AsyncAccelerationSensor): entities.append(HomematicipAccelerationSensor(hap, device)) @@ -136,6 +136,44 @@ async def async_setup_entry( async_add_entities(entities) +class HomematicipCloudConnectionSensor(HomematicipGenericEntity, BinarySensorEntity): + """Representation of the HomematicIP cloud connection sensor.""" + + def __init__(self, hap: HomematicipHAP) -> None: + """Initialize the cloud connection sensor.""" + super().__init__(hap, hap.home, "Cloud Connection") + + @property + def device_info(self) -> Dict[str, Any]: + """Return device specific attributes.""" + # Adds a sensor to the existing HAP device + return { + "identifiers": { + # Serial numbers of Homematic IP device + (HMIPC_DOMAIN, self._home.id) + } + } + + @property + def icon(self) -> str: + """Return the icon of the access point entity.""" + return ( + "mdi:access-point-network" + if self._home.connected + else "mdi:access-point-network-off" + ) + + @property + def is_on(self) -> bool: + """Return true if hap is connected to cloud.""" + return self._home.connected + + @property + def available(self) -> bool: + """Sensor is always available.""" + return True + + class HomematicipBaseActionSensor(HomematicipGenericEntity, BinarySensorEntity): """Representation of the HomematicIP base action sensor.""" diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 32191cde20e..86881f565ce 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -125,7 +125,7 @@ class HomematicipAccesspointStatus(HomematicipGenericEntity): def __init__(self, hap: HomematicipHAP) -> None: """Initialize access point status entity.""" - super().__init__(hap, hap.home) + super().__init__(hap, hap.home, "Duty Cycle") @property def device_info(self) -> Dict[str, Any]: @@ -134,7 +134,7 @@ class HomematicipAccesspointStatus(HomematicipGenericEntity): return { "identifiers": { # Serial numbers of Homematic IP device - (HMIPC_DOMAIN, self._device.id) + (HMIPC_DOMAIN, self._home.id) } } diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py index f15b2b56a95..6d6ba84e243 100644 --- a/tests/components/homematicip_cloud/test_binary_sensor.py +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -38,6 +38,29 @@ async def test_manually_configured_platform(hass): assert not hass.data.get(HMIPC_DOMAIN) +async def test_hmip_access_point_cloud_connection_sensor( + hass, default_mock_hap_factory +): + """Test HomematicipCloudConnectionSensor.""" + entity_id = "binary_sensor.access_point_cloud_connection" + entity_name = "Access Point Cloud Connection" + device_model = None + 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 + + await async_manipulate_test_data(hass, hmip_device, "connected", False) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + async def test_hmip_acceleration_sensor(hass, default_mock_hap_factory): """Test HomematicipAccelerationSensor.""" entity_id = "binary_sensor.garagentor" diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index f7999b5f015..c47f0bf25ea 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) == 191 + assert len(mock_hap.hmip_device_by_entity_id) == 192 async def test_hmip_remove_device(hass, default_mock_hap_factory): diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index fe7283b471e..55d1e32bf2b 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -44,8 +44,8 @@ async def test_manually_configured_platform(hass): async def test_hmip_accesspoint_status(hass, default_mock_hap_factory): """Test HomematicipSwitch.""" - entity_id = "sensor.access_point" - entity_name = "Access Point" + entity_id = "sensor.access_point_duty_cycle" + entity_name = "Access Point Duty Cycle" device_model = None mock_hap = await default_mock_hap_factory.async_get_mock_hap( test_devices=[entity_name] From 6b317ced17efade42b2d5d821cadcf12ae8513a8 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sat, 19 Sep 2020 11:02:15 -0500 Subject: [PATCH 253/514] Update roku media browser classes (#40285) * update roku media browser classes this should allow proper icons to be shown vs plain directory * Update test_media_player.py * Update browse_media.py * Update browse_media.py * Update browse_media.py * Update browse_media.py * Update browse_media.py * Update browse_media.py * Update test_media_player.py --- homeassistant/components/roku/browse_media.py | 17 +++++++++++++---- tests/components/roku/test_media_player.py | 2 ++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/roku/browse_media.py b/homeassistant/components/roku/browse_media.py index f6f8c8976f1..b5be3e99d9a 100644 --- a/homeassistant/components/roku/browse_media.py +++ b/homeassistant/components/roku/browse_media.py @@ -13,8 +13,13 @@ from homeassistant.components.media_player.const import ( CONTENT_TYPE_MEDIA_CLASS = { MEDIA_TYPE_APP: MEDIA_CLASS_APP, - MEDIA_TYPE_APPS: MEDIA_CLASS_DIRECTORY, + MEDIA_TYPE_APPS: MEDIA_CLASS_APP, MEDIA_TYPE_CHANNEL: MEDIA_CLASS_CHANNEL, + MEDIA_TYPE_CHANNELS: MEDIA_CLASS_CHANNEL, +} + +CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS = { + MEDIA_TYPE_APPS: MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_CHANNELS: MEDIA_CLASS_DIRECTORY, } @@ -37,6 +42,7 @@ def build_item_response(coordinator, payload): thumbnail = None title = None media = None + children_media_class = None if search_type == MEDIA_TYPE_APPS: title = "Apps" @@ -44,6 +50,7 @@ def build_item_response(coordinator, payload): {"app_id": item.app_id, "title": item.name, "type": MEDIA_TYPE_APP} for item in coordinator.data.apps ] + children_media_class = MEDIA_CLASS_APP elif search_type == MEDIA_TYPE_CHANNELS: title = "Channels" media = [ @@ -54,18 +61,22 @@ def build_item_response(coordinator, payload): } for item in coordinator.data.channels ] + children_media_class = MEDIA_CLASS_CHANNEL if media is None: return None return BrowseMedia( - media_class=MEDIA_CLASS_DIRECTORY, + 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, can_play=search_type in PLAYABLE_MEDIA_TYPES and search_id, can_expand=True, children=[item_payload(item, coordinator) for item in media], + children_media_class=children_media_class, thumbnail=thumbnail, ) @@ -148,7 +159,5 @@ def library_payload(coordinator): 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/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index e9d5091d664..b4ce1811c91 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -529,6 +529,7 @@ async def test_media_browse(hass, aioclient_mock, hass_ws_client): 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"]["children_media_class"] == MEDIA_CLASS_APP assert msg["result"]["can_expand"] assert not msg["result"]["can_play"] assert len(msg["result"]["children"]) == 11 @@ -573,6 +574,7 @@ async def test_media_browse(hass, aioclient_mock, hass_ws_client): 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"]["children_media_class"] == MEDIA_CLASS_CHANNEL assert msg["result"]["can_expand"] assert not msg["result"]["can_play"] assert len(msg["result"]["children"]) == 2 From a9168c04fd0bd5a463299429d2876b41c3e97743 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 19 Sep 2020 12:39:02 -0400 Subject: [PATCH 254/514] Update ZHA dependencies (#40283) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index c6b0fa78799..b03d4afd971 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -8,8 +8,8 @@ "pyserial==3.4", "zha-quirks==0.0.44", "zigpy-cc==0.5.2", - "zigpy-deconz==0.9.2", - "zigpy==0.23.2", + "zigpy-deconz==0.10.0", + "zigpy==0.24.1", "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 b328dc716da..5ec5ccd8784 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2305,7 +2305,7 @@ ziggo-mediabox-xl==1.1.0 zigpy-cc==0.5.2 # homeassistant.components.zha -zigpy-deconz==0.9.2 +zigpy-deconz==0.10.0 # homeassistant.components.zha zigpy-xbee==0.13.0 @@ -2317,7 +2317,7 @@ zigpy-zigate==0.6.2 zigpy-znp==0.1.1 # homeassistant.components.zha -zigpy==0.23.2 +zigpy==0.24.1 # homeassistant.components.zoneminder zm-py==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e580617ac15..0c04b2f707c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1068,7 +1068,7 @@ zha-quirks==0.0.44 zigpy-cc==0.5.2 # homeassistant.components.zha -zigpy-deconz==0.9.2 +zigpy-deconz==0.10.0 # homeassistant.components.zha zigpy-xbee==0.13.0 @@ -1080,7 +1080,7 @@ zigpy-zigate==0.6.2 zigpy-znp==0.1.1 # homeassistant.components.zha -zigpy==0.23.2 +zigpy==0.24.1 # homeassistant.components.zoneminder zm-py==0.4.0 From a50f1210111cdc6eb847bdb4c633be205b43850f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 19 Sep 2020 22:10:01 +0200 Subject: [PATCH 255/514] Get option flow defaults from yaml for non configured MQTT options (#40177) --- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/mqtt/config_flow.py | 41 +++++++++++++------- homeassistant/components/mqtt/const.py | 2 + 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index d9bf1bbadfa..bafbead96d6 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -60,6 +60,7 @@ from .const import ( CONF_RETAIN, CONF_STATE_TOPIC, CONF_WILL_MESSAGE, + DATA_MQTT_CONFIG, DEFAULT_BIRTH, DEFAULT_DISCOVERY, DEFAULT_PAYLOAD_AVAILABLE, @@ -88,7 +89,6 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "mqtt" DATA_MQTT = "mqtt" -DATA_MQTT_CONFIG = "mqtt_config" SERVICE_PUBLISH = "publish" SERVICE_DUMP = "dump" diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 8b1c350323c..5c4016437a6 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -24,6 +24,7 @@ from .const import ( CONF_BROKER, CONF_DISCOVERY, CONF_WILL_MESSAGE, + DATA_MQTT_CONFIG, DEFAULT_BIRTH, DEFAULT_DISCOVERY, DEFAULT_WILL, @@ -162,6 +163,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): """Manage the MQTT options.""" errors = {} current_config = self.config_entry.data + yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {}) if user_input is not None: can_connect = await self.hass.async_add_executor_job( try_connection, @@ -178,20 +180,22 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): errors["base"] = "cannot_connect" fields = OrderedDict() - fields[vol.Required(CONF_BROKER, default=current_config[CONF_BROKER])] = str - fields[vol.Required(CONF_PORT, default=current_config[CONF_PORT])] = vol.Coerce( - int - ) + current_broker = current_config.get(CONF_BROKER, yaml_config.get(CONF_BROKER)) + current_port = current_config.get(CONF_PORT, yaml_config.get(CONF_PORT)) + current_user = current_config.get(CONF_USERNAME, yaml_config.get(CONF_USERNAME)) + current_pass = current_config.get(CONF_PASSWORD, yaml_config.get(CONF_PASSWORD)) + fields[vol.Required(CONF_BROKER, default=current_broker)] = str + fields[vol.Required(CONF_PORT, default=current_port)] = vol.Coerce(int) fields[ vol.Optional( CONF_USERNAME, - description={"suggested_value": current_config.get(CONF_USERNAME)}, + description={"suggested_value": current_user}, ) ] = str fields[ vol.Optional( CONF_PASSWORD, - description={"suggested_value": current_config.get(CONF_PASSWORD)}, + description={"suggested_value": current_pass}, ) ] = str @@ -205,6 +209,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): """Manage the MQTT options.""" errors = {} current_config = self.config_entry.data + yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {}) options_config = {} if user_input is not None: bad_birth = False @@ -253,16 +258,24 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): ) return self.async_create_entry(title="", data=None) - birth = {**DEFAULT_BIRTH, **current_config.get(CONF_BIRTH_MESSAGE, {})} - will = {**DEFAULT_WILL, **current_config.get(CONF_WILL_MESSAGE, {})} + birth = { + **DEFAULT_BIRTH, + **current_config.get( + CONF_BIRTH_MESSAGE, yaml_config.get(CONF_BIRTH_MESSAGE, {}) + ), + } + will = { + **DEFAULT_WILL, + **current_config.get( + CONF_WILL_MESSAGE, yaml_config.get(CONF_WILL_MESSAGE, {}) + ), + } + discovery = current_config.get( + CONF_DISCOVERY, yaml_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY) + ) fields = OrderedDict() - fields[ - vol.Optional( - CONF_DISCOVERY, - default=current_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY), - ) - ] = bool + fields[vol.Optional(CONF_DISCOVERY, default=discovery)] = bool # Birth message is disabled if CONF_BIRTH_MESSAGE = {} fields[ diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 7ea6d9d348b..5ab3f756311 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -17,6 +17,8 @@ CONF_RETAIN = ATTR_RETAIN CONF_STATE_TOPIC = "state_topic" CONF_WILL_MESSAGE = "will_message" +DATA_MQTT_CONFIG = "mqtt_config" + DEFAULT_PREFIX = "homeassistant" DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status" DEFAULT_DISCOVERY = False From 5dcaeebdac29c076a0ebf0ba779a3bcf9221b516 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 20 Sep 2020 00:05:00 +0000 Subject: [PATCH 256/514] [ci skip] Translation update --- .../binary_sensor/translations/et.json | 82 +++++++++++++++++++ .../components/canary/translations/ca.json | 31 +++++++ .../components/canary/translations/en.json | 31 +++++++ .../components/canary/translations/fr.json | 20 +++++ .../components/canary/translations/ru.json | 31 +++++++ .../canary/translations/zh-Hant.json | 31 +++++++ .../components/cover/translations/et.json | 18 ++++ .../components/deconz/translations/et.json | 7 ++ .../device_tracker/translations/et.json | 6 ++ .../components/fan/translations/et.json | 14 ++++ .../components/hue/translations/et.json | 13 ++- .../components/lock/translations/et.json | 10 +++ .../media_player/translations/et.json | 9 ++ .../components/met/translations/et.json | 19 +++++ .../components/neato/translations/et.json | 16 ++++ .../components/plugwise/translations/ca.json | 5 +- .../components/plugwise/translations/en.json | 5 +- .../components/plugwise/translations/fr.json | 3 +- .../components/plugwise/translations/ru.json | 3 +- .../components/sensor/translations/et.json | 21 +++++ .../components/smhi/translations/et.json | 13 +++ .../transmission/translations/et.json | 13 +++ .../components/vacuum/translations/et.json | 14 ++++ .../components/zha/translations/et.json | 7 ++ .../zoneminder/translations/no.json | 2 +- 25 files changed, 416 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/canary/translations/ca.json create mode 100644 homeassistant/components/canary/translations/en.json create mode 100644 homeassistant/components/canary/translations/fr.json create mode 100644 homeassistant/components/canary/translations/ru.json create mode 100644 homeassistant/components/canary/translations/zh-Hant.json create mode 100644 homeassistant/components/deconz/translations/et.json create mode 100644 homeassistant/components/met/translations/et.json create mode 100644 homeassistant/components/neato/translations/et.json create mode 100644 homeassistant/components/smhi/translations/et.json create mode 100644 homeassistant/components/transmission/translations/et.json create mode 100644 homeassistant/components/zha/translations/et.json diff --git a/homeassistant/components/binary_sensor/translations/et.json b/homeassistant/components/binary_sensor/translations/et.json index c496976f114..d6ea4dbbe7d 100644 --- a/homeassistant/components/binary_sensor/translations/et.json +++ b/homeassistant/components/binary_sensor/translations/et.json @@ -1,4 +1,86 @@ { + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} aku on t\u00fchjenemas", + "is_cold": "{entity_name} on k\u00fclm", + "is_connected": "{entity_name} on \u00fchendatud", + "is_gas": "{entity_name} tuvastab gaasi(leket)", + "is_hot": "{entity_name} on kuum", + "is_light": "{entity_name} tuvastab valgust", + "is_locked": "{entity_name} on lukustatud", + "is_moist": "{entity_name} on niiske", + "is_motion": "{entity_name} tuvastab liikumist", + "is_moving": "{entity_name} liigub", + "is_no_gas": "{entity_name} ei tuvasta gaasi(leket)", + "is_no_light": "{entity_name} ei tuvasta valgust", + "is_no_motion": "{entity_name} ei tuvasta liikumist", + "is_no_problem": "{entity_name} ei leia probleemi", + "is_no_smoke": "{entity_name} ei tuvasta suitsu", + "is_no_sound": "{entity_name} ei tuvasta heli", + "is_no_vibration": "{entity_name} ei tuvasta vibratsiooni", + "is_not_bat_low": "{entity_name} aku on laetud", + "is_not_cold": "{entity_name} ei ole k\u00fclm", + "is_not_connected": "{entity_name} pole \u00fchendatud", + "is_not_hot": "{entity_name} ei ole kuum", + "is_not_locked": "{entity_name} on lukustamata", + "is_not_moist": "{entity_name} on kuiv", + "is_not_moving": "{entity_name} liikumist ei tuvastatud", + "is_not_occupied": "{entity_name} pole h\u00f5ivatud", + "is_not_open": "{entity_name} on suletud", + "is_not_plugged_in": "{entity_name} on lahti \u00fchendatud", + "is_not_powered": "{entity_name} ei ole voolu all", + "is_not_present": "{entity_name} puudub", + "is_not_unsafe": "{entity_name} on turvaline", + "is_occupied": "{entity_name} on h\u00f5ivatud", + "is_off": "{entity_name} on v\u00e4lja l\u00fclitatud", + "is_on": "{entity_name} on sisse l\u00fclitatud", + "is_open": "{entity_name} on avatud", + "is_plugged_in": "{entity_name} on \u00fchendatud", + "is_powered": "{entity_name} on voolu all", + "is_present": "{entity_name} on saadaval", + "is_problem": "Olemil {entity_name} on probleem", + "is_smoke": "{entity_name} tuvastab suitsu", + "is_sound": "{entity_name} tuvastab heli", + "is_unsafe": "{entity_name} on ebaturvaline", + "is_vibration": "{entity_name} tuvastab vibratsiooni" + }, + "trigger_type": { + "bat_low": "{entity_name} aku hakkab t\u00fchjaks saama", + "cold": "{entity_name} muutus k\u00fclmaks", + "connected": "{entity_name} on \u00fchendatud", + "gas": "{entity_name} tuvastas gaasi(leket)", + "hot": "{entity_name} muutus kuumaks", + "light": "{entity_name} tuvastas valgust", + "locked": "{entity_name} on lukus", + "moist": "{entity_name} muutus niiskeks", + "motion": "{entity_name} tuvastas liikumist", + "moving": "{entity_name} hakkas liikuma", + "no_gas": "{entity_name} l\u00f5petas gaasi(lekke) tuvastamise", + "no_light": "{entity_name} l\u00f5petas valguse tuvastamise", + "no_motion": "{entity_name} l\u00f5petas liikumise tuvastamise", + "no_problem": "{entity_name} l\u00f5petas probleemi tuvastamise", + "no_smoke": "{entity_name} l\u00f5petas suitsu tuvastamise", + "no_sound": "{entity_name} l\u00f5petas heli tuvastamise", + "no_vibration": "{entity_name} l\u00f5petas vibratsiooni tuvastamise", + "not_bat_low": "{entity_name} aku on laetud", + "not_cold": "{entity_name} ei ole enam k\u00fclm", + "not_connected": "{entity_name} on lahti \u00fchendatud", + "not_hot": "{entity_name} ei ole enam kuum", + "not_locked": "{entity_name} on lukustamata", + "not_moist": "{entity_name} muutus kuivaks", + "not_moving": "{entity_name} liikumine peatus", + "not_occupied": "{entity_name} vabanes h\u00f5ivest", + "not_opened": "{entity_name} sulgus", + "not_plugged_in": "{entity_name} \u00fchendati vooluv\u00f5rgust v\u00e4lja", + "not_powered": "{entity_name} pole toidet", + "not_present": "{entity_name} puudub", + "not_unsafe": "{entity_name} muutus turvaliseks", + "occupied": "{entity_name} h\u00f5ivati", + "opened": "{entity_name} avanes", + "plugged_in": "{entity_name} \u00fchendati", + "powered": "{entity_name} l\u00fcltus voolu alla" + } + }, "state": { "_": { "off": "V\u00e4ljas", diff --git a/homeassistant/components/canary/translations/ca.json b/homeassistant/components/canary/translations/ca.json new file mode 100644 index 00000000000..c4b80d7537a --- /dev/null +++ b/homeassistant/components/canary/translations/ca.json @@ -0,0 +1,31 @@ +{ + "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" + }, + "flow_title": "Canary: {name}", + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "title": "Connexi\u00f3 amb Canary" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Par\u00e0metres enviats a ffmpeg per c\u00e0meres", + "timeout": "Temps d'espera de sol\u00b7licitud (segons)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/en.json b/homeassistant/components/canary/translations/en.json new file mode 100644 index 00000000000..1e04d1825f3 --- /dev/null +++ b/homeassistant/components/canary/translations/en.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible.", + "unknown": "Unexpected error" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "flow_title": "Canary: {name}", + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + }, + "title": "Connect to Canary" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Arguments passed to ffmpeg for cameras", + "timeout": "Request Timeout (seconds)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/fr.json b/homeassistant/components/canary/translations/fr.json new file mode 100644 index 00000000000..3d3fc087fd9 --- /dev/null +++ b/homeassistant/components/canary/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "flow_title": "Canary : {name}", + "step": { + "user": { + "title": "Se connecter \u00e0 Canary" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Arguments transmis \u00e0 ffmpeg pour les cam\u00e9ras", + "timeout": "D\u00e9lai d'expiration de la demande (secondes)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/ru.json b/homeassistant/components/canary/translations/ru.json new file mode 100644 index 00000000000..146863cf768 --- /dev/null +++ b/homeassistant/components/canary/translations/ru.json @@ -0,0 +1,31 @@ +{ + "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." + }, + "flow_title": "Canary: {name}", + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Canary" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "\u0410\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u044b, \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0435 \u0432 ffmpeg \u0434\u043b\u044f \u043a\u0430\u043c\u0435\u0440", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/zh-Hant.json b/homeassistant/components/canary/translations/zh-Hant.json new file mode 100644 index 00000000000..07463bc8a15 --- /dev/null +++ b/homeassistant/components/canary/translations/zh-Hant.json @@ -0,0 +1,31 @@ +{ + "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" + }, + "flow_title": "Canary\uff1a{name}", + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "\u9023\u7dda\u81f3 Canary" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "\u50b3\u905e\u81f3 ffmpeg \u4e4b\u651d\u5f71\u6a5f\u53c3\u6578", + "timeout": "\u8acb\u6c42\u903e\u6642\uff08\u79d2\uff09" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/translations/et.json b/homeassistant/components/cover/translations/et.json index 96d81b3a7b6..2c9ff745720 100644 --- a/homeassistant/components/cover/translations/et.json +++ b/homeassistant/components/cover/translations/et.json @@ -1,4 +1,22 @@ { + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} on suletud", + "is_closing": "{entity_name} sulgub", + "is_open": "{entity_name} on avatud", + "is_opening": "{entity_name} avaneb", + "is_position": "Praegune {entity_name} asend on", + "is_tilt_position": "Praegune {entity_name} kalle on" + }, + "trigger_type": { + "closed": "{entity_name} sulgus", + "closing": "{entity_name} sulgub", + "opened": "{entity_name} avanes", + "opening": "{entity_name} avaneb", + "position": "{entity_name} asend muutub", + "tilt_position": "{entity_name} kalle muutub" + } + }, "state": { "_": { "closed": "Suletud", diff --git a/homeassistant/components/deconz/translations/et.json b/homeassistant/components/deconz/translations/et.json new file mode 100644 index 00000000000..db8be2a955c --- /dev/null +++ b/homeassistant/components/deconz/translations/et.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "trigger_type": { + "remote_button_rotation_stopped": "Nupu \" {subtype} \" p\u00f6\u00f6ramine peatus" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/translations/et.json b/homeassistant/components/device_tracker/translations/et.json index 340c03665ff..c4f2b6f277d 100644 --- a/homeassistant/components/device_tracker/translations/et.json +++ b/homeassistant/components/device_tracker/translations/et.json @@ -1,4 +1,10 @@ { + "device_automation": { + "condition_type": { + "is_home": "{entity_name} on kodus", + "is_not_home": "{entity_name} on eemal" + } + }, "state": { "_": { "home": "Kodus", diff --git a/homeassistant/components/fan/translations/et.json b/homeassistant/components/fan/translations/et.json index 6652568a0a7..2b141351e1d 100644 --- a/homeassistant/components/fan/translations/et.json +++ b/homeassistant/components/fan/translations/et.json @@ -1,4 +1,18 @@ { + "device_automation": { + "action_type": { + "turn_off": "L\u00fclita {entity_name} v\u00e4lja", + "turn_on": "L\u00fclita {entity_name} sisse" + }, + "condition_type": { + "is_off": "{entity_name} on v\u00e4lja l\u00fclitatud", + "is_on": "{entity_name} on sisse l\u00fclitatud" + }, + "trigger_type": { + "turned_off": "{entity_name} l\u00fclitus v\u00e4lja", + "turned_on": "{entity_name} l\u00fclitus sisse" + } + }, "state": { "_": { "off": "V\u00e4ljas", diff --git a/homeassistant/components/hue/translations/et.json b/homeassistant/components/hue/translations/et.json index 92553c84cfe..842040251dd 100644 --- a/homeassistant/components/hue/translations/et.json +++ b/homeassistant/components/hue/translations/et.json @@ -1,13 +1,24 @@ { "config": { "abort": { + "all_configured": "K\u00f5ik Philips Hue sillad on juba konfigureeritud", + "discover_timeout": "Ei leia Philips Hue sildu", + "no_bridges": "Philips Hue sildu ei avastatud", "unknown": "Ilmnes tundmatu viga" }, + "error": { + "linking": "Ilmnes tundmatu linkimist\u00f5rge.", + "register_failed": "Registreerimine nurjus. Proovige uuesti" + }, "step": { "init": { "data": { "host": "" - } + }, + "title": "Valige Hue sild" + }, + "link": { + "description": "Vajutage silla nuppu, et registreerida Philips Hue Home Assistant abil. \n\n ! [Nupu asukoht sillal] (/ static / images / config_philips_hue.jpg)" } } } diff --git a/homeassistant/components/lock/translations/et.json b/homeassistant/components/lock/translations/et.json index 448d1d4531a..67aeb442937 100644 --- a/homeassistant/components/lock/translations/et.json +++ b/homeassistant/components/lock/translations/et.json @@ -1,4 +1,14 @@ { + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} on lukus", + "is_unlocked": "{entity_name} on lukustamata" + }, + "trigger_type": { + "locked": "{entity_name} on lukus", + "unlocked": "{entity_name} on lukustamata" + } + }, "state": { "_": { "locked": "Lukus", diff --git a/homeassistant/components/media_player/translations/et.json b/homeassistant/components/media_player/translations/et.json index 2800870e9cc..4d71a30a8ac 100644 --- a/homeassistant/components/media_player/translations/et.json +++ b/homeassistant/components/media_player/translations/et.json @@ -1,4 +1,13 @@ { + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} on j\u00f5udeolekus", + "is_off": "{entity_name} on v\u00e4lja l\u00fclitatud", + "is_on": "{entity_name} on sisse l\u00fclitatud", + "is_paused": "{entity_name} on peatatud", + "is_playing": "{entity_name} m\u00e4ngib" + } + }, "state": { "_": { "idle": "Ootel", diff --git a/homeassistant/components/met/translations/et.json b/homeassistant/components/met/translations/et.json new file mode 100644 index 00000000000..3d66c2a532e --- /dev/null +++ b/homeassistant/components/met/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "See asukoht on juba m\u00e4\u00e4ratud" + }, + "step": { + "user": { + "data": { + "elevation": "K\u00f5rgus merepinnast", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "name": "Nimi" + }, + "description": "Norra ilmateenistus", + "title": "Asukoht" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/et.json b/homeassistant/components/neato/translations/et.json new file mode 100644 index 00000000000..6a45426e3d0 --- /dev/null +++ b/homeassistant/components/neato/translations/et.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "unexpected_error": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi", + "vendor": "Tootja" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/ca.json b/homeassistant/components/plugwise/translations/ca.json index c61c28e6668..0477c6e1dad 100644 --- a/homeassistant/components/plugwise/translations/ca.json +++ b/homeassistant/components/plugwise/translations/ca.json @@ -13,9 +13,10 @@ "user": { "data": { "host": "Adre\u00e7a IP de Smile", - "password": "ID de Smile" + "password": "ID de Smile", + "port": "N\u00famero de port de Smile" }, - "description": "Detalls", + "description": "Introdueix:", "title": "Connecta't amb el Smile" } } diff --git a/homeassistant/components/plugwise/translations/en.json b/homeassistant/components/plugwise/translations/en.json index 238f435f3ab..8da8694f872 100644 --- a/homeassistant/components/plugwise/translations/en.json +++ b/homeassistant/components/plugwise/translations/en.json @@ -13,9 +13,10 @@ "user": { "data": { "host": "Smile IP address", - "password": "Smile ID" + "password": "Smile ID", + "port": "Smile port number" }, - "description": "Details", + "description": "Please enter:", "title": "Connect to the Smile" } } diff --git a/homeassistant/components/plugwise/translations/fr.json b/homeassistant/components/plugwise/translations/fr.json index 7ef9296193b..dddc95cfe0a 100644 --- a/homeassistant/components/plugwise/translations/fr.json +++ b/homeassistant/components/plugwise/translations/fr.json @@ -13,7 +13,8 @@ "user": { "data": { "host": "Adresse IP de Smile", - "password": "ID Smile" + "password": "ID Smile", + "port": "Num\u00e9ro de port Smile" }, "description": "D\u00e9tails", "title": "Se connecter \u00e0 Smile" diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json index 1c4f8ded80c..5d3afb061fc 100644 --- a/homeassistant/components/plugwise/translations/ru.json +++ b/homeassistant/components/plugwise/translations/ru.json @@ -13,7 +13,8 @@ "user": { "data": { "host": "IP-\u0430\u0434\u0440\u0435\u0441", - "password": "ID \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + "password": "ID \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "port": "\u041d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430 Smile" }, "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 Plugwise.", "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" diff --git a/homeassistant/components/sensor/translations/et.json b/homeassistant/components/sensor/translations/et.json index 8238a8b6ab0..545abddd1f9 100644 --- a/homeassistant/components/sensor/translations/et.json +++ b/homeassistant/components/sensor/translations/et.json @@ -1,4 +1,25 @@ { + "device_automation": { + "condition_type": { + "is_battery_level": "Praegune {entity_name} aku tase", + "is_humidity": "Praegune {entity_name} niiskus", + "is_illuminance": "Praegune {entity_name} valgustatus", + "is_power": "Praegune {entity_name} toide (v\u00f5imsus)", + "is_pressure": "Praegune {entity_name} r\u00f5hk", + "is_signal_strength": "Praegune {entity_name} signaali tugevus", + "is_temperature": "Praegune {entity_name} temperatuur", + "is_timestamp": "Praegune {entity_name} aeg", + "is_value": "Praegune {entity_name} v\u00e4\u00e4rtus" + }, + "trigger_type": { + "battery_level": "{entity_name} aku tase muutub", + "humidity": "{entity_name} niiskus muutub", + "illuminance": "{entity_name} valgustustugevus muutub", + "power": "{entity_name} energiare\u017eiimi muutub", + "pressure": "{entity_name} r\u00f5hk muutub", + "signal_strength": "{entity_name} signaalitugevus muutub" + } + }, "state": { "_": { "off": "V\u00e4ljas", diff --git a/homeassistant/components/smhi/translations/et.json b/homeassistant/components/smhi/translations/et.json new file mode 100644 index 00000000000..e972172e9d8 --- /dev/null +++ b/homeassistant/components/smhi/translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "name": "Nimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/et.json b/homeassistant/components/transmission/translations/et.json new file mode 100644 index 00000000000..d7f39519aad --- /dev/null +++ b/homeassistant/components/transmission/translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nimi", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/translations/et.json b/homeassistant/components/vacuum/translations/et.json index 7d221f9be58..32d361151b8 100644 --- a/homeassistant/components/vacuum/translations/et.json +++ b/homeassistant/components/vacuum/translations/et.json @@ -1,4 +1,18 @@ { + "device_automation": { + "action_type": { + "clean": "{entity_name} puhastamise lubamine", + "dock": "Laske {entity_name} dokki naasta" + }, + "condition_type": { + "is_cleaning": "{entity_name} puhastab", + "is_docked": "{entity_name} on emajaamas" + }, + "trigger_type": { + "cleaning": "{entity_name} alustas puhastamist", + "docked": "{entity_name} on emajaamas" + } + }, "state": { "_": { "cleaning": "Puhastamine", diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json new file mode 100644 index 00000000000..e72c3dea7b0 --- /dev/null +++ b/homeassistant/components/zha/translations/et.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "warn": "Hoiata" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/no.json b/homeassistant/components/zoneminder/translations/no.json index f711b6eaf06..3e5dc0867c0 100644 --- a/homeassistant/components/zoneminder/translations/no.json +++ b/homeassistant/components/zoneminder/translations/no.json @@ -15,7 +15,7 @@ "step": { "user": { "data": { - "host": "Vert og port (ex 10.10.0.4:8010)", + "host": "Vert og port (f.eks. 10.10.0.4:8010)", "password": "Passord", "path": "ZM-bane", "path_zms": "ZMS-bane", From 167490b71ce2e6006653ec6929e163aeaed74005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 20 Sep 2020 12:03:58 +0300 Subject: [PATCH 257/514] Complete helpers.service type hints (#40193) * Complete helpers.service type hints * Update homeassistant/helpers/service.py Co-authored-by: Paulus Schoutsen * Handle None entity.supported_features --- homeassistant/helpers/entity_platform.py | 2 +- homeassistant/helpers/service.py | 45 ++++++++++++++++-------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index da1a3635d72..39b09cef193 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -546,7 +546,7 @@ class EntityPlatform: async def handle_service(call: ServiceCall) -> None: """Handle the service.""" - await service.entity_service_call( # type: ignore + await service.entity_service_call( self.hass, [ plf diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index ad5a36467cf..20f7aa2d2d7 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -13,6 +13,7 @@ from typing import ( Optional, Set, Tuple, + Union, ) import voluptuous as vol @@ -43,10 +44,9 @@ from homeassistant.util.yaml.loader import JSON_TYPE if TYPE_CHECKING: from homeassistant.helpers.entity import Entity # noqa + from homeassistant.helpers.entity_platform import EntityPlatform -# mypy: allow-untyped-defs, no-check-untyped-defs - CONF_SERVICE_ENTITY_ID = "entity_id" CONF_SERVICE_DATA = "data" CONF_SERVICE_DATA_TEMPLATE = "data_template" @@ -340,7 +340,13 @@ def async_set_service_schema( @bind_hass -async def entity_service_call(hass, platforms, func, call, required_features=None): +async def entity_service_call( + hass: HomeAssistantType, + platforms: Iterable["EntityPlatform"], + func: Union[str, Callable[..., Any]], + call: ha.ServiceCall, + required_features: Optional[Iterable[int]] = None, +) -> None: """Handle an entity service call. Calls all platforms simultaneously. @@ -349,7 +355,9 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non user = await hass.auth.async_get_user(call.context.user_id) if user is None: raise UnknownUser(context=call.context) - entity_perms = user.permissions.check_entity + entity_perms: Optional[ + Callable[[str, str], bool] + ] = user.permissions.check_entity else: entity_perms = None @@ -361,7 +369,7 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non # If the service function is a string, we'll pass it the service call data if isinstance(func, str): - data = { + data: Union[Dict, ha.ServiceCall] = { key: val for key, val in call.data.items() if key not in cv.ENTITY_SERVICE_FIELDS @@ -373,7 +381,7 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non # Check the permissions # A list with entities to call the service on. - entity_candidates = [] + entity_candidates: List["Entity"] = [] if entity_perms is None: for platform in platforms: @@ -435,9 +443,12 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non continue # Skip entities that don't have the required feature. - if required_features is not None and not any( - entity.supported_features & feature_set == feature_set - for feature_set in required_features + if required_features is not None and ( + entity.supported_features is None + or not any( + entity.supported_features & feature_set == feature_set + for feature_set in required_features + ) ): continue @@ -476,12 +487,18 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non future.result() # pop exception if have -async def _handle_entity_call(hass, entity, func, data, context): +async def _handle_entity_call( + hass: HomeAssistantType, + entity: "Entity", + func: Union[str, Callable[..., Any]], + data: Union[Dict, ha.ServiceCall], + context: ha.Context, +) -> None: """Handle calling service method.""" entity.async_set_context(context) if isinstance(func, str): - result = hass.async_add_job(partial(getattr(entity, func), **data)) + result = hass.async_add_job(partial(getattr(entity, func), **data)) # type: ignore else: result = hass.async_add_job(func, entity, data) @@ -495,7 +512,7 @@ async def _handle_entity_call(hass, entity, func, data, context): func, entity.entity_id, ) - await result + await result # type: ignore @bind_hass @@ -530,12 +547,12 @@ def async_register_admin_service( def verify_domain_control(hass: HomeAssistantType, domain: str) -> Callable: """Ensure permission to access any entity under domain in service call.""" - def decorator(service_handler: Callable) -> Callable: + def decorator(service_handler: Callable[[ha.ServiceCall], Any]) -> Callable: """Decorate.""" if not asyncio.iscoroutinefunction(service_handler): raise HomeAssistantError("Can only decorate async functions.") - async def check_permissions(call): + async def check_permissions(call: ha.ServiceCall) -> Any: """Check user permission and raise before call if unauthorized.""" if not call.context.user_id: return await service_handler(call) From 4f5d3b4035db38e76a42c4170e4d4dbd9f8f4302 Mon Sep 17 00:00:00 2001 From: Brett Date: Sun, 20 Sep 2020 19:22:43 +1000 Subject: [PATCH 258/514] Rebuilt Splunk using custom library (#40123) * Rebuilt Splunk using splunk_data_sender * Fixing lint issues * Apply suggestions from code review Recommended Fixes Co-authored-by: Martin Hjelmare * Moved to single send queue and fixed ssl verify * Using coroutine and Asyncio.lock * Changed to custom library hass_splunk * Fixed "use_ssl" parameter * Better error catching * Better error log Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/splunk/__init__.py | 85 ++++---- homeassistant/components/splunk/manifest.json | 9 +- requirements_all.txt | 3 + tests/components/splunk/__init__.py | 1 - tests/components/splunk/test_init.py | 182 ------------------ 7 files changed, 60 insertions(+), 222 deletions(-) delete mode 100644 tests/components/splunk/__init__.py delete mode 100644 tests/components/splunk/test_init.py diff --git a/.coveragerc b/.coveragerc index e35695fb8b2..c1fe30c6f42 100644 --- a/.coveragerc +++ b/.coveragerc @@ -806,6 +806,7 @@ omit = homeassistant/components/spc/* homeassistant/components/speedtestdotnet/* homeassistant/components/spider/* + homeassistant/components/splunk/* homeassistant/components/spotcrime/sensor.py homeassistant/components/spotify/__init__.py homeassistant/components/spotify/media_player.py diff --git a/CODEOWNERS b/CODEOWNERS index 613617e9942..5b7f0c1ae3f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -404,6 +404,7 @@ homeassistant/components/sonos/* @cgtobi homeassistant/components/spaceapi/* @fabaff homeassistant/components/speedtestdotnet/* @rohankapoorcom @engrbm87 homeassistant/components/spider/* @peternijssen +homeassistant/components/splunk/* @Bre77 homeassistant/components/spotify/* @frenck homeassistant/components/sql/* @dgomes homeassistant/components/squeezebox/* @rajlaud diff --git a/homeassistant/components/splunk/__init__.py b/homeassistant/components/splunk/__init__.py index bbff510db14..ec1471a2272 100644 --- a/homeassistant/components/splunk/__init__.py +++ b/homeassistant/components/splunk/__init__.py @@ -1,9 +1,10 @@ -"""Support to send data to an Splunk instance.""" +"""Support to send data to a Splunk instance.""" import json import logging +import time -from aiohttp.hdrs import AUTHORIZATION -import requests +from aiohttp import ClientConnectionError, ClientResponseError +from hass_splunk import SplunkPayloadError, hass_splunk import voluptuous as vol from homeassistant.const import ( @@ -16,14 +17,15 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, ) from homeassistant.helpers import state as state_helper +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.helpers.json import JSONEncoder _LOGGER = logging.getLogger(__name__) -CONF_FILTER = "filter" DOMAIN = "splunk" +CONF_FILTER = "filter" DEFAULT_HOST = "localhost" DEFAULT_PORT = 8088 @@ -48,23 +50,7 @@ CONFIG_SCHEMA = vol.Schema( ) -def post_request(event_collector, body, headers, verify_ssl): - """Post request to Splunk.""" - try: - payload = {"host": event_collector, "event": body} - requests.post( - event_collector, - data=json.dumps(payload, cls=JSONEncoder), - headers=headers, - timeout=10, - verify=verify_ssl, - ) - - except requests.exceptions.RequestException as error: - _LOGGER.exception("Error saving event to Splunk: %s", error) - - -def setup(hass, config): +async def async_setup(hass, config): """Set up the Splunk component.""" conf = config[DOMAIN] host = conf.get(CONF_HOST) @@ -75,18 +61,33 @@ def setup(hass, config): name = conf.get(CONF_NAME) entity_filter = conf[CONF_FILTER] - if use_ssl: - uri_scheme = "https://" - else: - uri_scheme = "http://" + event_collector = hass_splunk( + session=async_get_clientsession(hass), + host=host, + port=port, + token=token, + use_ssl=use_ssl, + verify_ssl=verify_ssl, + ) - event_collector = f"{uri_scheme}{host}:{port}/services/collector/event" - headers = {AUTHORIZATION: f"Splunk {token}"} + if not await event_collector.check(connectivity=False, token=True, busy=False): + return False - def splunk_event_listener(event): + payload = { + "time": time.time(), + "host": name, + "event": { + "domain": DOMAIN, + "meta": "Splunk integration has started", + }, + } + + await event_collector.queue(json.dumps(payload, cls=JSONEncoder), send=False) + + async def splunk_event_listener(event): """Listen for new messages on the bus and sends them to Splunk.""" - state = event.data.get("new_state") + state = event.data.get("new_state") if state is None or not entity_filter(state.entity_id): return @@ -95,19 +96,29 @@ def setup(hass, config): except ValueError: _state = state.state - json_body = [ - { + payload = { + "time": event.time_fired.timestamp(), + "host": name, + "event": { "domain": state.domain, "entity_id": state.object_id, "attributes": dict(state.attributes), - "time": str(event.time_fired), "value": _state, - "host": name, - } - ] + }, + } - post_request(event_collector, json_body, headers, verify_ssl) + try: + await event_collector.queue(json.dumps(payload, cls=JSONEncoder), send=True) + except SplunkPayloadError as err: + if err.status == 401: + _LOGGER.error(err) + else: + _LOGGER.warning(err) + except ClientConnectionError as err: + _LOGGER.warning(err) + except ClientResponseError as err: + _LOGGER.error(err.message) - hass.bus.listen(EVENT_STATE_CHANGED, splunk_event_listener) + hass.bus.async_listen(EVENT_STATE_CHANGED, splunk_event_listener) return True diff --git a/homeassistant/components/splunk/manifest.json b/homeassistant/components/splunk/manifest.json index 337458b4c3f..aaddac2609d 100644 --- a/homeassistant/components/splunk/manifest.json +++ b/homeassistant/components/splunk/manifest.json @@ -2,5 +2,10 @@ "domain": "splunk", "name": "Splunk", "documentation": "https://www.home-assistant.io/integrations/splunk", - "codeowners": [] -} + "requirements": [ + "hass_splunk==0.1.0" + ], + "codeowners": [ + "@Bre77" + ] +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 5ec5ccd8784..73e33aa80ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -722,6 +722,9 @@ hangups==0.4.11 # homeassistant.components.cloud hass-nabucasa==0.37.0 +# homeassistant.components.splunk +hass_splunk==0.1.0 + # homeassistant.components.jewish_calendar hdate==0.9.5 diff --git a/tests/components/splunk/__init__.py b/tests/components/splunk/__init__.py deleted file mode 100644 index 709483291e3..00000000000 --- a/tests/components/splunk/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the splunk component.""" diff --git a/tests/components/splunk/test_init.py b/tests/components/splunk/test_init.py deleted file mode 100644 index 86de865bc0d..00000000000 --- a/tests/components/splunk/test_init.py +++ /dev/null @@ -1,182 +0,0 @@ -"""The tests for the Splunk component.""" -import json -import unittest -from unittest import mock - -import homeassistant.components.splunk as splunk -from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON -from homeassistant.core import State -from homeassistant.helpers import state as state_helper -from homeassistant.setup import setup_component -import homeassistant.util.dt as dt_util - -from tests.common import get_test_home_assistant, mock_state_change_event - - -class TestSplunk(unittest.TestCase): - """Test the Splunk component.""" - - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.addCleanup(self.tear_down_cleanup) - - def tear_down_cleanup(self): - """Stop everything that was started.""" - self.hass.stop() - - def test_setup_config_full(self): - """Test setup with all data.""" - config = { - "splunk": { - "host": "host", - "port": 123, - "token": "secret", - "ssl": "False", - "verify_ssl": "True", - "name": "hostname", - "filter": { - "exclude_domains": ["fake"], - "exclude_entities": ["fake.entity"], - }, - } - } - - self.hass.bus.listen = mock.MagicMock() - assert setup_component(self.hass, splunk.DOMAIN, config) - assert self.hass.bus.listen.called - assert EVENT_STATE_CHANGED == self.hass.bus.listen.call_args_list[0][0][0] - - def test_setup_config_defaults(self): - """Test setup with defaults.""" - config = {"splunk": {"host": "host", "token": "secret"}} - - self.hass.bus.listen = mock.MagicMock() - assert setup_component(self.hass, splunk.DOMAIN, config) - assert self.hass.bus.listen.called - assert EVENT_STATE_CHANGED == self.hass.bus.listen.call_args_list[0][0][0] - - def _setup(self, mock_requests): - """Test the setup.""" - # pylint: disable=attribute-defined-outside-init - self.mock_post = mock_requests.post - self.mock_request_exception = Exception - mock_requests.exceptions.RequestException = self.mock_request_exception - config = {"splunk": {"host": "host", "token": "secret", "port": 8088}} - - self.hass.bus.listen = mock.MagicMock() - setup_component(self.hass, splunk.DOMAIN, config) - self.handler_method = self.hass.bus.listen.call_args_list[0][0][1] - - @mock.patch.object(splunk, "requests") - def test_event_listener(self, mock_requests): - """Test event listener.""" - self._setup(mock_requests) - - now = dt_util.now() - valid = {"1": 1, "1.0": 1.0, STATE_ON: 1, STATE_OFF: 0, "foo": "foo"} - - for in_, out in valid.items(): - state = mock.MagicMock( - state=in_, - domain="fake", - object_id="entity", - attributes={"datetime_attr": now}, - ) - event = mock.MagicMock(data={"new_state": state}, time_fired=12345) - - try: - out = state_helper.state_as_number(state) - except ValueError: - out = state.state - - body = [ - { - "domain": "fake", - "entity_id": "entity", - "attributes": {"datetime_attr": now.isoformat()}, - "time": "12345", - "value": out, - "host": "HASS", - } - ] - - payload = { - "host": "http://host:8088/services/collector/event", - "event": body, - } - self.handler_method(event) - assert self.mock_post.call_count == 1 - assert self.mock_post.call_args == mock.call( - payload["host"], - data=json.dumps(payload), - headers={"Authorization": "Splunk secret"}, - timeout=10, - verify=True, - ) - self.mock_post.reset_mock() - - def _setup_with_filter(self, addl_filters=None): - """Test the setup.""" - config = { - "splunk": { - "host": "host", - "token": "secret", - "port": 8088, - "filter": { - "exclude_domains": ["excluded_domain"], - "exclude_entities": ["other_domain.excluded_entity"], - }, - } - } - if addl_filters: - config["splunk"]["filter"].update(addl_filters) - - setup_component(self.hass, splunk.DOMAIN, config) - - @mock.patch.object(splunk, "post_request") - def test_splunk_entityfilter(self, mock_requests): - """Test event listener.""" - # pylint: disable=no-member - self._setup_with_filter() - - testdata = [ - {"entity_id": "other_domain.other_entity", "filter_expected": False}, - {"entity_id": "other_domain.excluded_entity", "filter_expected": True}, - {"entity_id": "excluded_domain.other_entity", "filter_expected": True}, - ] - - for test in testdata: - mock_state_change_event(self.hass, State(test["entity_id"], "on")) - self.hass.block_till_done() - - if test["filter_expected"]: - assert not splunk.post_request.called - else: - assert splunk.post_request.called - - splunk.post_request.reset_mock() - - @mock.patch.object(splunk, "post_request") - def test_splunk_entityfilter_with_glob_filter(self, mock_requests): - """Test event listener.""" - # pylint: disable=no-member - self._setup_with_filter({"exclude_entity_globs": ["*.skip_*"]}) - - testdata = [ - {"entity_id": "other_domain.other_entity", "filter_expected": False}, - {"entity_id": "other_domain.excluded_entity", "filter_expected": True}, - {"entity_id": "excluded_domain.other_entity", "filter_expected": True}, - {"entity_id": "test.skip_me", "filter_expected": True}, - ] - - for test in testdata: - mock_state_change_event(self.hass, State(test["entity_id"], "on")) - self.hass.block_till_done() - - if test["filter_expected"]: - assert not splunk.post_request.called - else: - assert splunk.post_request.called - - splunk.post_request.reset_mock() From 0c077685b6f74179cbaecde31577b0da4f975ad6 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 20 Sep 2020 13:19:10 +0200 Subject: [PATCH 259/514] Use content type text plain constant (#40313) --- tests/components/prometheus/test_init.py | 3 ++- tests/components/rest/test_sensor.py | 4 ++-- tests/components/rest_command/test_init.py | 13 +++++++------ 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index e04cbf9e632..f187429b151 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -9,6 +9,7 @@ from homeassistant.components.demo.sensor import DemoSensor import homeassistant.components.prometheus as prometheus from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONTENT_TYPE_TEXT_PLAIN, DEGREE, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, @@ -97,7 +98,7 @@ async def test_view(hass, hass_client): resp = await client.get(prometheus.API_ENDPOINT) assert resp.status == 200 - assert resp.headers["content-type"] == "text/plain" + assert resp.headers["content-type"] == CONTENT_TYPE_TEXT_PLAIN body = await resp.text() body = body.split("\n") diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 4351239064a..7fbb211fb4f 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -12,7 +12,7 @@ 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, SERVICE_RELOAD +from homeassistant.const import CONTENT_TYPE_TEXT_PLAIN, DATA_MEGABYTES, SERVICE_RELOAD from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.config_validation import template from homeassistant.setup import async_setup_component, setup_component @@ -413,7 +413,7 @@ class TestRestSensor(unittest.TestCase): "rest.RestData.update", side_effect=self.update_side_effect( "This is text rather than JSON data.", - CaseInsensitiveDict({"Content-Type": "text/plain"}), + CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_TEXT_PLAIN}), ), ) self.sensor = rest.RestSensor( diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py index 0aee8ccfbcc..780e513ea38 100644 --- a/tests/components/rest_command/test_init.py +++ b/tests/components/rest_command/test_init.py @@ -4,6 +4,7 @@ import asyncio import aiohttp import homeassistant.components.rest_command as rc +from homeassistant.const import CONTENT_TYPE_TEXT_PLAIN from homeassistant.setup import setup_component from tests.common import assert_setup_component, get_test_home_assistant @@ -218,7 +219,7 @@ class TestRestCommandComponent: header_config_variations = { rc.DOMAIN: { "no_headers_test": {}, - "content_type_test": {"content_type": "text/plain"}, + "content_type_test": {"content_type": CONTENT_TYPE_TEXT_PLAIN}, "headers_test": { "headers": { "Accept": "application/json", @@ -227,14 +228,14 @@ class TestRestCommandComponent: }, "headers_and_content_type_test": { "headers": {"Accept": "application/json"}, - "content_type": "text/plain", + "content_type": CONTENT_TYPE_TEXT_PLAIN, }, "headers_and_content_type_override_test": { "headers": { "Accept": "application/json", aiohttp.hdrs.CONTENT_TYPE: "application/pdf", }, - "content_type": "text/plain", + "content_type": CONTENT_TYPE_TEXT_PLAIN, }, "headers_template_test": { "headers": { @@ -285,7 +286,7 @@ class TestRestCommandComponent: assert len(aioclient_mock.mock_calls[1][3]) == 1 assert ( aioclient_mock.mock_calls[1][3].get(aiohttp.hdrs.CONTENT_TYPE) - == "text/plain" + == CONTENT_TYPE_TEXT_PLAIN ) # headers_test @@ -297,7 +298,7 @@ class TestRestCommandComponent: assert len(aioclient_mock.mock_calls[3][3]) == 2 assert ( aioclient_mock.mock_calls[3][3].get(aiohttp.hdrs.CONTENT_TYPE) - == "text/plain" + == CONTENT_TYPE_TEXT_PLAIN ) assert aioclient_mock.mock_calls[3][3].get("Accept") == "application/json" @@ -305,7 +306,7 @@ class TestRestCommandComponent: assert len(aioclient_mock.mock_calls[4][3]) == 2 assert ( aioclient_mock.mock_calls[4][3].get(aiohttp.hdrs.CONTENT_TYPE) - == "text/plain" + == CONTENT_TYPE_TEXT_PLAIN ) assert aioclient_mock.mock_calls[4][3].get("Accept") == "application/json" From 45288431f91e8d304283af3f3857fb0523e5cbca Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sun, 20 Sep 2020 23:40:36 +0200 Subject: [PATCH 260/514] Update xknx to 0.14.2 (#40304) * Updates xknx to 0.14.0 * Review: Explicity add state attributes to weather device * Review: Remove state attributes from weather device * Review: Add `counter` as a state attribute to binary_sensors --- homeassistant/components/knx/__init__.py | 48 ++++++++++++------- homeassistant/components/knx/binary_sensor.py | 11 ++++- homeassistant/components/knx/const.py | 2 + homeassistant/components/knx/factory.py | 30 +++--------- homeassistant/components/knx/manifest.json | 2 +- homeassistant/components/knx/schema.py | 28 ++++------- homeassistant/components/knx/weather.py | 1 + requirements_all.txt | 2 +- 8 files changed, 61 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 5a2f29e6247..f9f78f195bb 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -6,7 +6,12 @@ from xknx import XKNX 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.io import ( + DEFAULT_MCAST_GRP, + DEFAULT_MCAST_PORT, + ConnectionConfig, + ConnectionType, +) from xknx.telegram import AddressFilter, GroupAddress, Telegram from homeassistant.const import ( @@ -48,6 +53,9 @@ CONF_KNX_ROUTING = "routing" CONF_KNX_TUNNELING = "tunneling" CONF_KNX_FIRE_EVENT = "fire_event" CONF_KNX_FIRE_EVENT_FILTER = "fire_event_filter" +CONF_KNX_INDIVIDUAL_ADDRESS = "individual_address" +CONF_KNX_MCAST_GRP = "multicast_group" +CONF_KNX_MCAST_PORT = "multicast_port" CONF_KNX_STATE_UPDATER = "state_updater" CONF_KNX_RATE_LIMIT = "rate_limit" CONF_KNX_EXPOSE = "expose" @@ -72,6 +80,11 @@ CONFIG_SCHEMA = vol.Schema( vol.Inclusive(CONF_KNX_FIRE_EVENT_FILTER, "fire_ev"): vol.All( cv.ensure_list, [cv.string] ), + vol.Optional( + CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS + ): cv.string, + vol.Optional(CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP): cv.string, + vol.Optional(CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT): cv.port, vol.Optional(CONF_KNX_STATE_UPDATER, default=True): cv.boolean, vol.Optional(CONF_KNX_RATE_LIMIT, default=20): vol.All( vol.Coerce(int), vol.Range(min=1, max=100) @@ -130,17 +143,15 @@ async def async_setup(hass, config): hass.data[DATA_KNX].async_create_exposures() await hass.data[DATA_KNX].start() except XKNXException as ex: - _LOGGER.warning("Can't connect to KNX interface: %s", ex) + _LOGGER.warning("Could not connect to KNX interface: %s", ex) hass.components.persistent_notification.async_create( - f"Can't connect to KNX interface:
{ex}", title="KNX" + f"Could not connect to KNX interface:
{ex}", title="KNX" ) 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 - ) + create_knx_device(platform, hass.data[DATA_KNX].xknx, device_config) # 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: @@ -181,7 +192,10 @@ class KNXModule: self.xknx = XKNX( config=self.config_file(), loop=self.hass.loop, + own_address=self.config[DOMAIN][CONF_KNX_INDIVIDUAL_ADDRESS], rate_limit=self.config[DOMAIN][CONF_KNX_RATE_LIMIT], + multicast_group=self.config[DOMAIN][CONF_KNX_MCAST_GRP], + multicast_port=self.config[DOMAIN][CONF_KNX_MCAST_PORT], ) async def start(self): @@ -229,12 +243,10 @@ class KNXModule: def connection_config_tunneling(self): """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) + gateway_port = self.config[DOMAIN][CONF_KNX_TUNNELING][CONF_PORT] 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( connection_type=ConnectionType.TUNNELING, gateway_ip=gateway_ip, @@ -267,7 +279,7 @@ class KNXModule: 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"]: + if expose_type.lower() in ["time", "date", "datetime"]: exposure = KNXExposeTime(self.xknx, expose_type, address) exposure.async_register() self.exposures.append(exposure) @@ -313,29 +325,29 @@ class KNXModule: payload = calculate_payload(attr_payload) address = GroupAddress(attr_address) - telegram = Telegram() - telegram.payload = payload - telegram.group_address = address + telegram = Telegram(group_address=address, payload=payload) await self.xknx.telegrams.put(telegram) class KNXExposeTime: """Object to Expose Time/Date object to KNX bus.""" - def __init__(self, xknx, expose_type, address): + def __init__(self, xknx: XKNX, expose_type: str, address: str): """Initialize of Expose class.""" self.xknx = xknx - self.type = expose_type + self.expose_type = expose_type self.address = address self.device = None @callback def async_register(self): """Register listener.""" - broadcast_type_string = self.type.upper() - broadcast_type = broadcast_type_string self.device = DateTime( - self.xknx, "Time", broadcast_type=broadcast_type, group_address=self.address + self.xknx, + name=self.expose_type.capitalize(), + broadcast_type=self.expose_type.upper(), + localtime=True, + group_address=self.address, ) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index f3b7e881134..24fbe472ae3 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -1,10 +1,12 @@ """Support for KNX/IP binary sensors.""" +from typing import Any, Dict, Optional + from xknx.devices import BinarySensor as XknxBinarySensor from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import callback -from . import DATA_KNX +from .const import ATTR_COUNTER, DATA_KNX async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -27,7 +29,7 @@ class KNXBinarySensor(BinarySensorEntity): def async_register_callbacks(self): """Register callbacks to update hass after device was changed.""" - async def after_update_callback(device): + async def after_update_callback(device: XknxBinarySensor): """Call after device was updated.""" self.async_write_ha_state() @@ -65,3 +67,8 @@ class KNXBinarySensor(BinarySensorEntity): def is_on(self): """Return true if the binary sensor is on.""" return self.device.is_on() + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return device specific state attributes.""" + return {ATTR_COUNTER: self.device.counter} diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index a81fc526415..427bdb0bd0b 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -60,3 +60,5 @@ PRESET_MODES = { "Standby": PRESET_AWAY, "Comfort": PRESET_COMFORT, } + +ATTR_COUNTER = "counter" diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index 42c4dd675f5..3334e49ce38 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -1,7 +1,6 @@ """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, @@ -16,11 +15,9 @@ from xknx.devices import ( ) 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 DOMAIN, ColorTempModes, SupportedPlatforms +from .const import ColorTempModes, SupportedPlatforms from .schema import ( BinarySensorSchema, ClimateSchema, @@ -34,7 +31,6 @@ from .schema import ( def create_knx_device( - hass: HomeAssistant, platform: SupportedPlatforms, knx_module: XKNX, config: ConfigType, @@ -62,7 +58,7 @@ def create_knx_device( return _create_scene(knx_module, config) if platform is SupportedPlatforms.binary_sensor: - return _create_binary_sensor(hass, knx_module, config) + return _create_binary_sensor(knx_module, config) if platform is SupportedPlatforms.weather: return _create_weather(knx_module, config) @@ -239,24 +235,9 @@ def _create_scene(knx_module: XKNX, config: ConfigType) -> XknxScene: ) -def _create_binary_sensor( - hass: HomeAssistant, knx_module: XKNX, config: ConfigType -) -> XknxBinarySensor: +def _create_binary_sensor(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, @@ -265,8 +246,8 @@ def _create_binary_sensor( sync_state=config[BinarySensorSchema.CONF_SYNC_STATE], device_class=config.get(CONF_DEVICE_CLASS), ignore_internal_state=config[BinarySensorSchema.CONF_IGNORE_INTERNAL_STATE], + context_timeout=config[BinarySensorSchema.CONF_CONTEXT_TIMEOUT], reset_after=config.get(BinarySensorSchema.CONF_RESET_AFTER), - actions=actions, ) @@ -287,6 +268,9 @@ def _create_weather(knx_module: XKNX, config: ConfigType) -> XknxWeather: group_address_brightness_west=config.get( WeatherSchema.CONF_KNX_BRIGHTNESS_WEST_ADDRESS ), + group_address_brightness_north=config.get( + WeatherSchema.CONF_KNX_BRIGHTNESS_NORTH_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( diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 8986d85b8b6..af9f99677f3 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.13.0"], + "requirements": ["xknx==0.14.2"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"] } diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index a436f2dcdc8..84a54536db5 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -1,6 +1,7 @@ """Voluptuous schemas for the KNX integration.""" import voluptuous as vol from xknx.devices.climate import SetpointShiftMode +from xknx.io import DEFAULT_MCAST_PORT from homeassistant.const import ( CONF_ADDRESS, @@ -29,9 +30,9 @@ class ConnectionSchema: TUNNELING_SCHEMA = vol.Schema( { + vol.Optional(CONF_PORT, default=DEFAULT_MCAST_PORT): cv.port, vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_KNX_LOCAL_IP): cv.string, - vol.Optional(CONF_PORT): cv.port, } ) @@ -84,27 +85,14 @@ class BinarySensorSchema: 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_CONTEXT_TIMEOUT = "context_timeout" 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"), + cv.deprecated("automation"), vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -113,11 +101,13 @@ class BinarySensorSchema: cv.boolean, cv.string, ), - vol.Optional(CONF_IGNORE_INTERNAL_STATE, default=False): cv.boolean, + vol.Optional(CONF_IGNORE_INTERNAL_STATE, default=True): cv.boolean, + vol.Optional(CONF_CONTEXT_TIMEOUT, default=1.0): vol.All( + vol.Coerce(float), vol.Range(min=0, max=10) + ), 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, } ), ) @@ -350,6 +340,7 @@ class WeatherSchema: 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_BRIGHTNESS_NORTH_ADDRESS = "address_brightness_north" CONF_KNX_WIND_SPEED_ADDRESS = "address_wind_speed" CONF_KNX_RAIN_ALARM_ADDRESS = "address_rain_alarm" CONF_KNX_FROST_ALARM_ADDRESS = "address_frost_alarm" @@ -374,6 +365,7 @@ class WeatherSchema: 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_BRIGHTNESS_NORTH_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, diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 97500ef8194..09dc1a305c6 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -1,4 +1,5 @@ """Support for KNX/IP weather station.""" + from xknx.devices import Weather as XknxWeather from homeassistant.components.weather import WeatherEntity diff --git a/requirements_all.txt b/requirements_all.txt index 73e33aa80ab..6146c531bf2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2265,7 +2265,7 @@ xboxapi==2.0.1 xfinity-gateway==0.0.4 # homeassistant.components.knx -xknx==0.13.0 +xknx==0.14.2 # homeassistant.components.bluesound # homeassistant.components.rest From 83b0954e58dedef16ed545552966f229a7fd78c8 Mon Sep 17 00:00:00 2001 From: Michael Thingnes Date: Mon, 21 Sep 2020 09:56:04 +1200 Subject: [PATCH 261/514] Fix Met.no missing conditions in API forecasts (#40373) --- homeassistant/components/met/weather.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 3355c497aab..3abd7638516 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -214,8 +214,9 @@ class MetWeather(CoordinatorEntity, WeatherEntity): 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] - ) + if ha_item.get(ATTR_FORECAST_CONDITION): + ha_item[ATTR_FORECAST_CONDITION] = format_condition( + ha_item[ATTR_FORECAST_CONDITION] + ) ha_forecast.append(ha_item) return ha_forecast From 432911c994166614905f6b15f3caecdfa451f71f Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 20 Sep 2020 17:38:02 -0500 Subject: [PATCH 262/514] Apply code review for canary config flow (#40355) * apply code review for canary config flow * Update __init__.py * Update __init__.py * Update __init__.py * Update camera.py * Update camera.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 --- homeassistant/components/canary/__init__.py | 30 +++++++++++++-------- homeassistant/components/canary/camera.py | 11 ++++++-- tests/components/canary/test_config_flow.py | 16 +++++++---- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index c0245b5b9d0..06f4134e24e 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -89,10 +89,8 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass.config_entries.async_update_entry(entry, options=options) try: - canary_data = CanaryData( - entry.data[CONF_USERNAME], - entry.data[CONF_PASSWORD], - entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), + canary_data = await hass.async_add_executor_job( + _get_canary_data_instance, entry ) except (ConnectTimeout, HTTPError) as error: _LOGGER.error("Unable to connect to Canary service: %s", str(error)) @@ -137,18 +135,14 @@ async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> class CanaryData: - """Get the latest data and update the states.""" + """Manages the data retrieved from Canary API.""" - def __init__(self, username, password, timeout): + def __init__(self, api: Api): """Init the Canary data object.""" - - self._api = Api(username, password, timeout) - + self._api = api self._locations_by_id = {} self._readings_by_device_id = {} - self.update() - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self, **kwargs): """Get the latest data from py-canary with a throttle.""" @@ -200,3 +194,17 @@ class CanaryData: def get_live_stream_session(self, device): """Return live stream session.""" return self._api.get_live_stream_session(device) + + +def _get_canary_data_instance(entry: ConfigEntry) -> CanaryData: + """Initialize a new instance of CanaryData.""" + canary = Api( + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), + ) + + canary_data = CanaryData(canary) + canary_data.update() + + return canary_data diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index bbf1284c602..1cc7a535344 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -31,8 +31,15 @@ _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_FFMPEG_ARGUMENTS): cv.string} +PLATFORM_SCHEMA = vol.All( + cv.deprecated(CONF_FFMPEG_ARGUMENTS, invalidation_version="0.118"), + PLATFORM_SCHEMA.extend( + { + vol.Optional( + CONF_FFMPEG_ARGUMENTS, default=DEFAULT_FFMPEG_ARGUMENTS + ): cv.string + } + ), ) diff --git a/tests/components/canary/test_config_flow.py b/tests/components/canary/test_config_flow.py index 2bd6ae6443d..36c6990a663 100644 --- a/tests/components/canary/test_config_flow.py +++ b/tests/components/canary/test_config_flow.py @@ -18,6 +18,8 @@ from homeassistant.setup import async_setup_component from . import USER_INPUT, _patch_async_setup, _patch_async_setup_entry, init_integration +from tests.async_mock import patch + async def test_user_form(hass, canary_config_flow): """Test we get the user initiated form.""" @@ -103,7 +105,9 @@ async def test_user_form_single_instance_allowed(hass, canary_config_flow): async def test_options_flow(hass): """Test updating options.""" - entry = await init_integration(hass, skip_entry_setup=True) + with patch("homeassistant.components.canary.PLATFORMS", []): + entry = await init_integration(hass) + assert entry.options[CONF_FFMPEG_ARGUMENTS] == DEFAULT_FFMPEG_ARGUMENTS assert entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT @@ -111,10 +115,12 @@ async def test_options_flow(hass): 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_FFMPEG_ARGUMENTS: "-v", CONF_TIMEOUT: 7}, - ) + with _patch_async_setup(), _patch_async_setup_entry(): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_FFMPEG_ARGUMENTS: "-v", CONF_TIMEOUT: 7}, + ) + await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_FFMPEG_ARGUMENTS] == "-v" From 587e3f1eb21514e1762382a2648c1ce8b63c900a Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 21 Sep 2020 00:02:47 +0000 Subject: [PATCH 263/514] [ci skip] Translation update --- .../accuweather/translations/et.json | 35 ++++++++ .../accuweather/translations/sensor.et.json | 9 ++ .../components/airvisual/translations/et.json | 12 +++ .../alarm_control_panel/translations/et.json | 15 ++++ .../alarmdecoder/translations/ko.json | 74 ++++++++++++++++ .../components/almond/translations/ko.json | 3 +- .../components/august/translations/ko.json | 3 +- .../binary_sensor/translations/et.json | 10 ++- .../components/broadlink/translations/ko.json | 7 ++ .../components/brother/translations/et.json | 29 +++++++ .../components/canary/translations/ko.json | 30 +++++++ .../components/cast/translations/et.json | 13 +++ .../components/climate/translations/et.json | 2 +- .../coronavirus/translations/et.json | 15 ++++ .../components/cover/translations/et.json | 2 +- .../dialogflow/translations/et.json | 17 ++++ .../components/dsmr/translations/ko.json | 7 ++ .../emulated_roku/translations/et.json | 3 +- .../home_connect/translations/ko.json | 3 +- .../homekit_controller/translations/ko.json | 21 +++++ .../homematicip_cloud/translations/ko.json | 1 + .../input_boolean/translations/et.json | 2 +- .../input_datetime/translations/et.json | 2 +- .../input_number/translations/et.json | 2 +- .../input_select/translations/et.json | 2 +- .../input_text/translations/et.json | 2 +- .../components/insteon/translations/ko.json | 34 ++++++++ .../components/ipp/translations/et.json | 35 ++++++++ .../components/light/translations/et.json | 15 ++++ .../components/local_ip/translations/et.json | 13 +++ .../components/lock/translations/et.json | 5 ++ .../mobile_app/translations/et.json | 12 +++ .../moon/translations/sensor.et.json | 14 +++ .../components/mqtt/translations/et.json | 85 +++++++++++++++++++ .../components/netatmo/translations/ko.json | 3 +- .../components/notify/translations/et.json | 2 +- .../components/nzbget/translations/ko.json | 36 ++++++++ .../openweathermap/translations/ko.json | 35 ++++++++ .../components/ozw/translations/et.json | 7 ++ .../components/plugwise/translations/ko.json | 10 +++ .../progettihwsw/translations/ko.json | 43 ++++++++++ .../components/remote/translations/ko.json | 15 ++++ .../components/risco/translations/ko.json | 28 ++++++ .../components/rpi_power/translations/ko.json | 14 +++ .../season/translations/sensor.et.json | 16 ++++ .../components/sensor/translations/et.json | 5 +- .../components/sharkiq/translations/ko.json | 29 +++++++ .../components/shelly/translations/ko.json | 15 ++++ .../components/smappee/translations/ko.json | 3 +- .../components/somfy/translations/ko.json | 3 +- .../components/spotify/translations/et.json | 17 ++++ .../components/spotify/translations/ko.json | 7 +- .../components/starline/translations/et.json | 15 ++++ .../components/switch/translations/et.json | 6 ++ .../synology_dsm/translations/ko.json | 3 +- .../components/toon/translations/ko.json | 3 +- .../components/tuya/translations/et.json | 24 ++++++ .../components/unifi/translations/ko.json | 3 +- .../components/upnp/translations/et.json | 30 +++++++ .../components/withings/translations/ko.json | 3 +- .../components/wled/translations/et.json | 24 ++++++ .../components/yeelight/translations/ko.json | 40 +++++++++ .../zoneminder/translations/ko.json | 30 +++++++ 63 files changed, 981 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/accuweather/translations/et.json create mode 100644 homeassistant/components/accuweather/translations/sensor.et.json create mode 100644 homeassistant/components/airvisual/translations/et.json create mode 100644 homeassistant/components/alarmdecoder/translations/ko.json create mode 100644 homeassistant/components/broadlink/translations/ko.json create mode 100644 homeassistant/components/brother/translations/et.json create mode 100644 homeassistant/components/canary/translations/ko.json create mode 100644 homeassistant/components/cast/translations/et.json create mode 100644 homeassistant/components/coronavirus/translations/et.json create mode 100644 homeassistant/components/dialogflow/translations/et.json create mode 100644 homeassistant/components/dsmr/translations/ko.json create mode 100644 homeassistant/components/insteon/translations/ko.json create mode 100644 homeassistant/components/ipp/translations/et.json create mode 100644 homeassistant/components/local_ip/translations/et.json create mode 100644 homeassistant/components/mobile_app/translations/et.json create mode 100644 homeassistant/components/moon/translations/sensor.et.json create mode 100644 homeassistant/components/mqtt/translations/et.json create mode 100644 homeassistant/components/nzbget/translations/ko.json create mode 100644 homeassistant/components/openweathermap/translations/ko.json create mode 100644 homeassistant/components/ozw/translations/et.json create mode 100644 homeassistant/components/progettihwsw/translations/ko.json create mode 100644 homeassistant/components/risco/translations/ko.json create mode 100644 homeassistant/components/rpi_power/translations/ko.json create mode 100644 homeassistant/components/season/translations/sensor.et.json create mode 100644 homeassistant/components/sharkiq/translations/ko.json create mode 100644 homeassistant/components/shelly/translations/ko.json create mode 100644 homeassistant/components/spotify/translations/et.json create mode 100644 homeassistant/components/starline/translations/et.json create mode 100644 homeassistant/components/tuya/translations/et.json create mode 100644 homeassistant/components/upnp/translations/et.json create mode 100644 homeassistant/components/wled/translations/et.json create mode 100644 homeassistant/components/yeelight/translations/ko.json create mode 100644 homeassistant/components/zoneminder/translations/ko.json diff --git a/homeassistant/components/accuweather/translations/et.json b/homeassistant/components/accuweather/translations/et.json new file mode 100644 index 00000000000..501f55001de --- /dev/null +++ b/homeassistant/components/accuweather/translations/et.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sidumine juba tehtud. V\u00f5imalik on ainult 1 sidumine," + }, + "error": { + "cannot_connect": "\u00dchendus eba\u00f5nnestus", + "invalid_api_key": "API v\u00f5ti on vale", + "requests_exceeded": "Accuweatheri API-le esitatud taotluste lubatud arv on \u00fcletatud. Peate ootama v\u00f5i muutma API v\u00f5tit." + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "name": "Sidumise nimi" + }, + "description": "Kui vajate seadistamisel abi vaadake siit: https://www.home-assistant.io/integrations/accuweather/ \n\n M\u00f5ni andur pole vaikimisi lubatud. P\u00e4rast sidumise seadistamist saate need \u00fcksused lubada. \n Ilmapennustus pole vaikimisi lubatud. Saate selle lubada sidumise s\u00e4tetes.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Ilmateade" + }, + "description": "AccuWeather API tasuta versioonis toimub ilmaennustuse lubamisel andmete v\u00e4rskendamine iga 32 minuti asemel iga 64 minuti j\u00e4rel.", + "title": "AccuWeatheri valikud" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/sensor.et.json b/homeassistant/components/accuweather/translations/sensor.et.json new file mode 100644 index 00000000000..ca58cd9ab6b --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.et.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Langev", + "rising": "T\u00f5usev", + "steady": "\u00dchtlane" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/et.json b/homeassistant/components/airvisual/translations/et.json new file mode 100644 index 00000000000..5bbc2c47689 --- /dev/null +++ b/homeassistant/components/airvisual/translations/et.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "geography": { + "data": { + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/translations/et.json b/homeassistant/components/alarm_control_panel/translations/et.json index 28c47b5a06d..cb87bc6ed04 100644 --- a/homeassistant/components/alarm_control_panel/translations/et.json +++ b/homeassistant/components/alarm_control_panel/translations/et.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "arm_away": "Valvesta {entity_name}", + "arm_home": "Valvesta {entity_name} kodus re\u017eiimis", + "arm_night": "Valvesta {entity_name} \u00f6\u00f6re\u017eiimis", + "disarm": "V\u00f5ta {entity_name} valvest maha", + "trigger": "K\u00e4ivita {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} on valvestatud", + "armed_home": "{entity_name} on valvestatud kodure\u017eiimis", + "armed_night": "{entity_name} on valvestatud \u00f6\u00f6re\u017eiimis", + "disarmed": "{entity_name} v\u00f5eti valvest maha" + } + }, "state": { "_": { "armed": "Valves", diff --git a/homeassistant/components/alarmdecoder/translations/ko.json b/homeassistant/components/alarmdecoder/translations/ko.json new file mode 100644 index 00000000000..c4038572ece --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/ko.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "\uc7a5\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4." + }, + "create_entry": { + "default": "AlarmDecoder\uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "service_unavailable": "\uc5f0\uacb0 \uc2e4\ud328" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "\uc7a5\uce58 \uc804\uc1a1 \uc18d\ub3c4", + "device_path": "\uc7a5\uce58 \uacbd\ub85c", + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8" + }, + "title": "\uc5f0\uacb0 \uc124\uc815 \uad6c\uc131" + }, + "user": { + "data": { + "protocol": "\ud504\ub85c\ud1a0\ucf5c" + }, + "title": "AlarmDecoder \ud504\ub85c\ud1a0\ucf5c \uc120\ud0dd" + } + } + }, + "options": { + "error": { + "int": "\uc544\ub798 \ud544\ub4dc\ub294 \uc815\uc218\uc5ec\uc57c \ud569\ub2c8\ub2e4.", + "loop_range": "RF \ub8e8\ud504\ub294 1\uc5d0\uc11c 4 \uc0ac\uc774\uc758 \uc815\uc218\uc5ec\uc57c \ud569\ub2c8\ub2e4.", + "loop_rfid": "RF \ub8e8\ud504\ub294 RF \uc2dc\ub9ac\uc5bc\uc5c6\uc774 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "relay_inclusive": "\ub9b4\ub808\uc774 \uc8fc\uc18c\uc640 \ub9b4\ub808\uc774 \ucc44\ub110\uc740 \uc11c\ub85c \uc758\uc874\uc801\uc774\uba70 \ud568\uaed8 \ud3ec\ud568\ub418\uc5b4\uc57c\ud569\ub2c8\ub2e4." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "\ub300\uccb4 \uc57c\uac04 \ubaa8\ub4dc", + "auto_bypass": "\uacbd\ube44\uc911 \uc790\ub3d9 \uc6b0\ud68c", + "code_arm_required": "\uacbd\ube44\uc5d0 \ud544\uc694\ud55c \ucf54\ub4dc" + }, + "title": "AlarmDecoder \uad6c\uc131" + }, + "init": { + "data": { + "edit_select": "\ud3b8\uc9d1" + }, + "description": "\ubb34\uc5c7\uc744 \ud3b8\uc9d1 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "AlarmDecoder \uad6c\uc131" + }, + "zone_details": { + "data": { + "zone_loop": "RF \ub8e8\ud504", + "zone_name": "\uc601\uc5ed \uc774\ub984", + "zone_relayaddr": "\ub9b4\ub808\uc774 \uc8fc\uc18c", + "zone_relaychan": "\ub9b4\ub808\uc774 \ucc44\ub110", + "zone_rfid": "RF \uc2dc\ub9ac\uc5bc", + "zone_type": "\uc601\uc5ed \uc720\ud615" + }, + "description": "{zone_number} \uc601\uc5ed\uc5d0 \ub300\ud55c \uc138\ubd80 \uc815\ubcf4\ub97c \uc785\ub825\ud569\ub2c8\ub2e4. {zone_number} \uc601\uc5ed\uc744 \uc0ad\uc81c\ud558\ub824\uba74 \uc601\uc5ed \uc774\ub984\uc744 \ube44\uc6cc \ub461\ub2c8\ub2e4.", + "title": "AlarmDecoder \uad6c\uc131" + }, + "zone_select": { + "data": { + "zone_number": "\uad6c\uc5ed \ubc88\ud638" + }, + "description": "\ucd94\uac00, \ud3b8\uc9d1 \ub610\ub294 \uc81c\uac70\ud560 \uc601\uc5ed \ubc88\ud638\ub97c \uc785\ub825\ud569\ub2c8\ub2e4.", + "title": "AlarmDecoder \uad6c\uc131" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/ko.json b/homeassistant/components/almond/translations/ko.json index 645eaafab08..cb1f53882f8 100644 --- a/homeassistant/components/almond/translations/ko.json +++ b/homeassistant/components/almond/translations/ko.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "\ud558\ub098\uc758 Almond \uacc4\uc815\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "cannot_connect": "Almond \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", - "missing_configuration": "Almond \uc124\uc815 \ubc29\ubc95\uc5d0 \ub300\ud55c \uc124\uba85\uc11c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694." + "missing_configuration": "Almond \uc124\uc815 \ubc29\ubc95\uc5d0 \ub300\ud55c \uc124\uba85\uc11c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6b0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/august/translations/ko.json b/homeassistant/components/august/translations/ko.json index 52f939c45a0..c11bc55ec40 100644 --- a/homeassistant/components/august/translations/ko.json +++ b/homeassistant/components/august/translations/ko.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "reauth_successful": "\uc7ac\uc778\uc99d\uc774 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4." }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", diff --git a/homeassistant/components/binary_sensor/translations/et.json b/homeassistant/components/binary_sensor/translations/et.json index d6ea4dbbe7d..c074c56675a 100644 --- a/homeassistant/components/binary_sensor/translations/et.json +++ b/homeassistant/components/binary_sensor/translations/et.json @@ -78,7 +78,15 @@ "occupied": "{entity_name} h\u00f5ivati", "opened": "{entity_name} avanes", "plugged_in": "{entity_name} \u00fchendati", - "powered": "{entity_name} l\u00fcltus voolu alla" + "powered": "{entity_name} l\u00fcltus voolu alla", + "present": "{entity_name} on saadaval", + "problem": "{entity_name} avastas probleemi", + "smoke": "{entity_name} tuvastas suitsu", + "sound": "{entity_name} tuvastas heli", + "turned_off": "{entity_name} l\u00fclitus v\u00e4lja", + "turned_on": "{entity_name} l\u00fclitus sisse", + "unsafe": "{entity_name} on ebaturvaline", + "vibration": "{entity_name} registreeris vibratsiooni" } }, "state": { diff --git a/homeassistant/components/broadlink/translations/ko.json b/homeassistant/components/broadlink/translations/ko.json new file mode 100644 index 00000000000..a3121d0b7de --- /dev/null +++ b/homeassistant/components/broadlink/translations/ko.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "\uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \uc7a5\uce58" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/translations/et.json b/homeassistant/components/brother/translations/et.json new file mode 100644 index 00000000000..4c5bb032f0b --- /dev/null +++ b/homeassistant/components/brother/translations/et.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "unsupported_model": "Seda printeri mudelit ei toetata." + }, + "error": { + "connection_error": "\u00dchenduse t\u00f5rge.", + "snmp_error": "SNMP-server on v\u00e4lja l\u00fclitatud v\u00f5i printerit ei toetata.", + "wrong_host": "Sobimatu hostinimi v\u00f5i IP-aadress." + }, + "flow_title": "Brotheri printer: {model} {serial_number}", + "step": { + "user": { + "data": { + "host": "Host", + "type": "Printeri t\u00fc\u00fcp" + }, + "description": "Seadistage Brotheri printeri sidumine. Kui teil on seadistamisega probleeme minge aadressile https://www.home-assistant.io/integrations/brother" + }, + "zeroconf_confirm": { + "data": { + "type": "Printeri t\u00fc\u00fcp" + }, + "description": "Kas soovite lisada Home Assistanti Brotheri printeri {model} seerianumbriga \" {serial_number} \"?", + "title": "Avastatud Brotheri printer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/ko.json b/homeassistant/components/canary/translations/ko.json new file mode 100644 index 00000000000..3a68ce2da6c --- /dev/null +++ b/homeassistant/components/canary/translations/ko.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uc124\uc815\ub418\uc5b4 \uc788\uc74c. \ud558\ub098\uc758 \uc124\uc815\ub9cc \uac00\ub2a5\ud568.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec \ubc1c\uc0dd" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328" + }, + "step": { + "user": { + "data": { + "password": "\uc554\ud638", + "username": "\uc0ac\uc6a9\uc790\uba85" + }, + "title": "Canary\uc5d0 \uc5f0\uacb0" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "\uce74\uba54\ub77c ffmpeg\uc5d0 \uc804\ub2ec \ub41c \uc778\uc218", + "timeout": "\uc694\uccad \uc81c\ud55c \uc2dc\uac04 (\ucd08)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/translations/et.json b/homeassistant/components/cast/translations/et.json new file mode 100644 index 00000000000..05287b5a52b --- /dev/null +++ b/homeassistant/components/cast/translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi Google Casti seadet.", + "single_instance_allowed": "Vajalik on ainult \u00fcks Google Casti konfiguratsioon." + }, + "step": { + "confirm": { + "description": "Kas soovid seadistada Google Casti?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/translations/et.json b/homeassistant/components/climate/translations/et.json index 4cba2fc5e41..7be57f4cdaa 100644 --- a/homeassistant/components/climate/translations/et.json +++ b/homeassistant/components/climate/translations/et.json @@ -25,5 +25,5 @@ "off": "V\u00e4ljas" } }, - "title": "Kliima" + "title": "Kliimaseade" } \ No newline at end of file diff --git a/homeassistant/components/coronavirus/translations/et.json b/homeassistant/components/coronavirus/translations/et.json new file mode 100644 index 00000000000..a69b845e623 --- /dev/null +++ b/homeassistant/components/coronavirus/translations/et.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "See riik on juba seadistatud." + }, + "step": { + "user": { + "data": { + "country": "Riik" + }, + "title": "Vali j\u00e4lgiv riik" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/translations/et.json b/homeassistant/components/cover/translations/et.json index 2c9ff745720..8b48976a9e7 100644 --- a/homeassistant/components/cover/translations/et.json +++ b/homeassistant/components/cover/translations/et.json @@ -26,5 +26,5 @@ "stopped": "Peatatud" } }, - "title": "Kate" + "title": "Kardin" } \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/et.json b/homeassistant/components/dialogflow/translations/et.json new file mode 100644 index 00000000000..99298014f71 --- /dev/null +++ b/homeassistant/components/dialogflow/translations/et.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Dialogflow teavituste vastuv\u00f5tmiseks peab teie Home Assistant olema Interneti kaudu ligip\u00e4\u00e4setav.", + "one_instance_allowed": "Vaja on ainult \u00fchte \u00fcksust." + }, + "create_entry": { + "default": "S\u00fcndmuste saatmiseks Home Assistantile peate seadistama [Dialogflow'i veebihaagii integreerimine] ( {dialogflow_url} ). \n\n Sisestage j\u00e4rgmine teave: \n\n - URL: \" {webhook_url} \" \n - Meetod: POST \n - Sisu t\u00fc\u00fcp: rakendus / json \n\n Lisateavet leiate [dokumentatsioonist] ( {docs_url} )." + }, + "step": { + "user": { + "description": "Kas oled kindel, et soovid seadistada Dialogflow?", + "title": "Seadistage Dialogflow veebihaak" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/ko.json b/homeassistant/components/dsmr/translations/ko.json new file mode 100644 index 00000000000..9c8fbbe80a9 --- /dev/null +++ b/homeassistant/components/dsmr/translations/ko.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\uc7a5\uce58\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4." + } + } +} \ 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..cc4dd04a9ea 100644 --- a/homeassistant/components/emulated_roku/translations/et.json +++ b/homeassistant/components/emulated_roku/translations/et.json @@ -4,7 +4,8 @@ "user": { "data": { "host_ip": "", - "name": "Nimi" + "name": "Nimi", + "upnp_bind_multicast": "Seo multicast (jah/ei)" } } } diff --git a/homeassistant/components/home_connect/translations/ko.json b/homeassistant/components/home_connect/translations/ko.json index 973e1a0ec88..2b8e0c0af0f 100644 --- a/homeassistant/components/home_connect/translations/ko.json +++ b/homeassistant/components/home_connect/translations/ko.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "missing_configuration": "Home Connect \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + "missing_configuration": "Home Connect \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_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6b0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" }, "create_entry": { "default": "Home Connect \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." diff --git a/homeassistant/components/homekit_controller/translations/ko.json b/homeassistant/components/homekit_controller/translations/ko.json index 55c5ee0053d..a70a6269bb6 100644 --- a/homeassistant/components/homekit_controller/translations/ko.json +++ b/homeassistant/components/homekit_controller/translations/ko.json @@ -7,6 +7,7 @@ "already_paired": "\uc774 \uc561\uc138\uc11c\ub9ac\ub294 \uc774\ubbf8 \ub2e4\ub978 \uae30\uae30\uc640 \ud398\uc5b4\ub9c1\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc11c\ub9ac\ub97c \uc7ac\uc124\uc815\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "ignored_model": "\uc774 \ubaa8\ub378\uc5d0 \ub300\ud55c HomeKit \uc9c0\uc6d0\uc740 \ub354 \ub9ce\uc740 \uae30\ub2a5\uc744 \uc81c\uacf5\ud558\ub294 \uae30\ubcf8 \uad6c\uc131\uc694\uc18c\ub85c \uc778\ud574 \ucc28\ub2e8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "invalid_config_entry": "\uc774 \uae30\uae30\ub294 \ud398\uc5b4\ub9c1 \ud560 \uc900\ube44\uac00 \ub418\uc5c8\uc9c0\ub9cc Home Assistant \uc5d0 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \ucda9\ub3cc\ud558\ub294 \uad6c\uc131\uc694\uc18c\uac00 \uc788\uc2b5\ub2c8\ub2e4. \uba3c\uc800 \ud574\ub2f9 \uad6c\uc131\uc694\uc18c\ub97c \uc81c\uac70\ud574\uc8fc\uc138\uc694.", + "invalid_properties": "\uc7a5\uce58\uc5d0\uc11c\uc120\uc5b8\ud55c \uc798\ubabb\ub41c \uc18d\uc131\uc785\ub2c8\ub2e4.", "no_devices": "\ud398\uc5b4\ub9c1\uc774 \ud544\uc694\ud55c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" }, "error": { @@ -48,5 +49,25 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button1": "\ubc84\ud2bc 1", + "button10": "\ubc84\ud2bc 10", + "button2": "\ubc84\ud2bc 2", + "button3": "\ubc84\ud2bc 3", + "button4": "\ubc84\ud2bc 4", + "button5": "\ubc84\ud2bc 5", + "button6": "\ubc84\ud2bc 6", + "button7": "\ubc84\ud2bc 7", + "button8": "\ubc84\ud2bc 8", + "button9": "\ubc84\ud2bc 9", + "doorbell": "\ucd08\uc778\uc885" + }, + "trigger_type": { + "double_press": "\" {subtype} \"\uc744 \ub450\ubc88 \ub204\ub984", + "long_press": "\" {subtype} \"\uc744 \uae38\uac8c \ub204\ub984", + "single_press": "\"{subtype}\" \uc744 \ud55c\ubc88 \ub204\ub984" + } + }, "title": "HomeKit \ucee8\ud2b8\ub864\ub7ec" } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/ko.json b/homeassistant/components/homematicip_cloud/translations/ko.json index b85b8ac00b1..962faa68066 100644 --- a/homeassistant/components/homematicip_cloud/translations/ko.json +++ b/homeassistant/components/homematicip_cloud/translations/ko.json @@ -6,6 +6,7 @@ "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { + "invalid_pin": "\uc798\ubabb\ub41c PIN\uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud558\uc2ed\uc2dc\uc624.", "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.", diff --git a/homeassistant/components/input_boolean/translations/et.json b/homeassistant/components/input_boolean/translations/et.json index 3edfbf3cb5d..e0b2d46b168 100644 --- a/homeassistant/components/input_boolean/translations/et.json +++ b/homeassistant/components/input_boolean/translations/et.json @@ -5,5 +5,5 @@ "on": "Sees" } }, - "title": "Sisesta t\u00f5ev\u00e4\u00e4rtus" + "title": "T\u00f5ev\u00e4\u00e4rtuse abiline" } \ No newline at end of file diff --git a/homeassistant/components/input_datetime/translations/et.json b/homeassistant/components/input_datetime/translations/et.json index e72e7b10288..83acbf89262 100644 --- a/homeassistant/components/input_datetime/translations/et.json +++ b/homeassistant/components/input_datetime/translations/et.json @@ -1,3 +1,3 @@ { - "title": "Sisesta kuup\u00e4ev ja kellaaeg" + "title": "Kuup\u00e4eva ja kellaaja abiline" } \ No newline at end of file diff --git a/homeassistant/components/input_number/translations/et.json b/homeassistant/components/input_number/translations/et.json index f4182fbb3d5..a241a89ff53 100644 --- a/homeassistant/components/input_number/translations/et.json +++ b/homeassistant/components/input_number/translations/et.json @@ -1,3 +1,3 @@ { - "title": "Sisendi number" + "title": "Arvv\u00e4\u00e4rtuse abiline" } \ No newline at end of file diff --git a/homeassistant/components/input_select/translations/et.json b/homeassistant/components/input_select/translations/et.json index 22378cd35a0..5cce5f5ce0b 100644 --- a/homeassistant/components/input_select/translations/et.json +++ b/homeassistant/components/input_select/translations/et.json @@ -1,3 +1,3 @@ { - "title": "Vali sisend" + "title": "Valikmen\u00fc\u00fc abiline" } \ No newline at end of file diff --git a/homeassistant/components/input_text/translations/et.json b/homeassistant/components/input_text/translations/et.json index 047874d6328..42d7d57f419 100644 --- a/homeassistant/components/input_text/translations/et.json +++ b/homeassistant/components/input_text/translations/et.json @@ -1,3 +1,3 @@ { - "title": "Teksti sisestamine" + "title": "Tekstisisestuse abiline" } \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/ko.json b/homeassistant/components/insteon/translations/ko.json new file mode 100644 index 00000000000..37b62b95cdc --- /dev/null +++ b/homeassistant/components/insteon/translations/ko.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub428. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "step": { + "hubv1": { + "data": { + "host": "IP \uc8fc\uc18c", + "port": "\ud3ec\ud2b8" + }, + "description": "Insteon Hub \ubc84\uc804 1 (2014 \ub144 \uc774\uc804)\uc744 \uad6c\uc131\ud569\ub2c8\ub2e4.", + "title": "Insteon Hub \ubc84\uc804 1" + }, + "hubv2": { + "data": { + "host": "IP \uc8fc\uc18c", + "password": "\uc554\ud638", + "port": "\ud3ec\ud2b8", + "username": "\uc0ac\uc6a9\uc790\uba85" + }, + "description": "Insteon Hub \ubc84\uc804 2\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.", + "title": "Insteon Hub \ubc84\uc804 2" + }, + "user": { + "data": { + "modem_type": "\ubaa8\ub380 \uc720\ud615." + }, + "description": "Insteon \ubaa8\ub380 \uc720\ud615\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624.", + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/et.json b/homeassistant/components/ipp/translations/et.json new file mode 100644 index 00000000000..44e9fc8218f --- /dev/null +++ b/homeassistant/components/ipp/translations/et.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba seadistatud", + "connection_error": "\u00dchendumine eba\u00f5nnestus", + "connection_upgrade": "Printeriga \u00fchenduse loomine eba\u00f5nnestus kuna \u00fchenduse uuendamine on vajalik.", + "ipp_error": "Ilmnes IPP viga.", + "ipp_version_error": "Printer ei toeta seda IPP versiooni.", + "parse_error": "Printeri vastuse s\u00f5elumine nurjus.", + "unique_id_required": "Seadmel puudub avastamiseks vajalik kordumatu ID." + }, + "error": { + "connection_error": "\u00dchendumine eba\u00f5nnestus", + "connection_upgrade": "Printeriga \u00fchenduse loomine nurjus. Proovige uuesti kui SSL/TLS-i suvand on m\u00e4rgitud." + }, + "flow_title": "Printer: {name}", + "step": { + "user": { + "data": { + "base_path": "Printeri suhteline rada", + "host": "Host", + "port": "Port", + "ssl": "Printer toetab SSL/TLS \u00fchendust", + "verify_ssl": "Printer kasutab \u00f5iget SSL-serti" + }, + "description": "Seadistage oma printer Interneti-printimisprotokolli (IPP) kaudu, et see integreeruks Home Assistantiga.", + "title": "Linkige oma printer" + }, + "zeroconf_confirm": { + "description": "Kas soovite seadistada {name}?", + "title": "Avastatud printer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/translations/et.json b/homeassistant/components/light/translations/et.json index 4eef7a85267..59e0b904637 100644 --- a/homeassistant/components/light/translations/et.json +++ b/homeassistant/components/light/translations/et.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "Muuda {entity_name} olekut", + "turn_off": "L\u00fclita {entity_name} v\u00e4lja", + "turn_on": "L\u00fclita {entity_name} sisse" + }, + "condition_type": { + "is_off": "{entity_name} on v\u00e4lja l\u00fclitatud", + "is_on": "{entity_name} on sisse l\u00fclitatud" + }, + "trigger_type": { + "turned_off": "{entity_name} l\u00fclitus v\u00e4lja", + "turned_on": "{entity_name} l\u00fclitus sisse" + } + }, "state": { "_": { "off": "V\u00e4ljas", diff --git a/homeassistant/components/local_ip/translations/et.json b/homeassistant/components/local_ip/translations/et.json new file mode 100644 index 00000000000..70493aee468 --- /dev/null +++ b/homeassistant/components/local_ip/translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Anduri nimi" + }, + "title": "Kohalik IP-aadress" + } + } + }, + "title": "Kohalik IP-aadress" +} \ No newline at end of file diff --git a/homeassistant/components/lock/translations/et.json b/homeassistant/components/lock/translations/et.json index 67aeb442937..1ebf7e1e6dd 100644 --- a/homeassistant/components/lock/translations/et.json +++ b/homeassistant/components/lock/translations/et.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "Lukusta {entity_name}", + "open": "Ava {entity_name}", + "unlock": "Tee {entity_name} lahti" + }, "condition_type": { "is_locked": "{entity_name} on lukus", "is_unlocked": "{entity_name} on lukustamata" diff --git a/homeassistant/components/mobile_app/translations/et.json b/homeassistant/components/mobile_app/translations/et.json new file mode 100644 index 00000000000..e5c01546976 --- /dev/null +++ b/homeassistant/components/mobile_app/translations/et.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "install_app": "Home Assistantiga sidumiseks avage mobiilirakendus. \u00dchilduvate rakenduste loendi leiate jaotisest [dokumendid] ( {apps_url} )." + }, + "step": { + "confirm": { + "description": "Kas soovid seadistada mobiilirakenduse sidumist?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/sensor.et.json b/homeassistant/components/moon/translations/sensor.et.json new file mode 100644 index 00000000000..926e20adbb4 --- /dev/null +++ b/homeassistant/components/moon/translations/sensor.et.json @@ -0,0 +1,14 @@ +{ + "state": { + "moon__phase": { + "first_quarter": "Kasvav poolkuu", + "full_moon": "T\u00e4iskuu", + "last_quarter": "Kahanev poolkuu", + "new_moon": "Kuu loomine", + "waning_crescent": "Kahanev kuu", + "waning_gibbous": "Kahanev kuu", + "waxing_crescent": "Noorkuu", + "waxing_gibbous": "Kasvav kuu" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/et.json b/homeassistant/components/mqtt/translations/et.json new file mode 100644 index 00000000000..78e88cdc854 --- /dev/null +++ b/homeassistant/components/mqtt/translations/et.json @@ -0,0 +1,85 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Lubatud on ainult \u00fcks MQTT konfiguratsioon." + }, + "error": { + "cannot_connect": "Vahendajaga ei saa \u00fchendust luua." + }, + "step": { + "broker": { + "data": { + "broker": "Vahendaja", + "discovery": "Luba automaatne avastamine", + "password": "Salas\u00f5na", + "port": "Port", + "username": "Kasutajanimi" + }, + "description": "Sisestage oma MQTT vahendaja andmed." + }, + "hassio_confirm": { + "data": { + "discovery": "Luba automaatne avastamine" + }, + "description": "Kas soovite seadistada Home Assistanti \u00fchenduse loomiseks Hass.io lisandmooduli {addon} pakutava MQTT vahendajaga?", + "title": "MQTT vahendaja Hass.io pistikprogrammi kaudu" + } + } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Esimene nupp", + "button_2": "Teine nupp", + "button_3": "Kolmas nupp", + "button_4": "Neljas nupp", + "button_5": "Viies nupp", + "button_6": "Kuues nupp", + "turn_off": "L\u00fclita v\u00e4lja", + "turn_on": "L\u00fclita sisse" + }, + "trigger_type": { + "button_double_press": "\" {subtype} \" on topeltkl\u00f5psatud", + "button_long_press": "\" {subtype} \" on pikalt alla vajutatud", + "button_long_release": "\"{alamt\u00fc\u00fcp}\" vabastatati p\u00e4rast pikka vajutust", + "button_quadruple_press": "\"{alamt\u00fc\u00fcp}\" on neljakordselt kl\u00f5psatud", + "button_quintuple_press": "\"{alamt\u00fc\u00fcp}\" on viiekordselt kl\u00f5psatud", + "button_short_press": "\u201e {subtype} \u201d on vajutatud", + "button_short_release": "\" {subtype} \" vabastati", + "button_triple_press": "\"{alamt\u00fc\u00fcp}\" on kolmekordselt kl\u00f5psatud" + } + }, + "options": { + "error": { + "bad_birth": "Kehtetu loomise teavitus.", + "bad_will": "Kehtetu l\u00f5petamise teavitus.", + "cannot_connect": "Vahendajaga ei saa \u00fchendust luua." + }, + "step": { + "broker": { + "data": { + "broker": "Vahendaja", + "password": "Salas\u00f5na", + "port": "Port", + "username": "Kasutajanimi" + }, + "description": "Sisestage oma MQTT vahendaja \u00fchenduse teave." + }, + "options": { + "data": { + "birth_enable": "Luba loomisteavitus", + "birth_payload": "S\u00fcnniteate v\u00e4\u00e4rtus", + "birth_qos": "S\u00fcnniteate QoS", + "birth_retain": "S\u00fcnniteate j\u00e4\u00e4dvustamine", + "birth_topic": "S\u00fcnniteate teema", + "discovery": "Luba avastamine", + "will_enable": "Luba loomisteavitus", + "will_payload": "L\u00f5petamisteate v\u00e4\u00e4rtus", + "will_qos": "L\u00f5petamisteate QoS", + "will_retain": "L\u00f5petamisteate j\u00e4\u00e4dvustamine", + "will_topic": "L\u00f5petamisteade" + }, + "description": "Valige MQTT s\u00e4tted." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/ko.json b/homeassistant/components/netatmo/translations/ko.json index f8c052bd5f8..1793e0a765a 100644 --- a/homeassistant/components/netatmo/translations/ko.json +++ b/homeassistant/components/netatmo/translations/ko.json @@ -2,7 +2,8 @@ "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." + "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_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6b0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" }, "create_entry": { "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/notify/translations/et.json b/homeassistant/components/notify/translations/et.json index d2c08643c06..798972fe384 100644 --- a/homeassistant/components/notify/translations/et.json +++ b/homeassistant/components/notify/translations/et.json @@ -1,3 +1,3 @@ { - "title": "Teata" + "title": "Teavitused" } \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/ko.json b/homeassistant/components/nzbget/translations/ko.json new file mode 100644 index 00000000000..ea9108b9367 --- /dev/null +++ b/homeassistant/components/nzbget/translations/ko.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub428. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d" + }, + "flow_title": "NZBGet : {name}", + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "name": "\uc774\ub984", + "password": "\uc554\ud638", + "port": "\ud3ec\ud2b8", + "ssl": "NZBGet\uc740 SSL \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4.", + "username": "\uc0ac\uc6a9\uc790\uba85", + "verify_ssl": "NZBGet\uc740 \uc801\uc808\ud55c \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4." + }, + "title": "NZBGet\uc5d0 \uc5f0\uacb0" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9 (\ucd08)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/ko.json b/homeassistant/components/openweathermap/translations/ko.json new file mode 100644 index 00000000000..12e76d85506 --- /dev/null +++ b/homeassistant/components/openweathermap/translations/ko.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774\ub7ec\ud55c \uc88c\ud45c\uc5d0 \ub300\ud55c OpenWeatherMap \ud1b5\ud569\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4." + }, + "error": { + "auth": "API \ud0a4\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "connection": "OWM API\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "api_key": "OpenWeatherMap API \ud0a4", + "language": "\uc5b8\uc5b4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "mode": "\ubaa8\ub4dc", + "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uba85" + }, + "description": "OpenWeatherMap \ud1b5\ud569\uc744 \uc124\uc815\ud558\uc138\uc694. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://openweathermap.org/appid\ub85c \uc774\ub3d9\ud558\uc2ed\uc2dc\uc624.", + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "\uc5b8\uc5b4", + "mode": "\ubaa8\ub4dc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/et.json b/homeassistant/components/ozw/translations/et.json new file mode 100644 index 00000000000..a167a4edd31 --- /dev/null +++ b/homeassistant/components/ozw/translations/et.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "mqtt_required": "MQTT sidumine pole seadistatud" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/ko.json b/homeassistant/components/plugwise/translations/ko.json index df480242f6f..089e2daea82 100644 --- a/homeassistant/components/plugwise/translations/ko.json +++ b/homeassistant/components/plugwise/translations/ko.json @@ -19,5 +19,15 @@ "title": "Smile \uc5d0 \uc5f0\uacb0\ud558\uae30" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\uc2a4\uce94 \uac04\uaca9 (\ucd08)" + }, + "description": "Plugwise \uc635\uc158 \uc870\uc815" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/ko.json b/homeassistant/components/progettihwsw/translations/ko.json new file mode 100644 index 00000000000..b8b78de069c --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/ko.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_configured": "\uc7a5\uce58\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec", + "wrong_info_relay_modes": "\ub9b4\ub808\uc774 \ubaa8\ub4dc \uc120\ud0dd\uc740 Monostable \ub610\ub294 Bistable \uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4." + }, + "step": { + "relay_modes": { + "data": { + "relay_1": "\ub9b4\ub808\uc774 1", + "relay_10": "\ub9b4\ub808\uc774 10", + "relay_11": "\ub9b4\ub808\uc774 11", + "relay_12": "\ub9b4\ub808\uc774 12", + "relay_13": "\ub9b4\ub808\uc774 13", + "relay_14": "\ub9b4\ub808\uc774 14", + "relay_15": "\ub9b4\ub808\uc774 15", + "relay_16": "\ub9b4\ub808\uc774 16", + "relay_2": "\ub9b4\ub808\uc774 2", + "relay_3": "\ub9b4\ub808\uc774 3", + "relay_4": "\ub9b4\ub808\uc774 4", + "relay_5": "\ub9b4\ub808\uc774 5", + "relay_6": "\ub9b4\ub808\uc774 6", + "relay_7": "\ub9b4\ub808\uc774 7", + "relay_8": "\ub9b4\ub808\uc774 8", + "relay_9": "\ub9b4\ub808\uc774 9" + }, + "title": "\ub9b4\ub808\uc774 \uc124\uc815" + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8" + }, + "title": "\ubcf4\ub4dc \uc124\uc815" + } + } + }, + "title": "ProgettiHWSW \uc790\ub3d9\ud654" +} \ No newline at end of file diff --git a/homeassistant/components/remote/translations/ko.json b/homeassistant/components/remote/translations/ko.json index b866fd7fee5..bd055e21f5b 100644 --- a/homeassistant/components/remote/translations/ko.json +++ b/homeassistant/components/remote/translations/ko.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "{entity_name} \ud1a0\uae00", + "turn_off": "{entity_name} \ub044\uae30", + "turn_on": "{entity_name} \ucf1c\uae30" + }, + "condition_type": { + "is_off": "{entity_name} \uc774 \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name} \uc774 \ucf1c\uc838 \uc788\uc73c\uba74" + }, + "trigger_type": { + "turned_off": "{entity_name} \uaebc\uc9d0", + "turned_on": "{entity_name} \ucf1c\uc9d0" + } + }, "state": { "_": { "off": "\uaebc\uc9d0", diff --git a/homeassistant/components/risco/translations/ko.json b/homeassistant/components/risco/translations/ko.json new file mode 100644 index 00000000000..1268388acce --- /dev/null +++ b/homeassistant/components/risco/translations/ko.json @@ -0,0 +1,28 @@ +{ + "options": { + "step": { + "ha_to_risco": { + "data": { + "armed_away": "\uacbd\ube44\uc911(\uc678\ucd9c)", + "armed_custom_bypass": "\uacbd\ube44\uc911(\uc0ac\uc6a9\uc790 \uc6b0\ud68c)", + "armed_home": "\uc9d1\uc548 \uacbd\ube44\uc911", + "armed_night": "\uc57c\uac04 \uacbd\ube44\uc911" + }, + "description": "Home Assistant \uc54c\ub78c\uc744 \ud65c\uc131\ud654 \ud560 \ub54c Risco \uc54c\ub78c\uc758 \uc0c1\ud0dc\ub97c \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624.", + "title": "Home Assistant \uc0c1\ud0dc\ub97c Risco \uc0c1\ud0dc\ub85c \ub9e4\ud551" + }, + "risco_to_ha": { + "data": { + "A": "\uadf8\ub8f9 A", + "B": "\uadf8\ub8f9 B", + "C": "\uadf8\ub8f9 C", + "D": "\uadf8\ub8f9 D", + "arm": "\uacbd\ube44\uc911(\uc678\ucd9c)", + "partial_arm": "\ubd80\ubd84 \uacbd\ube44 \uc124\uc815 (\uc7ac\uc2e4)" + }, + "description": "Risco\uc5d0\uc11c\ubcf4\uace0\ud558\ub294 \ubaa8\ub4e0 \uc0c1\ud0dc\uc5d0 \ub300\ud574 Home Assistant \uc54c\ub78c\uc774 \ubcf4\uace0 \ud560 \uc0c1\ud0dc\ub97c \uc120\ud0dd\ud569\ub2c8\ub2e4.", + "title": "Risco \uc0c1\ud0dc\ub97c \ud648 \uc5b4\uc2dc\uc2a4\ud134\ud2b8 \uc0c1\ud0dc\uc5d0 \ub9e4\ud551" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/ko.json b/homeassistant/components/rpi_power/translations/ko.json new file mode 100644 index 00000000000..02271833220 --- /dev/null +++ b/homeassistant/components/rpi_power/translations/ko.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "\uc774 \uad6c\uc131 \uc694\uc18c\uc5d0 \ud544\uc694\ud55c \uc2dc\uc2a4\ud15c \ud074\ub798\uc2a4\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucee4\ub110\uc774 \ucd5c\uc2e0\uc774\uace0 \ud558\ub4dc\uc6e8\uc5b4\uac00 \uc9c0\uc6d0\ub418\ub294\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624.", + "single_instance_allowed": "\uc774\ubbf8 \uc124\uc815\ub418\uc5b4 \uc788\uc74c. \ud558\ub098\uc758 \uc124\uc815\ub9cc \uac00\ub2a5\ud568." + }, + "step": { + "confirm": { + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + }, + "title": "\ub77c\uc988\ubca0\ub9ac\ud30c\uc774 \uc804\uc6d0 \uacf5\uae09 \uc7a5\uce58 \uac80\uc0ac\uae30" +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.et.json b/homeassistant/components/season/translations/sensor.et.json new file mode 100644 index 00000000000..eb9953a73ce --- /dev/null +++ b/homeassistant/components/season/translations/sensor.et.json @@ -0,0 +1,16 @@ +{ + "state": { + "season__season": { + "autumn": "S\u00fcgis", + "spring": "Kevad", + "summer": "Suvi", + "winter": "Talv" + }, + "season__season__": { + "autumn": "S\u00fcgis", + "spring": "Kevad", + "summer": "Suvi", + "winter": "Talv" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/et.json b/homeassistant/components/sensor/translations/et.json index 545abddd1f9..d45ba964d70 100644 --- a/homeassistant/components/sensor/translations/et.json +++ b/homeassistant/components/sensor/translations/et.json @@ -17,7 +17,10 @@ "illuminance": "{entity_name} valgustustugevus muutub", "power": "{entity_name} energiare\u017eiimi muutub", "pressure": "{entity_name} r\u00f5hk muutub", - "signal_strength": "{entity_name} signaalitugevus muutub" + "signal_strength": "{entity_name} signaalitugevus muutub", + "temperature": "{entity_name} temperatuur muutub", + "timestamp": "{entity_name} aeg muutub", + "value": "{entity_name} v\u00e4\u00e4rtus muutub" } }, "state": { diff --git a/homeassistant/components/sharkiq/translations/ko.json b/homeassistant/components/sharkiq/translations/ko.json new file mode 100644 index 00000000000..d7e196c09fb --- /dev/null +++ b/homeassistant/components/sharkiq/translations/ko.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured_account": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.", + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "reauth_successful": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc131\uacf5\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + }, + "step": { + "reauth": { + "data": { + "password": "\uc554\ud638", + "username": "\uc0ac\uc6a9\uc790\uba85" + } + }, + "user": { + "data": { + "password": "\uc554\ud638", + "username": "\uc0ac\uc6a9\uc790\uba85" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/ko.json b/homeassistant/components/shelly/translations/ko.json new file mode 100644 index 00000000000..3bf8db7d50e --- /dev/null +++ b/homeassistant/components/shelly/translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d" + }, + "step": { + "credentials": { + "data": { + "password": "\uc554\ud638", + "username": "\uc0ac\uc6a9\uc790\uba85" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/ko.json b/homeassistant/components/smappee/translations/ko.json index 05557a6046d..129cc0814bf 100644 --- a/homeassistant/components/smappee/translations/ko.json +++ b/homeassistant/components/smappee/translations/ko.json @@ -2,7 +2,8 @@ "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." + "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_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6b0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/somfy/translations/ko.json b/homeassistant/components/somfy/translations/ko.json index 9748da483bf..157640c1fa7 100644 --- a/homeassistant/components/somfy/translations/ko.json +++ b/homeassistant/components/somfy/translations/ko.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "\ud558\ub098\uc758 Somfy \uacc4\uc815\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "missing_configuration": "Somfy \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + "missing_configuration": "Somfy \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_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6b0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" }, "create_entry": { "default": "Somfy \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." diff --git a/homeassistant/components/spotify/translations/et.json b/homeassistant/components/spotify/translations/et.json new file mode 100644 index 00000000000..059b892f009 --- /dev/null +++ b/homeassistant/components/spotify/translations/et.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_setup": "Saate konfigureerida ainult \u00fche Spotify konto.", + "authorize_url_timeout": "Kinnituse URLi ajal\u00f5pp", + "missing_configuration": "Spotify sidumine pole seadistatud. Palun j\u00e4rgige dokumentatsiooni." + }, + "create_entry": { + "default": "Edukalt Spotifyga autenditud." + }, + "step": { + "pick_implementation": { + "title": "Valige autentimismeetod" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/ko.json b/homeassistant/components/spotify/translations/ko.json index deb55479c1e..2b9ebfe8bf6 100644 --- a/homeassistant/components/spotify/translations/ko.json +++ b/homeassistant/components/spotify/translations/ko.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "\ud558\ub098\uc758 Spotify \uacc4\uc815\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "missing_configuration": "Spotify \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + "missing_configuration": "Spotify \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_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6b0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" }, "create_entry": { "default": "Spotify \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." @@ -11,6 +12,10 @@ "step": { "pick_implementation": { "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" + }, + "reauth_confirm": { + "description": "Spotify \ud1b5\ud569\uc740 \uacc4\uc815 {account} \ub300\ud574 Spotify\ub85c \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c\ud569\ub2c8\ub2e4.", + "title": "Spotify\ub85c \uc7ac \uc778\uc99d" } } } diff --git a/homeassistant/components/starline/translations/et.json b/homeassistant/components/starline/translations/et.json new file mode 100644 index 00000000000..7e3c4103ccf --- /dev/null +++ b/homeassistant/components/starline/translations/et.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "auth_mfa": { + "title": "Kaheastmeline autoriseerimine" + }, + "auth_user": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/translations/et.json b/homeassistant/components/switch/translations/et.json index d992df0421f..404f9c961bf 100644 --- a/homeassistant/components/switch/translations/et.json +++ b/homeassistant/components/switch/translations/et.json @@ -1,4 +1,10 @@ { + "device_automation": { + "condition_type": { + "is_off": "{entity_name} on v\u00e4lja l\u00fclitatud", + "is_on": "{entity_name} on sisse l\u00fclitatud" + } + }, "state": { "_": { "off": "V\u00e4ljas", diff --git a/homeassistant/components/synology_dsm/translations/ko.json b/homeassistant/components/synology_dsm/translations/ko.json index 81b7d5f1435..6c0dc98b4ae 100644 --- a/homeassistant/components/synology_dsm/translations/ko.json +++ b/homeassistant/components/synology_dsm/translations/ko.json @@ -44,7 +44,8 @@ "step": { "init": { "data": { - "scan_interval": "\uc2a4\uce94 \uac04\uaca9(\ubd84)" + "scan_interval": "\uc2a4\uce94 \uac04\uaca9(\ubd84)", + "timeout": "\uc81c\ud55c \uc2dc\uac04 (\ucd08)" } } } diff --git a/homeassistant/components/toon/translations/ko.json b/homeassistant/components/toon/translations/ko.json index bebd8bb912e..e0903a8087c 100644 --- a/homeassistant/components/toon/translations/ko.json +++ b/homeassistant/components/toon/translations/ko.json @@ -5,7 +5,8 @@ "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.", "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_agreements": "\uc774 \uacc4\uc815\uc5d0\ub294 Toon \ub514\uc2a4\ud50c\ub808\uc774\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.", + "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6b0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" }, "step": { "agreement": { diff --git a/homeassistant/components/tuya/translations/et.json b/homeassistant/components/tuya/translations/et.json new file mode 100644 index 00000000000..0086fd641f8 --- /dev/null +++ b/homeassistant/components/tuya/translations/et.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "auth_failed": "Viga tuvastamisel", + "conn_error": "\u00dchendamine eba\u00f5nnestus", + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks sidumine." + }, + "error": { + "auth_failed": "Vigane tuvastamine" + }, + "flow_title": "Tuya seaded", + "step": { + "user": { + "data": { + "country_code": "Teie konto riigikood (nt 1 USA v\u00f5i 372 Eesti)", + "password": "Salas\u00f5na", + "platform": "\u00c4pp kus teie konto registreeriti", + "username": "Kasutajanimi" + }, + "description": "Sisestage oma Tuya konto andmed." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/ko.json b/homeassistant/components/unifi/translations/ko.json index a3d2c8f3b69..94160829bad 100644 --- a/homeassistant/components/unifi/translations/ko.json +++ b/homeassistant/components/unifi/translations/ko.json @@ -54,7 +54,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ub300\uc5ed\ud3ed \uc0ac\uc6a9\ub7c9 \uc13c\uc11c" + "allow_bandwidth_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ub300\uc5ed\ud3ed \uc0ac\uc6a9\ub7c9 \uc13c\uc11c", + "allow_uptime_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8\ub97c \uc704\ud55c \uac00\ub3d9 \uc2dc\uac04 \uc13c\uc11c" }, "description": "\ud1b5\uacc4 \uc13c\uc11c \uad6c\uc131", "title": "UniFi \uc635\uc158 3/3" diff --git a/homeassistant/components/upnp/translations/et.json b/homeassistant/components/upnp/translations/et.json new file mode 100644 index 00000000000..76145d6e6e1 --- /dev/null +++ b/homeassistant/components/upnp/translations/et.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP / IGD on juba seadistatud", + "incomplete_discovery": "Mittet\u00e4ielik avastamine", + "no_devices_discovered": "\u00dchtegi UPnP / IGD-d ei avastatud", + "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi UPnP / IGD-seadet." + }, + "error": { + "one": "\u00fcks", + "other": "Teine" + }, + "flow_title": "UPnP / IGD: {name}", + "step": { + "init": { + "one": "\u00dcks", + "other": "Teine" + }, + "ssdp_confirm": { + "description": "Kas soovite UPnP / IGD seadme seadistada?" + }, + "user": { + "data": { + "scan_interval": "P\u00e4ringute intervall (sekundites, v\u00e4hemalt 30)", + "usn": "Seade" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/ko.json b/homeassistant/components/withings/translations/ko.json index 74dacdd2cac..2104b2a9570 100644 --- a/homeassistant/components/withings/translations/ko.json +++ b/homeassistant/components/withings/translations/ko.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "\ud504\ub85c\ud544\uc5d0 \ub300\ud55c \uad6c\uc131\uc774 \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "missing_configuration": "Withings \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + "missing_configuration": "Withings \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", + "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6b0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" }, "create_entry": { "default": "Withings \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." diff --git a/homeassistant/components/wled/translations/et.json b/homeassistant/components/wled/translations/et.json new file mode 100644 index 00000000000..794bd87e42f --- /dev/null +++ b/homeassistant/components/wled/translations/et.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "See WLED seade on juba konfigureeritud.", + "connection_error": "WLED-seadmega \u00fchenduse loomine nurjus." + }, + "error": { + "connection_error": "WLED-seadmega \u00fchenduse loomine nurjus." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Host" + }, + "description": "Seadista WLED-i sidumine Home Assistantiga." + }, + "zeroconf_confirm": { + "description": "Kas soovite lisada WLED {nimi} Home Assistanti?", + "title": "Leitud WLED seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/ko.json b/homeassistant/components/yeelight/translations/ko.json new file mode 100644 index 00000000000..c04006e2c8f --- /dev/null +++ b/homeassistant/components/yeelight/translations/ko.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\uc7a5\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.", + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c \uc0c1\uc5d0 \ubc1c\uacac\ub41c \uc7a5\uce58\uac00 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328" + }, + "step": { + "pick_device": { + "data": { + "device": "\uc7a5\uce58" + } + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "ip_address": "IP \uc8fc\uc18c" + }, + "description": "\ud638\uc2a4\ud2b8\ub97c \ube44\uc6cc\ub450\uba74 \uc7a5\uce58\ub97c \ucc3e\ub294 \ub370 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "\ubaa8\ub378(\uc120\ud0dd \uc0ac\ud56d)", + "nightlight_switch": "\uc57c\uac04 \uc870\uba85 \uc2a4\uc704\uce58 \uc0ac\uc6a9", + "save_on_change": "\ubcc0\uacbd\uc2dc \uc0c1\ud0dc \uc800\uc7a5", + "transition": "\uc804\ud658 \uc2dc\uac04(ms)", + "use_music_mode": "\uc74c\uc545 \ubaa8\ub4dc \ud65c\uc131\ud654" + }, + "description": "\ubaa8\ub378\uc744 \ube44\uc6cc \ub450\uba74 \uc790\ub3d9\uc73c\ub85c \uac80\uc0c9\ub429\ub2c8\ub2e4." + } + } + }, + "title": "\uc774\ub77c\uc774\ud2b8" +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/ko.json b/homeassistant/components/zoneminder/translations/ko.json new file mode 100644 index 00000000000..3625d6e402e --- /dev/null +++ b/homeassistant/components/zoneminder/translations/ko.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "auth_fail": "\uc0ac\uc6a9\uc790\uba85\uacfc \uc554\ud638\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "connection_error": "ZoneMinder \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4." + }, + "create_entry": { + "default": "ZoneMinder \uc11c\ubc84\uac00 \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "auth_fail": "\uc0ac\uc6a9\uc790\uba85\uacfc \uc554\ud638\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "connection_error": "ZoneMinder \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4." + }, + "flow_title": "ZoneMinder", + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8 \ubc0f \ud3ec\ud2b8(\uc608: 10.10.0.4:8010)", + "password": "\uc554\ud638", + "path": "ZMS \uacbd\ub85c", + "path_zms": "ZMS \uacbd\ub85c", + "ssl": "ZoneMinder \uc5f0\uacb0\uc5d0 SSL \uc0ac\uc6a9", + "username": "\uc0ac\uc6a9\uc790\uba85", + "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" + }, + "title": "ZoneMinder \uc11c\ubc84\ub97c \ucd94\uac00\ud558\uc138\uc694." + } + } + } +} \ No newline at end of file From 8d3e4b6b3f7e283e5a38d568153df7b2dfaa8bcf Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 21 Sep 2020 09:26:24 +0800 Subject: [PATCH 264/514] Ignore packets with missing dts in peek_first_pts (#40299) --- homeassistant/components/stream/worker.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index b76896b815a..40231d87a53 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -113,7 +113,11 @@ def _stream_worker_internal(hass, stream, quit_event): # Get to first video keyframe while first_packet[video_stream] is None: packet = next(container.demux()) - if packet.stream == video_stream and packet.is_keyframe: + if ( + packet.stream == video_stream + and packet.is_keyframe + and packet.dts is not None + ): first_packet[video_stream] = packet initial_packets.append(packet) # Get first_pts from subsequent frame to first keyframe @@ -121,6 +125,8 @@ def _stream_worker_internal(hass, stream, quit_event): [pts is None for pts in {**first_packet, **first_pts}.values()] ) and (len(initial_packets) < PACKETS_TO_WAIT_FOR_AUDIO): packet = next(container.demux((video_stream, audio_stream))) + if packet.dts is None: + continue # Discard packets with no dts if ( first_packet[packet.stream] is None ): # actually video already found above so only for audio From 37e51aa166e2f27980c9c1c7a3118cae9b60053c Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 20 Sep 2020 21:15:48 -0500 Subject: [PATCH 265/514] Add reauth source constant for config entries (#40352) --- homeassistant/config_entries.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 347ca294d34..139e2066d17 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -39,6 +39,9 @@ SOURCE_IGNORE = "ignore" # been removed and unloaded. SOURCE_UNIGNORE = "unignore" +# This is used to signal that re-authentication is required by the user. +SOURCE_REAUTH = "reauth" + HANDLERS = Registry() STORAGE_KEY = "core.config_entries" From a0df6ccb81303a02f20b75a938bc0f599233652c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 21 Sep 2020 10:28:24 +0200 Subject: [PATCH 266/514] Add supervisor install add-on helper (#40138) --- homeassistant/components/hassio/__init__.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 69c53225d49..49ea884418f 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -127,20 +127,27 @@ MAP_SERVICE_API = { @bind_hass -async def async_get_addon_info(hass: HomeAssistantType, addon_id: str) -> dict: +async def async_get_addon_info(hass: HomeAssistantType, slug: str) -> dict: """Return add-on info. - The addon_id is a snakecased concatenation of the 'repository' value - found in the add-on info and the 'slug' value found in the add-on config.json. - In the add-on info the addon_id is called 'slug'. - The caller of the function should handle HassioAPIError. """ hassio = hass.data[DOMAIN] - result = await hassio.get_addon_info(addon_id) + result = await hassio.get_addon_info(slug) return result["data"] +@bind_hass +async def async_install_addon(hass: HomeAssistantType, slug: str) -> None: + """Install add-on. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/install" + await hassio.send_command(command) + + @callback @bind_hass def get_info(hass): From 7cebfa75fca09a56dbd2ac4e6405077a3ca5a3a0 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Mon, 21 Sep 2020 10:49:16 +0200 Subject: [PATCH 267/514] Upgrade youtube_dl to version 2020.09.20 (#40395) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index afb0838de37..1dbb642aee9 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -2,7 +2,7 @@ "domain": "media_extractor", "name": "Media Extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", - "requirements": ["youtube_dl==2020.09.14"], + "requirements": ["youtube_dl==2020.09.20"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/requirements_all.txt b/requirements_all.txt index 6146c531bf2..c3f431ff8fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2287,7 +2287,7 @@ yeelight==0.5.3 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2020.09.14 +youtube_dl==2020.09.20 # homeassistant.components.zengge zengge==0.2 From 4aa9b727397b0c63f2ae89e3422f59774a71e176 Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Mon, 21 Sep 2020 13:10:39 +0100 Subject: [PATCH 268/514] Remove myself as Luci code owner (#40398) --- CODEOWNERS | 2 +- homeassistant/components/luci/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 5b7f0c1ae3f..2913dd68953 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -238,7 +238,7 @@ 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/luci/* @mzdrale homeassistant/components/luftdaten/* @fabaff homeassistant/components/lupusec/* @majuss homeassistant/components/lutron/* @JonGilmore diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index d18cbae5103..3b51aab6e4a 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -3,5 +3,5 @@ "name": "OpenWRT (luci)", "documentation": "https://www.home-assistant.io/integrations/luci", "requirements": ["openwrt-luci-rpc==1.1.6"], - "codeowners": ["@fbradyirl", "@mzdrale"] + "codeowners": ["@mzdrale"] } From 2ef3dfb6738e8dd96b6e89b8b11fa6197de82ef0 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 21 Sep 2020 16:26:09 +0200 Subject: [PATCH 269/514] Fix supervisor get addon info (#40412) --- homeassistant/components/hassio/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 49ea884418f..777d5938b1b 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -133,8 +133,7 @@ async def async_get_addon_info(hass: HomeAssistantType, slug: str) -> dict: The caller of the function should handle HassioAPIError. """ hassio = hass.data[DOMAIN] - result = await hassio.get_addon_info(slug) - return result["data"] + return await hassio.get_addon_info(slug) @bind_hass From 8b9c757fdc7aea55891925e5ea853be09ce7c2b1 Mon Sep 17 00:00:00 2001 From: Julien Tant Date: Mon, 21 Sep 2020 08:41:30 -0700 Subject: [PATCH 270/514] Add zodiac integration (#38935) * add zodiac sign integration * add tests & refacto * Apply suggestions from code review Co-authored-by: Chris Talkington * fix indentation from suggested correction, fix quality scale and remove useless functions * fix code formatting * Create const.py * Update sensor.py * Update const.py * Update test_sensor.py * Update test_sensor.py * Update test_sensor.py * Update test_sensor.py * Update test_sensor.py * Update sensor.py * Update test_sensor.py * Update __init__.py * Update sensor.py * Update test_sensor.py * Update sensor.py * Update __init__.py * Update test_sensor.py * Update __init__.py * Fix zodiac time patch * Delete sensor.fr.json * Update sensor.py * Delete sensor.en.json * Update test_sensor.py * Apply suggestions from code review Co-authored-by: Chris Talkington Co-authored-by: Martin Hjelmare --- CODEOWNERS | 1 + homeassistant/components/zodiac/__init__.py | 19 ++ homeassistant/components/zodiac/const.py | 31 +++ homeassistant/components/zodiac/manifest.json | 7 + homeassistant/components/zodiac/sensor.py | 220 ++++++++++++++++++ .../components/zodiac/strings.sensor.json | 18 ++ tests/components/zodiac/__init__.py | 1 + tests/components/zodiac/test_sensor.py | 50 ++++ 8 files changed, 347 insertions(+) create mode 100644 homeassistant/components/zodiac/__init__.py create mode 100644 homeassistant/components/zodiac/const.py create mode 100644 homeassistant/components/zodiac/manifest.json create mode 100644 homeassistant/components/zodiac/sensor.py create mode 100644 homeassistant/components/zodiac/strings.sensor.json create mode 100644 tests/components/zodiac/__init__.py create mode 100644 tests/components/zodiac/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 2913dd68953..3fa8a7a366d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -505,6 +505,7 @@ homeassistant/components/yi/* @bachya homeassistant/components/zeroconf/* @Kane610 homeassistant/components/zerproc/* @emlove homeassistant/components/zha/* @dmulcahey @adminiuga +homeassistant/components/zodiac/* @JulienTant homeassistant/components/zone/* @home-assistant/core homeassistant/components/zoneminder/* @rohankapoorcom @vangorra homeassistant/components/zwave/* @home-assistant/z-wave diff --git a/homeassistant/components/zodiac/__init__.py b/homeassistant/components/zodiac/__init__.py new file mode 100644 index 00000000000..d00cc560f22 --- /dev/null +++ b/homeassistant/components/zodiac/__init__.py @@ -0,0 +1,19 @@ +"""The zodiac component.""" +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.discovery import async_load_platform + +from .const import DOMAIN + +CONFIG_SCHEMA = vol.Schema( + {vol.Optional(DOMAIN): {}}, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the zodiac component.""" + hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) + + return True diff --git a/homeassistant/components/zodiac/const.py b/homeassistant/components/zodiac/const.py new file mode 100644 index 00000000000..c3e7f13d5e3 --- /dev/null +++ b/homeassistant/components/zodiac/const.py @@ -0,0 +1,31 @@ +"""Constants for Zodiac.""" +DOMAIN = "zodiac" + +# Signs +SIGN_ARIES = "aries" +SIGN_TAURUS = "taurus" +SIGN_GEMINI = "gemini" +SIGN_CANCER = "cancer" +SIGN_LEO = "leo" +SIGN_VIRGO = "virgo" +SIGN_LIBRA = "libra" +SIGN_SCORPIO = "scorpio" +SIGN_SAGITTARIUS = "sagittarius" +SIGN_CAPRICORN = "capricorn" +SIGN_AQUARIUS = "aquarius" +SIGN_PISCES = "pisces" + +# Elements +ELEMENT_FIRE = "fire" +ELEMENT_AIR = "air" +ELEMENT_EARTH = "earth" +ELEMENT_WATER = "water" + +# Modality +MODALITY_CARDINAL = "cardinal" +MODALITY_FIXED = "fixed" +MODALITY_MUTABLE = "mutable" + +# Attributes +ATTR_ELEMENT = "element" +ATTR_MODALITY = "modality" diff --git a/homeassistant/components/zodiac/manifest.json b/homeassistant/components/zodiac/manifest.json new file mode 100644 index 00000000000..9d38c2cff39 --- /dev/null +++ b/homeassistant/components/zodiac/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "zodiac", + "name": "Zodiac", + "documentation": "https://www.home-assistant.io/integrations/zodiac", + "codeowners": ["@JulienTant"], + "quality_scale": "silver" +} diff --git a/homeassistant/components/zodiac/sensor.py b/homeassistant/components/zodiac/sensor.py new file mode 100644 index 00000000000..06bd52f6bf5 --- /dev/null +++ b/homeassistant/components/zodiac/sensor.py @@ -0,0 +1,220 @@ +"""Support for tracking the zodiac sign.""" +import logging + +from homeassistant.helpers.entity import Entity +from homeassistant.util.dt import as_local, utcnow + +from .const import ( + ATTR_ELEMENT, + ATTR_MODALITY, + DOMAIN, + ELEMENT_AIR, + ELEMENT_EARTH, + ELEMENT_FIRE, + ELEMENT_WATER, + MODALITY_CARDINAL, + MODALITY_FIXED, + MODALITY_MUTABLE, + SIGN_AQUARIUS, + SIGN_ARIES, + SIGN_CANCER, + SIGN_CAPRICORN, + SIGN_GEMINI, + SIGN_LEO, + SIGN_LIBRA, + SIGN_PISCES, + SIGN_SAGITTARIUS, + SIGN_SCORPIO, + SIGN_TAURUS, + SIGN_VIRGO, +) + +_LOGGER = logging.getLogger(__name__) + +ZODIAC_BY_DATE = ( + ( + (21, 3), + (20, 4), + SIGN_ARIES, + { + ATTR_ELEMENT: ELEMENT_FIRE, + ATTR_MODALITY: MODALITY_CARDINAL, + }, + ), + ( + (21, 4), + (20, 5), + SIGN_TAURUS, + { + ATTR_ELEMENT: ELEMENT_EARTH, + ATTR_MODALITY: MODALITY_FIXED, + }, + ), + ( + (21, 5), + (21, 6), + SIGN_GEMINI, + { + ATTR_ELEMENT: ELEMENT_AIR, + ATTR_MODALITY: MODALITY_MUTABLE, + }, + ), + ( + (22, 6), + (22, 7), + SIGN_CANCER, + { + ATTR_ELEMENT: ELEMENT_WATER, + ATTR_MODALITY: MODALITY_CARDINAL, + }, + ), + ( + (23, 7), + (22, 8), + SIGN_LEO, + { + ATTR_ELEMENT: ELEMENT_FIRE, + ATTR_MODALITY: MODALITY_FIXED, + }, + ), + ( + (23, 8), + (21, 9), + SIGN_VIRGO, + { + ATTR_ELEMENT: ELEMENT_EARTH, + ATTR_MODALITY: MODALITY_MUTABLE, + }, + ), + ( + (22, 9), + (22, 10), + SIGN_LIBRA, + { + ATTR_ELEMENT: ELEMENT_AIR, + ATTR_MODALITY: MODALITY_CARDINAL, + }, + ), + ( + (23, 10), + (22, 11), + SIGN_SCORPIO, + { + ATTR_ELEMENT: ELEMENT_WATER, + ATTR_MODALITY: MODALITY_FIXED, + }, + ), + ( + (23, 11), + (21, 12), + SIGN_SAGITTARIUS, + { + ATTR_ELEMENT: ELEMENT_FIRE, + ATTR_MODALITY: MODALITY_MUTABLE, + }, + ), + ( + (22, 12), + (20, 1), + SIGN_CAPRICORN, + { + ATTR_ELEMENT: ELEMENT_EARTH, + ATTR_MODALITY: MODALITY_CARDINAL, + }, + ), + ( + (21, 1), + (19, 2), + SIGN_AQUARIUS, + { + ATTR_ELEMENT: ELEMENT_AIR, + ATTR_MODALITY: MODALITY_FIXED, + }, + ), + ( + (20, 2), + (20, 3), + SIGN_PISCES, + { + ATTR_ELEMENT: ELEMENT_WATER, + ATTR_MODALITY: MODALITY_MUTABLE, + }, + ), +) + +ZODIAC_ICONS = { + SIGN_ARIES: "mdi:zodiac-aries", + SIGN_TAURUS: "mdi:zodiac-taurus", + SIGN_GEMINI: "mdi:zodiac-gemini", + SIGN_CANCER: "mdi:zodiac-cancer", + SIGN_LEO: "mdi:zodiac-leo", + SIGN_VIRGO: "mdi:zodiac-virgo", + SIGN_LIBRA: "mdi:zodiac-libra", + SIGN_SCORPIO: "mdi:zodiac-scorpio", + SIGN_SAGITTARIUS: "mdi:zodiac-sagittarius", + SIGN_CAPRICORN: "mdi:zodiac-capricorn", + SIGN_AQUARIUS: "mdi:zodiac-aquarius", + SIGN_PISCES: "mdi:zodiac-pisces", +} + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Zodiac sensor platform.""" + if discovery_info is None: + return + + async_add_entities([ZodiacSensor()], True) + + +class ZodiacSensor(Entity): + """Representation of a Zodiac sensor.""" + + def __init__(self): + """Initialize the zodiac sensor.""" + self._attrs = None + self._state = None + + @property + def unique_id(self): + """Return a unique ID.""" + return DOMAIN + + @property + def name(self): + """Return the name of the entity.""" + return "Zodiac" + + @property + def device_class(self): + """Return the device class of the entity.""" + return "zodiac__sign" + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ZODIAC_ICONS.get(self._state) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attrs + + async def async_update(self): + """Get the time and updates the state.""" + today = as_local(utcnow()).date() + + month = int(today.month) + day = int(today.day) + + for sign in ZODIAC_BY_DATE: + if (month == sign[0][1] and day >= sign[0][0]) or ( + month == sign[1][1] and day <= sign[1][0] + ): + self._state = sign[2] + self._attrs = sign[3] + break diff --git a/homeassistant/components/zodiac/strings.sensor.json b/homeassistant/components/zodiac/strings.sensor.json new file mode 100644 index 00000000000..e33465967e3 --- /dev/null +++ b/homeassistant/components/zodiac/strings.sensor.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aries": "Aries", + "taurus": "Taurus", + "gemini": "Gemini", + "cancer": "Cancer", + "leo": "Leo", + "virgo": "Virgo", + "libra": "Libra", + "scorpio": "Scorpio", + "sagittarius": "Sagittarius", + "capricorn": "Capricorn", + "aquarius": "Aquarius", + "pisces": "Pisces" + } + } +} \ No newline at end of file diff --git a/tests/components/zodiac/__init__.py b/tests/components/zodiac/__init__.py new file mode 100644 index 00000000000..e9bae20c442 --- /dev/null +++ b/tests/components/zodiac/__init__.py @@ -0,0 +1 @@ +"""Tests for the zodiac component.""" diff --git a/tests/components/zodiac/test_sensor.py b/tests/components/zodiac/test_sensor.py new file mode 100644 index 00000000000..b08352dd2c0 --- /dev/null +++ b/tests/components/zodiac/test_sensor.py @@ -0,0 +1,50 @@ +"""The test for the zodiac sensor platform.""" +from datetime import datetime + +import pytest + +from homeassistant.components.zodiac.const import ( + ATTR_ELEMENT, + ATTR_MODALITY, + DOMAIN, + ELEMENT_EARTH, + ELEMENT_FIRE, + ELEMENT_WATER, + MODALITY_CARDINAL, + MODALITY_FIXED, + SIGN_ARIES, + SIGN_SCORPIO, + SIGN_TAURUS, +) +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.async_mock import patch + +DAY1 = datetime(2020, 11, 15, tzinfo=dt_util.UTC) +DAY2 = datetime(2020, 4, 20, tzinfo=dt_util.UTC) +DAY3 = datetime(2020, 4, 21, tzinfo=dt_util.UTC) + + +@pytest.mark.parametrize( + "now,sign,element,modality", + [ + (DAY1, SIGN_SCORPIO, ELEMENT_WATER, MODALITY_FIXED), + (DAY2, SIGN_ARIES, ELEMENT_FIRE, MODALITY_CARDINAL), + (DAY3, SIGN_TAURUS, ELEMENT_EARTH, MODALITY_FIXED), + ], +) +async def test_zodiac_day(hass, now, sign, element, modality): + """Test the zodiac sensor.""" + config = {DOMAIN: {}} + + with patch("homeassistant.components.zodiac.sensor.utcnow", return_value=now): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.zodiac") + assert state + assert state.state == sign + assert state.attributes + assert state.attributes[ATTR_ELEMENT] == element + assert state.attributes[ATTR_MODALITY] == modality From 2a4d7dc561b8e1522b660fea84590e073ff9cf50 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 21 Sep 2020 17:43:35 +0200 Subject: [PATCH 271/514] Update voluptuous to 0.12.0 (#40401) --- 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 0f88f88469b..0a73a23489f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -27,7 +27,7 @@ requests==2.24.0 ruamel.yaml==0.15.100 sqlalchemy==1.3.19 voluptuous-serialize==2.4.0 -voluptuous==0.11.7 +voluptuous==0.12.0 yarl==1.4.2 zeroconf==0.28.5 diff --git a/requirements.txt b/requirements.txt index baa48241a06..91f09a54390 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,6 @@ pytz>=2020.1 pyyaml==5.3.1 requests==2.24.0 ruamel.yaml==0.15.100 -voluptuous==0.11.7 +voluptuous==0.12.0 voluptuous-serialize==2.4.0 yarl==1.4.2 diff --git a/setup.py b/setup.py index 0bbdf9f05a8..022f5547655 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ REQUIRES = [ "pyyaml==5.3.1", "requests==2.24.0", "ruamel.yaml==0.15.100", - "voluptuous==0.11.7", + "voluptuous==0.12.0", "voluptuous-serialize==2.4.0", "yarl==1.4.2", ] From c13fbf795d34d04f58c6737067af32a851da66d1 Mon Sep 17 00:00:00 2001 From: Robin Wohlers-Reichel Date: Tue, 22 Sep 2020 01:50:03 +1000 Subject: [PATCH 272/514] Update Solax Library to 0.2.4 (#40330) --- homeassistant/components/solax/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index 5d8590389d8..bf2d3d72cc5 100644 --- a/homeassistant/components/solax/manifest.json +++ b/homeassistant/components/solax/manifest.json @@ -2,6 +2,6 @@ "domain": "solax", "name": "SolaX Power", "documentation": "https://www.home-assistant.io/integrations/solax", - "requirements": ["solax==0.2.3"], + "requirements": ["solax==0.2.4"], "codeowners": ["@squishykid"] } diff --git a/requirements_all.txt b/requirements_all.txt index c3f431ff8fb..987d085dd79 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2035,7 +2035,7 @@ solaredge-local==0.2.0 solaredge==0.0.2 # homeassistant.components.solax -solax==0.2.3 +solax==0.2.4 # homeassistant.components.honeywell somecomfort==0.5.2 From 3d6434be75f920d4274b7d98e70556e4af281c7c Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Mon, 21 Sep 2020 18:08:35 +0200 Subject: [PATCH 273/514] Use centralized KnxEntity for all KNX platforms (#40381) --- homeassistant/components/knx/__init__.py | 14 +-- homeassistant/components/knx/binary_sensor.py | 53 ++------- homeassistant/components/knx/climate.py | 84 +++++--------- homeassistant/components/knx/const.py | 1 - homeassistant/components/knx/cover.py | 80 +++++-------- homeassistant/components/knx/knx_entity.py | 51 +++++++++ homeassistant/components/knx/light.py | 106 +++++++----------- homeassistant/components/knx/notify.py | 4 +- homeassistant/components/knx/scene.py | 18 ++- homeassistant/components/knx/sensor.py | 56 ++------- homeassistant/components/knx/switch.py | 49 ++------ homeassistant/components/knx/weather.py | 28 +++-- 12 files changed, 206 insertions(+), 338 deletions(-) create mode 100644 homeassistant/components/knx/knx_entity.py diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index f9f78f195bb..501ff856333 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -29,7 +29,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, SupportedPlatforms +from .const import DOMAIN, SupportedPlatforms from .factory import create_knx_device from .schema import ( BinarySensorSchema, @@ -139,9 +139,9 @@ SERVICE_KNX_SEND_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the KNX component.""" try: - hass.data[DATA_KNX] = KNXModule(hass, config) - hass.data[DATA_KNX].async_create_exposures() - await hass.data[DATA_KNX].start() + hass.data[DOMAIN] = KNXModule(hass, config) + hass.data[DOMAIN].async_create_exposures() + await hass.data[DOMAIN].start() except XKNXException as ex: _LOGGER.warning("Could not connect to KNX interface: %s", ex) hass.components.persistent_notification.async_create( @@ -151,7 +151,7 @@ async def async_setup(hass, config): for platform in SupportedPlatforms: if platform.value in config[DOMAIN]: for device_config in config[DOMAIN][platform.value]: - create_knx_device(platform, hass.data[DATA_KNX].xknx, device_config) + create_knx_device(platform, hass.data[DOMAIN].xknx, device_config) # 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: @@ -159,7 +159,7 @@ async def async_setup(hass, config): discovery.async_load_platform(hass, platform.value, DOMAIN, {}, config) ) - if not hass.data[DATA_KNX].xknx.devices: + if not hass.data[DOMAIN].xknx.devices: _LOGGER.warning( "No KNX devices are configured. Please read " "https://www.home-assistant.io/blog/2020/09/17/release-115/#breaking-changes" @@ -168,7 +168,7 @@ async def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_KNX_SEND, - hass.data[DATA_KNX].service_send_to_knx_bus, + hass.data[DOMAIN].service_send_to_knx_bus, schema=SERVICE_KNX_SEND_SCHEMA, ) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 24fbe472ae3..a62e95f1def 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -3,72 +3,41 @@ from typing import Any, Dict, Optional from xknx.devices import BinarySensor as XknxBinarySensor -from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.core import callback +from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity -from .const import ATTR_COUNTER, DATA_KNX +from .const import ATTR_COUNTER, DOMAIN +from .knx_entity import KnxEntity async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up binary sensor(s) for KNX platform.""" entities = [] - for device in hass.data[DATA_KNX].xknx.devices: + for device in hass.data[DOMAIN].xknx.devices: if isinstance(device, XknxBinarySensor): entities.append(KNXBinarySensor(device)) async_add_entities(entities) -class KNXBinarySensor(BinarySensorEntity): +class KNXBinarySensor(KnxEntity, BinarySensorEntity): """Representation of a KNX binary sensor.""" def __init__(self, device: XknxBinarySensor): """Initialize of KNX binary sensor.""" - self.device = device - - @callback - def async_register_callbacks(self): - """Register callbacks to update hass after device was changed.""" - - async def after_update_callback(device: XknxBinarySensor): - """Call after device was updated.""" - self.async_write_ha_state() - - self.device.register_device_updated_cb(after_update_callback) - - async def async_added_to_hass(self): - """Store register state change callback.""" - self.async_register_callbacks() - - async def async_update(self): - """Request a state update from KNX bus.""" - await self.device.sync() - - @property - def name(self): - """Return the name of the KNX device.""" - return self.device.name - - @property - def available(self): - """Return True if entity is available.""" - return self.hass.data[DATA_KNX].connected - - @property - def should_poll(self): - """No polling needed within KNX.""" - return False + super().__init__(device) @property def device_class(self): """Return the class of this sensor.""" - return self.device.device_class + if self._device.device_class in DEVICE_CLASSES: + return self._device.device_class + return None @property def is_on(self): """Return true if the binary sensor is on.""" - return self.device.is_on() + return self._device.is_on() @property def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return device specific state attributes.""" - return {ATTR_COUNTER: self.device.counter} + return {ATTR_COUNTER: self._device.counter} diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index b5aaeb67907..1960627a8d6 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -14,8 +14,8 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from . import DATA_KNX -from .const import OPERATION_MODES, PRESET_MODES +from .const import DOMAIN, OPERATION_MODES, PRESET_MODES +from .knx_entity import KnxEntity OPERATION_MODES_INV = dict(reversed(item) for item in OPERATION_MODES.items()) PRESET_MODES_INV = dict(reversed(item) for item in PRESET_MODES.items()) @@ -24,18 +24,19 @@ 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.""" entities = [] - for device in hass.data[DATA_KNX].xknx.devices: + for device in hass.data[DOMAIN].xknx.devices: if isinstance(device, XknxClimate): entities.append(KNXClimate(device)) async_add_entities(entities) -class KNXClimate(ClimateEntity): +class KNXClimate(KnxEntity, ClimateEntity): """Representation of a KNX climate device.""" def __init__(self, device: XknxClimate): """Initialize of a KNX climate device.""" - self.device = device + super().__init__(device) + self._unit_of_measurement = TEMP_CELSIUS @property @@ -43,35 +44,10 @@ class KNXClimate(ClimateEntity): """Return the list of supported features.""" return SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE - async def async_added_to_hass(self) -> None: - """Register callbacks to update hass after device was changed.""" - - async def after_update_callback(device): - """Call after device was updated.""" - self.async_write_ha_state() - - self.device.register_device_updated_cb(after_update_callback) - self.device.mode.register_device_updated_cb(after_update_callback) - async def async_update(self): """Request a state update from KNX bus.""" - await self.device.sync() - await self.device.mode.sync() - - @property - def name(self) -> str: - """Return the name of the KNX device.""" - return self.device.name - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self.hass.data[DATA_KNX].connected - - @property - def should_poll(self) -> bool: - """No polling needed within KNX.""" - return False + await self._device.sync() + await self._device.mode.sync() @property def temperature_unit(self): @@ -81,44 +57,44 @@ class KNXClimate(ClimateEntity): @property def current_temperature(self): """Return the current temperature.""" - return self.device.temperature.value + return self._device.temperature.value @property def target_temperature_step(self): """Return the supported step of target temperature.""" - return self.device.temperature_step + return self._device.temperature_step @property def target_temperature(self): """Return the temperature we try to reach.""" - return self.device.target_temperature.value + return self._device.target_temperature.value @property def min_temp(self): """Return the minimum temperature.""" - return self.device.target_temperature_min + return self._device.target_temperature_min @property def max_temp(self): """Return the maximum temperature.""" - return self.device.target_temperature_max + return self._device.target_temperature_max async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: return - await self.device.set_target_temperature(temperature) + await self._device.set_target_temperature(temperature) self.async_write_ha_state() @property def hvac_mode(self) -> Optional[str]: """Return current operation ie. heat, cool, idle.""" - if self.device.supports_on_off and not self.device.is_on: + if self._device.supports_on_off and not self._device.is_on: return HVAC_MODE_OFF - if self.device.mode.supports_operation_mode: + if self._device.mode.supports_operation_mode: return OPERATION_MODES.get( - self.device.mode.operation_mode.value, HVAC_MODE_HEAT + self._device.mode.operation_mode.value, HVAC_MODE_HEAT ) # default to "heat" return HVAC_MODE_HEAT @@ -128,10 +104,10 @@ class KNXClimate(ClimateEntity): """Return the list of available operation modes.""" _operations = [ OPERATION_MODES.get(operation_mode.value) - for operation_mode in self.device.mode.operation_modes + for operation_mode in self._device.mode.operation_modes ] - if self.device.supports_on_off: + if self._device.supports_on_off: if not _operations: _operations.append(HVAC_MODE_HEAT) _operations.append(HVAC_MODE_OFF) @@ -142,16 +118,16 @@ class KNXClimate(ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set operation mode.""" - if self.device.supports_on_off and hvac_mode == HVAC_MODE_OFF: - await self.device.turn_off() + if self._device.supports_on_off and hvac_mode == HVAC_MODE_OFF: + await self._device.turn_off() 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: + 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) + await self._device.mode.set_operation_mode(knx_operation_mode) self.async_write_ha_state() @property @@ -160,8 +136,8 @@ class KNXClimate(ClimateEntity): Requires SUPPORT_PRESET_MODE. """ - if self.device.mode.supports_operation_mode: - return PRESET_MODES.get(self.device.mode.operation_mode.value, PRESET_AWAY) + if self._device.mode.supports_operation_mode: + return PRESET_MODES.get(self._device.mode.operation_mode.value, PRESET_AWAY) return None @property @@ -172,14 +148,14 @@ class KNXClimate(ClimateEntity): """ _presets = [ PRESET_MODES.get(operation_mode.value) - for operation_mode in self.device.mode.operation_modes + for operation_mode in self._device.mode.operation_modes ] return list(filter(None, _presets)) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if self.device.mode.supports_operation_mode: + if self._device.mode.supports_operation_mode: knx_operation_mode = HVACOperationMode(PRESET_MODES_INV.get(preset_mode)) - await self.device.mode.set_operation_mode(knx_operation_mode) + await self._device.mode.set_operation_mode(knx_operation_mode) self.async_write_ha_state() diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 427bdb0bd0b..8b0dd90393b 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -15,7 +15,6 @@ from homeassistant.components.climate.const import ( ) DOMAIN = "knx" -DATA_KNX = "data_knx" CONF_STATE_ADDRESS = "state_address" CONF_SYNC_STATE = "sync_state" diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 8c50bb2afe9..c677b12c0ee 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -15,65 +15,39 @@ from homeassistant.components.cover import ( from homeassistant.core import callback from homeassistant.helpers.event import async_track_utc_time_change -from . import DATA_KNX +from .const import DOMAIN +from .knx_entity import KnxEntity async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up cover(s) for KNX platform.""" entities = [] - for device in hass.data[DATA_KNX].xknx.devices: + for device in hass.data[DOMAIN].xknx.devices: if isinstance(device, XknxCover): entities.append(KNXCover(device)) async_add_entities(entities) -class KNXCover(CoverEntity): +class KNXCover(KnxEntity, CoverEntity): """Representation of a KNX cover.""" def __init__(self, device: XknxCover): """Initialize the cover.""" - self.device = device + super().__init__(device) + self._unsubscribe_auto_updater = None @callback - def async_register_callbacks(self): - """Register callbacks to update hass after device was changed.""" - - 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) - - async def async_added_to_hass(self): - """Store register state change callback.""" - self.async_register_callbacks() - - async def async_update(self): - """Request a state update from KNX bus.""" - await self.device.sync() - - @property - def name(self): - """Return the name of the KNX device.""" - return self.device.name - - @property - def available(self): - """Return True if entity is available.""" - return self.hass.data[DATA_KNX].connected - - @property - def should_poll(self): - """No polling needed within KNX.""" - return False + async def after_update_callback(self, device): + """Call after device was updated.""" + self.async_write_ha_state() + if self._device.is_traveling(): + self.start_auto_updater() @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - if self.device.supports_angle: + if self._device.supports_angle: return DEVICE_CLASS_BLIND return None @@ -81,9 +55,9 @@ class KNXCover(CoverEntity): def supported_features(self): """Flag supported features.""" supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION - if self.device.supports_stop: + if self._device.supports_stop: supported_features |= SUPPORT_STOP - if self.device.supports_angle: + if self._device.supports_angle: supported_features |= SUPPORT_SET_TILT_POSITION return supported_features @@ -95,57 +69,57 @@ class KNXCover(CoverEntity): """ # In KNX 0 is open, 100 is closed. try: - return 100 - self.device.current_position() + 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() + return self._device.is_closed() @property def is_opening(self): """Return if the cover is opening or not.""" - return self.device.is_opening() + return self._device.is_opening() @property def is_closing(self): """Return if the cover is closing or not.""" - return self.device.is_closing() + return self._device.is_closing() async def async_close_cover(self, **kwargs): """Close the cover.""" - await self.device.set_down() + await self._device.set_down() async def async_open_cover(self, **kwargs): """Open the cover.""" - await self.device.set_up() + await self._device.set_up() async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" knx_position = 100 - kwargs[ATTR_POSITION] - await self.device.set_position(knx_position) + await self._device.set_position(knx_position) async def async_stop_cover(self, **kwargs): """Stop the cover.""" - await self.device.stop() + await self._device.stop() self.stop_auto_updater() @property def current_cover_tilt_position(self): """Return current tilt position of cover.""" - if not self.device.supports_angle: + if not self._device.supports_angle: return None try: - return 100 - self.device.current_angle() + 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.""" knx_tilt_position = 100 - kwargs[ATTR_TILT_POSITION] - await self.device.set_angle(knx_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.""" @@ -164,7 +138,7 @@ class KNXCover(CoverEntity): def auto_updater_hook(self, now): """Call for the autoupdater.""" self.async_write_ha_state() - if self.device.position_reached(): + if self._device.position_reached(): self.stop_auto_updater() - self.hass.add_job(self.device.auto_stop_if_necessary()) + self.hass.add_job(self._device.auto_stop_if_necessary()) diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py new file mode 100644 index 00000000000..296bcb2f540 --- /dev/null +++ b/homeassistant/components/knx/knx_entity.py @@ -0,0 +1,51 @@ +"""Base class for KNX devices.""" +from xknx.devices import Climate as XknxClimate, Device as XknxDevice + +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN + + +class KnxEntity(Entity): + """Representation of a KNX entity.""" + + def __init__(self, device: XknxDevice): + """Set up device.""" + self._device = device + + @property + def name(self): + """Return the name of the KNX device.""" + return self._device.name + + @property + def available(self): + """Return True if entity is available.""" + return self.hass.data[DOMAIN].connected + + @property + def should_poll(self): + """No polling needed within KNX.""" + return False + + async def async_update(self): + """Request a state update from KNX bus.""" + await self._device.sync() + + async def after_update_callback(self, device: XknxDevice): + """Call after device was updated.""" + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Store register state change callback.""" + self._device.register_device_updated_cb(self.after_update_callback) + + if isinstance(self._device, XknxClimate): + self._device.mode.register_device_updated_cb(self.after_update_callback) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect device object when removed.""" + self._device.unregister_device_updated_cb(self.after_update_callback) + + if isinstance(self._device, XknxClimate): + self._device.mode.unregister_device_updated_cb(self.after_update_callback) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 6d8438df0f9..d9f0f9c0d3a 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -12,10 +12,10 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, LightEntity, ) -from homeassistant.core import callback import homeassistant.util.color as color_util -from . import DATA_KNX +from .const import DOMAIN +from .knx_entity import KnxEntity DEFAULT_COLOR = (0.0, 0.0) DEFAULT_BRIGHTNESS = 255 @@ -25,18 +25,18 @@ DEFAULT_WHITE_VALUE = 255 async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up lights for KNX platform.""" entities = [] - for device in hass.data[DATA_KNX].xknx.devices: + for device in hass.data[DOMAIN].xknx.devices: if isinstance(device, XknxLight): entities.append(KNXLight(device)) async_add_entities(entities) -class KNXLight(LightEntity): +class KNXLight(KnxEntity, LightEntity): """Representation of a KNX light.""" def __init__(self, device: XknxLight): """Initialize of KNX light.""" - self.device = device + super().__init__(device) self._min_kelvin = device.min_kelvin self._max_kelvin = device.max_kelvin @@ -47,46 +47,13 @@ class KNXLight(LightEntity): self._min_kelvin ) - @callback - def async_register_callbacks(self): - """Register callbacks to update hass after device was changed.""" - - async def after_update_callback(device): - """Call after device was updated.""" - self.async_write_ha_state() - - self.device.register_device_updated_cb(after_update_callback) - - async def async_added_to_hass(self): - """Store register state change callback.""" - self.async_register_callbacks() - - async def async_update(self): - """Request a state update from KNX bus.""" - await self.device.sync() - - @property - def name(self): - """Return the name of the KNX device.""" - return self.device.name - - @property - def available(self): - """Return True if entity is available.""" - return self.hass.data[DATA_KNX].connected - - @property - def should_poll(self): - """No polling needed within KNX.""" - return False - @property def brightness(self): """Return the brightness of this light between 0..255.""" - if self.device.supports_brightness: - return self.device.current_brightness + if self._device.supports_brightness: + return self._device.current_brightness hsv_color = self._hsv_color - if self.device.supports_color and hsv_color: + if self._device.supports_color and hsv_color: return round(hsv_color[-1] / 100 * 255) return None @@ -94,35 +61,35 @@ class KNXLight(LightEntity): def hs_color(self): """Return the HS color value.""" rgb = None - if self.device.supports_rgbw or self.device.supports_color: - rgb, _ = self.device.current_color + if self._device.supports_rgbw or self._device.supports_color: + rgb, _ = self._device.current_color return color_util.color_RGB_to_hs(*rgb) if rgb else None @property def _hsv_color(self): """Return the HSV color value.""" rgb = None - if self.device.supports_rgbw or self.device.supports_color: - rgb, _ = self.device.current_color + if self._device.supports_rgbw or self._device.supports_color: + rgb, _ = self._device.current_color return color_util.color_RGB_to_hsv(*rgb) if rgb else None @property def white_value(self): """Return the white value.""" white = None - if self.device.supports_rgbw: - _, white = self.device.current_color + if self._device.supports_rgbw: + _, white = self._device.current_color return white @property def color_temp(self): """Return the color temperature in mireds.""" - if self.device.supports_color_temperature: - kelvin = self.device.current_color_temperature + if self._device.supports_color_temperature: + kelvin = self._device.current_color_temperature if kelvin is not None: return color_util.color_temperature_kelvin_to_mired(kelvin) - if self.device.supports_tunable_white: - relative_ct = self.device.current_tunable_white + if self._device.supports_tunable_white: + relative_ct = self._device.current_tunable_white if relative_ct is not None: # as KNX devices typically use Kelvin we use it as base for # calculating ct from percent @@ -155,19 +122,22 @@ class KNXLight(LightEntity): @property def is_on(self): """Return true if light is on.""" - return self.device.state + return self._device.state @property def supported_features(self): """Flag supported features.""" flags = 0 - if self.device.supports_brightness: + if self._device.supports_brightness: flags |= SUPPORT_BRIGHTNESS - if self.device.supports_color: + if self._device.supports_color: flags |= SUPPORT_COLOR | SUPPORT_BRIGHTNESS - if self.device.supports_rgbw: + if self._device.supports_rgbw: flags |= SUPPORT_COLOR | SUPPORT_WHITE_VALUE - if self.device.supports_color_temperature or self.device.supports_tunable_white: + if ( + self._device.supports_color_temperature + or self._device.supports_tunable_white + ): flags |= SUPPORT_COLOR_TEMP return flags @@ -191,14 +161,16 @@ class KNXLight(LightEntity): or update_white_value or update_color_temp ): - await self.device.set_on() + await self._device.set_on() - if self.device.supports_brightness and (update_brightness and not update_color): + if self._device.supports_brightness and ( + update_brightness and not update_color + ): # if we don't need to update the color, try updating brightness # directly if supported; don't do it if color also has to be # changed, as RGB color implicitly sets the brightness as well - await self.device.set_brightness(brightness) - elif (self.device.supports_rgbw or self.device.supports_color) and ( + await self._device.set_brightness(brightness) + elif (self._device.supports_rgbw or self._device.supports_color) and ( update_brightness or update_color or update_white_value ): # change RGB color, white value (if supported), and brightness @@ -208,25 +180,25 @@ class KNXLight(LightEntity): brightness = DEFAULT_BRIGHTNESS if hs_color is None: hs_color = DEFAULT_COLOR - if white_value is None and self.device.supports_rgbw: + if white_value is None and self._device.supports_rgbw: white_value = DEFAULT_WHITE_VALUE rgb = color_util.color_hsv_to_RGB(*hs_color, brightness * 100 / 255) - await self.device.set_color(rgb, white_value) + await self._device.set_color(rgb, white_value) if update_color_temp: kelvin = int(color_util.color_temperature_mired_to_kelvin(mireds)) kelvin = min(self._max_kelvin, max(self._min_kelvin, kelvin)) - if self.device.supports_color_temperature: - await self.device.set_color_temperature(kelvin) - elif self.device.supports_tunable_white: + if self._device.supports_color_temperature: + await self._device.set_color_temperature(kelvin) + elif self._device.supports_tunable_white: relative_ct = int( 255 * (kelvin - self._min_kelvin) / (self._max_kelvin - self._min_kelvin) ) - await self.device.set_tunable_white(relative_ct) + await self._device.set_tunable_white(relative_ct) async def async_turn_off(self, **kwargs): """Turn the light off.""" - await self.device.set_off() + await self._device.set_off() diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index e47cfca2794..7210795bd71 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -5,13 +5,13 @@ from xknx.devices import Notification as XknxNotification from homeassistant.components.notify import BaseNotificationService -from . import DATA_KNX +from .const import DOMAIN async def async_get_service(hass, config, discovery_info=None): """Get the KNX notification service.""" notification_devices = [] - for device in hass.data[DATA_KNX].xknx.devices: + for device in hass.data[DOMAIN].xknx.devices: if isinstance(device, XknxNotification): notification_devices.append(device) return ( diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index b4df94a0fd4..6c76fdbd199 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -5,30 +5,26 @@ from xknx.devices import Scene as XknxScene from homeassistant.components.scene import Scene -from . import DATA_KNX +from .const import DOMAIN +from .knx_entity import KnxEntity 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: + for device in hass.data[DOMAIN].xknx.devices: if isinstance(device, XknxScene): entities.append(KNXScene(device)) async_add_entities(entities) -class KNXScene(Scene): +class KNXScene(KnxEntity, Scene): """Representation of a KNX scene.""" - def __init__(self, scene: XknxScene): + def __init__(self, device: XknxScene): """Init KNX scene.""" - self.scene = scene - - @property - def name(self): - """Return the name of the scene.""" - return self.scene.name + super().__init__(device) async def async_activate(self, **kwargs: Any) -> None: """Activate the scene.""" - await self.scene.run() + await self._device.run() diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index d87119239cf..fc2cbced8bb 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -1,77 +1,43 @@ """Support for KNX/IP sensors.""" from xknx.devices import Sensor as XknxSensor -from homeassistant.core import callback +from homeassistant.components.sensor import DEVICE_CLASSES from homeassistant.helpers.entity import Entity -from . import DATA_KNX +from .const import DOMAIN +from .knx_entity import KnxEntity async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up sensor(s) for KNX platform.""" entities = [] - for device in hass.data[DATA_KNX].xknx.devices: + for device in hass.data[DOMAIN].xknx.devices: if isinstance(device, XknxSensor): entities.append(KNXSensor(device)) async_add_entities(entities) -class KNXSensor(Entity): +class KNXSensor(KnxEntity, Entity): """Representation of a KNX sensor.""" def __init__(self, device: XknxSensor): """Initialize of a KNX sensor.""" - self.device = device - - @callback - def async_register_callbacks(self): - """Register callbacks to update hass after device was changed.""" - - async def after_update_callback(device): - """Call after device was updated.""" - self.async_write_ha_state() - - self.device.register_device_updated_cb(after_update_callback) - - async def async_added_to_hass(self): - """Store register state change callback.""" - self.async_register_callbacks() - - async def async_update(self): - """Update the state from KNX.""" - await self.device.sync() - - @property - def name(self): - """Return the name of the KNX device.""" - return self.device.name - - @property - def available(self): - """Return True if entity is available.""" - return self.hass.data[DATA_KNX].connected - - @property - def should_poll(self): - """No polling needed within KNX.""" - return False + super().__init__(device) @property def state(self): """Return the state of the sensor.""" - return self.device.resolve_state() + return self._device.resolve_state() @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" - return self.device.unit_of_measurement() + return self._device.unit_of_measurement() @property def device_class(self): """Return the device class of the sensor.""" - return self.device.ha_device_class() - - @property - def device_state_attributes(self): - """Return the state attributes.""" + device_class = self._device.ha_device_class() + if device_class in DEVICE_CLASSES: + return device_class return None diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index c378d1b0ca4..ae3048e2d23 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -2,69 +2,36 @@ from xknx.devices import Switch as XknxSwitch from homeassistant.components.switch import SwitchEntity -from homeassistant.core import callback -from . import DATA_KNX +from . import DOMAIN +from .knx_entity import KnxEntity async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up switch(es) for KNX platform.""" entities = [] - for device in hass.data[DATA_KNX].xknx.devices: + for device in hass.data[DOMAIN].xknx.devices: if isinstance(device, XknxSwitch): entities.append(KNXSwitch(device)) async_add_entities(entities) -class KNXSwitch(SwitchEntity): +class KNXSwitch(KnxEntity, SwitchEntity): """Representation of a KNX switch.""" def __init__(self, device: XknxSwitch): """Initialize of KNX switch.""" - self.device = device - - @callback - def async_register_callbacks(self): - """Register callbacks to update hass after device was changed.""" - - async def after_update_callback(device): - """Call after device was updated.""" - self.async_write_ha_state() - - self.device.register_device_updated_cb(after_update_callback) - - async def async_added_to_hass(self): - """Store register state change callback.""" - self.async_register_callbacks() - - async def async_update(self): - """Request a state update from KNX bus.""" - await self.device.sync() - - @property - def name(self): - """Return the name of the KNX device.""" - return self.device.name - - @property - def available(self): - """Return true if entity is available.""" - return self.hass.data[DATA_KNX].connected - - @property - def should_poll(self): - """Return the polling state. Not needed within KNX.""" - return False + super().__init__(device) @property def is_on(self): """Return true if device is on.""" - return self.device.state + return self._device.state async def async_turn_on(self, **kwargs): """Turn the device on.""" - await self.device.set_on() + await self._device.set_on() async def async_turn_off(self, **kwargs): """Turn the device off.""" - await self.device.set_off() + await self._device.set_off() diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 09dc1a305c6..097fa661f4a 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -5,34 +5,30 @@ from xknx.devices import Weather as XknxWeather from homeassistant.components.weather import WeatherEntity from homeassistant.const import TEMP_CELSIUS -from .const import DATA_KNX +from .const import DOMAIN +from .knx_entity import KnxEntity 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: + for device in hass.data[DOMAIN].xknx.devices: if isinstance(device, XknxWeather): entities.append(KNXWeather(device)) async_add_entities(entities) -class KNXWeather(WeatherEntity): +class KNXWeather(KnxEntity, WeatherEntity): """Representation of a KNX weather device.""" def __init__(self, device: XknxWeather): """Initialize of a KNX sensor.""" - self.device = device - - @property - def name(self): - """Return the name of the weather device.""" - return self.device.name + super().__init__(device) @property def temperature(self): """Return current temperature.""" - return self.device.temperature + return self._device.temperature @property def temperature_unit(self): @@ -44,25 +40,27 @@ class KNXWeather(WeatherEntity): """Return current air pressure.""" # KNX returns pA - HA requires hPa return ( - self.device.air_pressure / 100 - if self.device.air_pressure is not None + 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 + 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 + 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 + self._device.wind_speed * 3.6 + if self._device.wind_speed is not None + else None ) From 31ece55c57e912fbf22ef519ed9df87651477368 Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Mon, 21 Sep 2020 13:18:54 -0400 Subject: [PATCH 274/514] bump pynws to 1.3.0 (#40386) --- homeassistant/components/nws/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index 23643699f3a..ef0a35b846a 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -3,7 +3,7 @@ "name": "National Weather Service (NWS)", "documentation": "https://www.home-assistant.io/integrations/nws", "codeowners": ["@MatthewFlamm"], - "requirements": ["pynws==1.2.1"], + "requirements": ["pynws==1.3.0"], "quality_scale": "platinum", "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 987d085dd79..29b4a361db8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1524,7 +1524,7 @@ pynuki==1.3.8 pynut2==2.1.2 # homeassistant.components.nws -pynws==1.2.1 +pynws==1.3.0 # homeassistant.components.nx584 pynx584==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c04b2f707c..e60a517b92d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -737,7 +737,7 @@ pymyq==2.0.5 pynut2==2.1.2 # homeassistant.components.nws -pynws==1.2.1 +pynws==1.3.0 # homeassistant.components.nx584 pynx584==0.5 From 1d7754f160b6cbada821fc2f0827d2a71378472e Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 21 Sep 2020 19:25:06 +0200 Subject: [PATCH 275/514] Add supervisor add-on uninstall helper (#40413) --- homeassistant/components/hassio/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 777d5938b1b..36bfcf04bca 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -147,6 +147,17 @@ async def async_install_addon(hass: HomeAssistantType, slug: str) -> None: await hassio.send_command(command) +@bind_hass +async def async_uninstall_addon(hass: HomeAssistantType, slug: str) -> None: + """Uninstall add-on. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/uninstall" + await hassio.send_command(command) + + @callback @bind_hass def get_info(hass): From 447446c56560aeace5a2ae5327d494339f572134 Mon Sep 17 00:00:00 2001 From: On Freund Date: Mon, 21 Sep 2020 21:27:00 +0300 Subject: [PATCH 276/514] Fix handling of empty ws port (#40399) --- homeassistant/components/kodi/config_flow.py | 4 ++ tests/components/kodi/test_config_flow.py | 45 ++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index 067ee8be476..c11255aba87 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -202,6 +202,10 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: self._ws_port = user_input.get(CONF_WS_PORT) + # optional ints return 0 rather than None when empty + if self._ws_port == 0: + self._ws_port = None + try: await validate_ws(self.hass, self._get_data()) except WSCannotConnect: diff --git a/tests/components/kodi/test_config_flow.py b/tests/components/kodi/test_config_flow.py index 4fd61ede8ba..71c2bce1307 100644 --- a/tests/components/kodi/test_config_flow.py +++ b/tests/components/kodi/test_config_flow.py @@ -165,6 +165,51 @@ async def test_form_valid_ws_port(hass, user_flow): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_empty_ws_port(hass, user_flow): + """Test we handle an empty websocket port input.""" + 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.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"], {"ws_port": 0} + ) + + assert result["type"] == "create_entry" + assert result["title"] == TEST_HOST["host"] + assert result["data"] == { + **TEST_HOST, + "ws_port": None, + "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_invalid_auth(hass, user_flow): """Test we handle invalid auth.""" with patch( From 663245c3515d0b44fac08c5e6c8ea29675e2b6ba Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 21 Sep 2020 21:10:02 +0200 Subject: [PATCH 277/514] Fix OSError (#40393) --- 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 83d5d7b9f3a..c3b701449c2 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -86,7 +86,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): try: async with async_timeout.timeout(5): return await self.device.update() - except aiocoap_error.Error as err: + except (aiocoap_error.Error, OSError) as err: raise update_coordinator.UpdateFailed("Error fetching data") from err @property From bfbaa1e8bbc151eb77980237146c99b403dd734a Mon Sep 17 00:00:00 2001 From: Michael Thingnes Date: Tue, 22 Sep 2020 07:56:08 +1200 Subject: [PATCH 278/514] Validate Met.no forecast entries before passing them on to HA (#40400) --- homeassistant/components/met/weather.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 3abd7638516..e4c64f9aeda 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -5,6 +5,8 @@ import voluptuous as vol from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TIME, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, @@ -209,8 +211,11 @@ class MetWeather(CoordinatorEntity, WeatherEntity): met_forecast = self.coordinator.data.hourly_forecast else: met_forecast = self.coordinator.data.daily_forecast + required_keys = {ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME} ha_forecast = [] for met_item in met_forecast: + if not set(met_item).issuperset(required_keys): + continue ha_item = { k: met_item[v] for k, v in FORECAST_MAP.items() if met_item.get(v) } From f78391ce2ac6358edf58b25113c9330cee111eaf Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Mon, 21 Sep 2020 16:40:29 -0400 Subject: [PATCH 279/514] Bump pyinsteon to 1.0.8 (#40383) --- homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 871629b6877..d20f56054b3 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -2,7 +2,7 @@ "domain": "insteon", "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", - "requirements": ["pyinsteon==1.0.7"], + "requirements": ["pyinsteon==1.0.8"], "codeowners": ["@teharris1"], "config_flow": true } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 29b4a361db8..23c7a950fe3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1404,7 +1404,7 @@ pyialarm==0.3 pyicloud==0.9.7 # homeassistant.components.insteon -pyinsteon==1.0.7 +pyinsteon==1.0.8 # homeassistant.components.intesishome pyintesishome==1.7.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e60a517b92d..8b8e96dc2b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -677,7 +677,7 @@ pyhomematic==0.1.68 pyicloud==0.9.7 # homeassistant.components.insteon -pyinsteon==1.0.7 +pyinsteon==1.0.8 # homeassistant.components.ipma pyipma==2.0.5 From 5e90a4d000f4db4975b5f5db1de2b838121a5220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 22 Sep 2020 00:03:39 +0300 Subject: [PATCH 280/514] Use more state attribute name constants (#40428) --- .../alarm_control_panel/device_action.py | 3 +- .../alarm_control_panel/device_condition.py | 3 +- .../alarm_control_panel/device_trigger.py | 3 +- homeassistant/components/alexa/handlers.py | 3 +- .../components/climate/device_action.py | 3 +- .../components/climate/device_condition.py | 6 +- homeassistant/components/dweet/__init__.py | 3 +- .../homekit/type_security_systems.py | 3 +- .../components/humidifier/device_action.py | 3 +- .../components/humidifier/device_condition.py | 3 +- .../components/mobile_app/webhook.py | 3 +- .../components/owntracks/__init__.py | 13 +++-- .../components/owntracks/messages.py | 7 ++- .../components/prometheus/__init__.py | 6 +- .../components/proximity/__init__.py | 18 +++--- tests/components/august/test_sensor.py | 6 +- .../components/config/test_entity_registry.py | 5 +- tests/components/demo/test_geo_location.py | 18 ++++-- tests/components/demo/test_media_player.py | 3 +- tests/components/dynalite/test_cover.py | 6 +- tests/components/dynalite/test_light.py | 5 +- tests/components/dynalite/test_switch.py | 4 +- tests/components/eafm/test_sensor.py | 11 ++-- tests/components/geofency/test_init.py | 6 +- tests/components/rflink/test_sensor.py | 13 +++-- tests/components/rfxtrx/test_sensor.py | 54 +++++++++--------- tests/components/sma/test_sensor.py | 4 +- tests/components/spaceapi/test_init.py | 8 +-- tests/components/startca/test_sensor.py | 55 ++++++++++--------- tests/components/statistics/test_sensor.py | 2 +- tests/components/teksavvy/test_sensor.py | 47 +++++++++------- tests/components/utility_meter/test_init.py | 11 ++-- tests/components/utility_meter/test_sensor.py | 27 +++++---- tests/components/vera/test_sensor.py | 6 +- tests/components/wunderground/test_sensor.py | 13 +++-- tests/helpers/test_template.py | 3 +- 36 files changed, 231 insertions(+), 156 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py index 81e444ae16f..0dc16fdcf42 100644 --- a/homeassistant/components/alarm_control_panel/device_action.py +++ b/homeassistant/components/alarm_control_panel/device_action.py @@ -6,6 +6,7 @@ import voluptuous as vol from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, CONF_CODE, CONF_DEVICE_ID, CONF_DOMAIN, @@ -56,7 +57,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: if state is None: continue - supported_features = state.attributes["supported_features"] + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] # Add actions for each entity that belongs to this integration if supported_features & SUPPORT_ALARM_ARM_AWAY: diff --git a/homeassistant/components/alarm_control_panel/device_condition.py b/homeassistant/components/alarm_control_panel/device_condition.py index c4d43d1b051..e5b3ec6aeee 100644 --- a/homeassistant/components/alarm_control_panel/device_condition.py +++ b/homeassistant/components/alarm_control_panel/device_condition.py @@ -11,6 +11,7 @@ from homeassistant.components.alarm_control_panel.const import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, CONF_CONDITION, CONF_DEVICE_ID, CONF_DOMAIN, @@ -73,7 +74,7 @@ async def async_get_conditions( if state is None: continue - supported_features = state.attributes["supported_features"] + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] # Add conditions for each entity that belongs to this integration conditions += [ diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py index eeea1dbbf33..cb07ff35e96 100644 --- a/homeassistant/components/alarm_control_panel/device_trigger.py +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -12,6 +12,7 @@ from homeassistant.components.automation import AutomationActionType from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import ( + ATTR_SUPPORTED_FEATURES, CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, @@ -64,7 +65,7 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: if entity_state is None: continue - supported_features = entity_state.attributes["supported_features"] + supported_features = entity_state.attributes[ATTR_SUPPORTED_FEATURES] # Add triggers for each entity that belongs to this integration triggers += [ diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 6eeb3235a64..783c7a36949 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -17,6 +17,7 @@ from homeassistant.components import ( from homeassistant.components.climate import const as climate from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, SERVICE_ALARM_ARM_AWAY, @@ -1532,7 +1533,7 @@ async def async_api_initialize_camera_stream(hass, config, directive, context): """Process a InitializeCameraStreams request.""" entity = directive.entity stream_source = await camera.async_request_stream(hass, entity.entity_id, fmt="hls") - camera_image = hass.states.get(entity.entity_id).attributes["entity_picture"] + camera_image = hass.states.get(entity.entity_id).attributes[ATTR_ENTITY_PICTURE] try: external_url = network.get_url( diff --git a/homeassistant/components/climate/device_action.py b/homeassistant/components/climate/device_action.py index 6f7725ac835..3f2b8dc23f2 100644 --- a/homeassistant/components/climate/device_action.py +++ b/homeassistant/components/climate/device_action.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, @@ -61,7 +62,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: CONF_TYPE: "set_hvac_mode", } ) - if state.attributes["supported_features"] & const.SUPPORT_PRESET_MODE: + if state.attributes[ATTR_SUPPORTED_FEATURES] & const.SUPPORT_PRESET_MODE: actions.append( { CONF_DEVICE_ID: device_id, diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py index 8a5b9ceede8..423efdf8196 100644 --- a/homeassistant/components/climate/device_condition.py +++ b/homeassistant/components/climate/device_condition.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, CONF_CONDITION, CONF_DEVICE_ID, CONF_DOMAIN, @@ -63,7 +64,10 @@ async def async_get_conditions( } ) - if state and state.attributes["supported_features"] & const.SUPPORT_PRESET_MODE: + if ( + state + and state.attributes[ATTR_SUPPORTED_FEATURES] & const.SUPPORT_PRESET_MODE + ): conditions.append( { CONF_CONDITION: "device", diff --git a/homeassistant/components/dweet/__init__.py b/homeassistant/components/dweet/__init__.py index db985e57a41..c076fc81628 100644 --- a/homeassistant/components/dweet/__init__.py +++ b/homeassistant/components/dweet/__init__.py @@ -6,6 +6,7 @@ import dweepy import voluptuous as vol from homeassistant.const import ( + ATTR_FRIENDLY_NAME, CONF_NAME, CONF_WHITELIST, EVENT_STATE_CHANGED, @@ -58,7 +59,7 @@ def setup(hass, config): except ValueError: _state = state.state - json_body[state.attributes.get("friendly_name")] = _state + json_body[state.attributes.get(ATTR_FRIENDLY_NAME)] = _state send_data(name, json_body) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index a5530a45d56..feae1b5cd06 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -14,6 +14,7 @@ from homeassistant.components.alarm_control_panel.const import ( from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_NIGHT, @@ -71,7 +72,7 @@ class SecuritySystem(HomeAccessory): self._alarm_code = self.config.get(ATTR_CODE) supported_states = state.attributes.get( - "supported_features", + ATTR_SUPPORTED_FEATURES, ( SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY diff --git a/homeassistant/components/humidifier/device_action.py b/homeassistant/components/humidifier/device_action.py index a6194994a9c..6bccd375207 100644 --- a/homeassistant/components/humidifier/device_action.py +++ b/homeassistant/components/humidifier/device_action.py @@ -6,6 +6,7 @@ import voluptuous as vol from homeassistant.components.device_automation import toggle_entity from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, @@ -63,7 +64,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: if state is None: continue - if state.attributes["supported_features"] & const.SUPPORT_MODES: + if state.attributes[ATTR_SUPPORTED_FEATURES] & const.SUPPORT_MODES: actions.append( { CONF_DEVICE_ID: device_id, diff --git a/homeassistant/components/humidifier/device_condition.py b/homeassistant/components/humidifier/device_condition.py index 7f37fc3b1fa..714a51ab016 100644 --- a/homeassistant/components/humidifier/device_condition.py +++ b/homeassistant/components/humidifier/device_condition.py @@ -6,6 +6,7 @@ import voluptuous as vol from homeassistant.components.device_automation import toggle_entity from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, CONF_CONDITION, CONF_DEVICE_ID, CONF_DOMAIN, @@ -48,7 +49,7 @@ async def async_get_conditions( state = hass.states.get(entry.entity_id) - if state and state.attributes["supported_features"] & const.SUPPORT_MODES: + if state and state.attributes[ATTR_SUPPORTED_FEATURES] & const.SUPPORT_MODES: conditions.append( { CONF_CONDITION: "device", diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 2f5e69fd02b..bbeea2e9521 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -26,6 +26,7 @@ from homeassistant.const import ( ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, + ATTR_SUPPORTED_FEATURES, CONF_WEBHOOK_ID, HTTP_BAD_REQUEST, HTTP_CREATED, @@ -267,7 +268,7 @@ async def webhook_stream_camera(hass, config_entry, data): resp = {"mjpeg_path": "/api/camera_proxy_stream/%s" % (camera.entity_id)} - if camera.attributes["supported_features"] & CAMERA_SUPPORT_STREAM: + if camera.attributes[ATTR_SUPPORTED_FEATURES] & CAMERA_SUPPORT_STREAM: try: resp["hls_path"] = await hass.components.camera.async_request_stream( camera.entity_id, "hls" diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 24dc99de71c..d3091d7d027 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -9,7 +9,12 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import mqtt -from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.const import ( + ATTR_GPS_ACCURACY, + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_WEBHOOK_ID, +) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_when_setup @@ -292,9 +297,9 @@ class OwnTracksContext: device_tracker_state = hass.states.get(f"device_tracker.{dev_id}") if device_tracker_state is not None: - acc = device_tracker_state.attributes.get("gps_accuracy") - lat = device_tracker_state.attributes.get("latitude") - lon = device_tracker_state.attributes.get("longitude") + acc = device_tracker_state.attributes.get(ATTR_GPS_ACCURACY) + lat = device_tracker_state.attributes.get(ATTR_LATITUDE) + lon = device_tracker_state.attributes.get(ATTR_LONGITUDE) if lat is not None and lon is not None: kwargs["gps"] = (lat, lon) diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 5e610d861fe..3a4aac6bfd1 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -10,7 +10,7 @@ from homeassistant.components.device_tracker import ( SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_GPS, ) -from homeassistant.const import STATE_HOME +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, STATE_HOME from homeassistant.util import decorator, slugify from .helper import supports_encryption @@ -97,7 +97,10 @@ def _set_gps_from_zone(kwargs, location, zone): Async friendly. """ if zone is not None: - kwargs["gps"] = (zone.attributes["latitude"], zone.attributes["longitude"]) + kwargs["gps"] = ( + zone.attributes[ATTR_LATITUDE], + zone.attributes[ATTR_LONGITUDE], + ) kwargs["gps_accuracy"] = zone.attributes["radius"] kwargs["location_name"] = location return kwargs diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 87a8a2c41f3..bd9a6e35276 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -19,7 +19,9 @@ from homeassistant.components.humidifier.const import ( ATTR_MODE, ) from homeassistant.const import ( + ATTR_BATTERY_LEVEL, ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CONTENT_TYPE_TEXT_PLAIN, @@ -234,7 +236,7 @@ class PrometheusMetrics: return { "entity": state.entity_id, "domain": state.domain, - "friendly_name": state.attributes.get("friendly_name"), + "friendly_name": state.attributes.get(ATTR_FRIENDLY_NAME), } def _battery(self, state): @@ -245,7 +247,7 @@ class PrometheusMetrics: "Battery level as a percentage of its capacity", ) try: - value = float(state.attributes["battery_level"]) + value = float(state.attributes[ATTR_BATTERY_LEVEL]) metric.labels(**self._labels(state)).set(value) except ValueError: pass diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index 7beaaaf00e1..2d0d14a69c1 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -4,6 +4,8 @@ import logging import voluptuous as vol from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, CONF_DEVICES, CONF_UNIT_OF_MEASUREMENT, CONF_ZONE, @@ -149,8 +151,8 @@ class Proximity(Entity): devices_in_zone = "" zone_state = self.hass.states.get(self.proximity_zone) - proximity_latitude = zone_state.attributes.get("latitude") - proximity_longitude = zone_state.attributes.get("longitude") + proximity_latitude = zone_state.attributes.get(ATTR_LATITUDE) + proximity_longitude = zone_state.attributes.get(ATTR_LONGITUDE) # Check for devices in the monitored zone. for device in self.proximity_devices: @@ -206,8 +208,8 @@ class Proximity(Entity): dist_to_zone = distance( proximity_latitude, proximity_longitude, - device_state.attributes["latitude"], - device_state.attributes["longitude"], + device_state.attributes[ATTR_LATITUDE], + device_state.attributes[ATTR_LONGITUDE], ) # Add the device and distance to a dictionary. @@ -250,14 +252,14 @@ class Proximity(Entity): old_distance = distance( proximity_latitude, proximity_longitude, - old_state.attributes["latitude"], - old_state.attributes["longitude"], + old_state.attributes[ATTR_LATITUDE], + old_state.attributes[ATTR_LONGITUDE], ) new_distance = distance( proximity_latitude, proximity_longitude, - new_state.attributes["latitude"], - new_state.attributes["longitude"], + new_state.attributes[ATTR_LATITUDE], + new_state.attributes[ATTR_LONGITUDE], ) distance_travelled = round(new_distance - old_distance, 1) diff --git a/tests/components/august/test_sensor.py b/tests/components/august/test_sensor.py index 7e69b59da07..51e00b9d09f 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 PERCENTAGE, STATE_UNAVAILABLE +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNAVAILABLE from tests.components.august.mocks import ( _create_august_with_devices, @@ -75,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"] == PERCENTAGE + assert state.attributes[ATTR_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" @@ -105,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"] == PERCENTAGE + assert state.attributes[ATTR_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/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 2a696624e0c..d63d10437cc 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -4,6 +4,7 @@ from collections import OrderedDict import pytest from homeassistant.components.config import entity_registry +from homeassistant.const import ATTR_ICON from homeassistant.helpers.entity_registry import RegistryEntry from tests.common import MockEntity, MockEntityPlatform, mock_registry @@ -140,7 +141,7 @@ async def test_update_entity(hass, client): state = hass.states.get("test_domain.world") assert state is not None assert state.name == "before update" - assert state.attributes["icon"] == "icon:before update" + assert state.attributes[ATTR_ICON] == "icon:before update" # UPDATE NAME & ICON await client.send_json( @@ -171,7 +172,7 @@ async def test_update_entity(hass, client): state = hass.states.get("test_domain.world") assert state.name == "after update" - assert state.attributes["icon"] == "icon:after update" + assert state.attributes[ATTR_ICON] == "icon:after update" # UPDATE DISABLED_BY TO USER await client.send_json( diff --git a/tests/components/demo/test_geo_location.py b/tests/components/demo/test_geo_location.py index 7ca870bc34f..ac32fff075f 100644 --- a/tests/components/demo/test_geo_location.py +++ b/tests/components/demo/test_geo_location.py @@ -8,7 +8,12 @@ from homeassistant.components.demo.geo_location import ( DEFAULT_UPDATE_INTERVAL, NUMBER_OF_DEMO_DEVICES, ) -from homeassistant.const import LENGTH_KILOMETERS +from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_UNIT_OF_MEASUREMENT, + LENGTH_KILOMETERS, +) from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util @@ -59,13 +64,14 @@ class TestDemoPlatform(unittest.TestCase): # ignore home zone state continue assert ( - abs(state.attributes["latitude"] - self.hass.config.latitude) < 1.0 - ) - assert ( - abs(state.attributes["longitude"] - self.hass.config.longitude) + abs(state.attributes[ATTR_LATITUDE] - self.hass.config.latitude) < 1.0 ) - assert state.attributes["unit_of_measurement"] == LENGTH_KILOMETERS + assert ( + abs(state.attributes[ATTR_LONGITUDE] - self.hass.config.longitude) + < 1.0 + ) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == LENGTH_KILOMETERS # Update (replaces 1 device). fire_time_changed(self.hass, utcnow + DEFAULT_UPDATE_INTERVAL) diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py index 1ab8195c4db..70b05cd25cc 100644 --- a/tests/components/demo/test_media_player.py +++ b/tests/components/demo/test_media_player.py @@ -3,6 +3,7 @@ import pytest import voluptuous as vol import homeassistant.components.media_player as mp +from homeassistant.const import ATTR_SUPPORTED_FEATURES from homeassistant.helpers.aiohttp_client import DATA_CLIENTSESSION from homeassistant.setup import async_setup_component @@ -203,7 +204,7 @@ async def test_seek(hass, mock_media_seek): await hass.async_block_till_done() ent_id = "media_player.living_room" state = hass.states.get(ent_id) - assert state.attributes["supported_features"] & mp.SUPPORT_SEEK + assert state.attributes[ATTR_SUPPORTED_FEATURES] & mp.SUPPORT_SEEK assert not mock_media_seek.called with pytest.raises(vol.Invalid): await common.async_media_seek(hass, None, ent_id) diff --git a/tests/components/dynalite/test_cover.py b/tests/components/dynalite/test_cover.py index cef4081c607..4f696d905d3 100644 --- a/tests/components/dynalite/test_cover.py +++ b/tests/components/dynalite/test_cover.py @@ -2,6 +2,8 @@ from dynalite_devices_lib.cover import DynaliteTimeCoverWithTiltDevice import pytest +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME + from .common import ( ATTR_ARGS, ATTR_METHOD, @@ -24,7 +26,7 @@ async def test_cover_setup(hass, mock_device): """Test a successful setup.""" await create_entity_from_device(hass, mock_device) entity_state = hass.states.get("cover.name") - assert entity_state.attributes["friendly_name"] == mock_device.name + assert entity_state.attributes[ATTR_FRIENDLY_NAME] == mock_device.name assert ( entity_state.attributes["current_position"] == mock_device.current_cover_position @@ -33,7 +35,7 @@ async def test_cover_setup(hass, mock_device): entity_state.attributes["current_tilt_position"] == mock_device.current_cover_tilt_position ) - assert entity_state.attributes["device_class"] == mock_device.device_class + assert entity_state.attributes[ATTR_DEVICE_CLASS] == mock_device.device_class await run_service_tests( hass, mock_device, diff --git a/tests/components/dynalite/test_light.py b/tests/components/dynalite/test_light.py index deea32d2e34..7df10fb08e8 100644 --- a/tests/components/dynalite/test_light.py +++ b/tests/components/dynalite/test_light.py @@ -4,6 +4,7 @@ from dynalite_devices_lib.light import DynaliteChannelLightDevice import pytest from homeassistant.components.light import SUPPORT_BRIGHTNESS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES from .common import ( ATTR_METHOD, @@ -25,9 +26,9 @@ async def test_light_setup(hass, mock_device): """Test a successful setup.""" await create_entity_from_device(hass, mock_device) entity_state = hass.states.get("light.name") - assert entity_state.attributes["friendly_name"] == mock_device.name + assert entity_state.attributes[ATTR_FRIENDLY_NAME] == mock_device.name assert entity_state.attributes["brightness"] == mock_device.brightness - assert entity_state.attributes["supported_features"] == SUPPORT_BRIGHTNESS + assert entity_state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_BRIGHTNESS await run_service_tests( hass, mock_device, diff --git a/tests/components/dynalite/test_switch.py b/tests/components/dynalite/test_switch.py index 7c0c5d632d3..de375e3b348 100644 --- a/tests/components/dynalite/test_switch.py +++ b/tests/components/dynalite/test_switch.py @@ -3,6 +3,8 @@ from dynalite_devices_lib.switch import DynalitePresetSwitchDevice import pytest +from homeassistant.const import ATTR_FRIENDLY_NAME + from .common import ( ATTR_METHOD, ATTR_SERVICE, @@ -22,7 +24,7 @@ async def test_switch_setup(hass, mock_device): """Test a successful setup.""" await create_entity_from_device(hass, mock_device) entity_state = hass.states.get("switch.name") - assert entity_state.attributes["friendly_name"] == mock_device.name + assert entity_state.attributes[ATTR_FRIENDLY_NAME] == mock_device.name await run_service_tests( hass, mock_device, diff --git a/tests/components/eafm/test_sensor.py b/tests/components/eafm/test_sensor.py index 6cce7a2bc4b..a7ee0403c7c 100644 --- a/tests/components/eafm/test_sensor.py +++ b/tests/components/eafm/test_sensor.py @@ -5,6 +5,7 @@ import aiohttp import pytest from homeassistant import config_entries +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -250,7 +251,7 @@ async def test_reading_is_sampled(hass, mock_get_station): state = hass.states.get("sensor.my_station_water_level_stage") assert state.state == "5" - assert state.attributes["unit_of_measurement"] == "m" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "m" async def test_multiple_readings_are_sampled(hass, mock_get_station): @@ -287,11 +288,11 @@ async def test_multiple_readings_are_sampled(hass, mock_get_station): state = hass.states.get("sensor.my_station_water_level_stage") assert state.state == "5" - assert state.attributes["unit_of_measurement"] == "m" + assert state.attributes[ATTR_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" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "m" async def test_ignore_no_latest_reading(hass, mock_get_station): @@ -327,7 +328,7 @@ async def test_ignore_no_latest_reading(hass, mock_get_station): state = hass.states.get("sensor.my_station_water_level_stage") assert state.state == "5" - assert state.attributes["unit_of_measurement"] == "m" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "m" state = hass.states.get("sensor.my_station_water_level_second_stage") assert state is None @@ -357,7 +358,7 @@ async def test_mark_existing_as_unavailable_if_no_latest(hass, mock_get_station) state = hass.states.get("sensor.my_station_water_level_stage") assert state.state == "5" - assert state.attributes["unit_of_measurement"] == "m" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "m" await poll( { diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index dd3132b0117..21b6830e7f4 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -6,6 +6,8 @@ from homeassistant.components import zone from homeassistant.components.geofency import CONF_MOBILE_BEACONS, DOMAIN from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, STATE_HOME, @@ -316,5 +318,5 @@ async def test_load_unload_entry(hass, geofency_client, webhook_id): assert state_1 is not state_2 assert STATE_HOME == state_2.state - assert state_2.attributes["latitude"] == HOME_LATITUDE - assert state_2.attributes["longitude"] == HOME_LONGITUDE + assert state_2.attributes[ATTR_LATITUDE] == HOME_LATITUDE + assert state_2.attributes[ATTR_LONGITUDE] == HOME_LONGITUDE diff --git a/tests/components/rflink/test_sensor.py b/tests/components/rflink/test_sensor.py index 1468037b70d..d18076a372a 100644 --- a/tests/components/rflink/test_sensor.py +++ b/tests/components/rflink/test_sensor.py @@ -12,7 +12,12 @@ from homeassistant.components.rflink import ( EVENT_KEY_SENSOR, TMP_ENTITY, ) -from homeassistant.const import PERCENTAGE, STATE_UNKNOWN, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, + STATE_UNKNOWN, + TEMP_CELSIUS, +) from tests.components.rflink.test_init import mock_rflink @@ -42,7 +47,7 @@ async def test_default_setup(hass, monkeypatch): config_sensor = hass.states.get("sensor.test") assert config_sensor assert config_sensor.state == "unknown" - assert config_sensor.attributes["unit_of_measurement"] == TEMP_CELSIUS + assert config_sensor.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS # test event for config sensor event_callback( @@ -62,7 +67,7 @@ async def test_default_setup(hass, monkeypatch): new_sensor = hass.states.get("sensor.test2") assert new_sensor assert new_sensor.state == "0" - assert new_sensor.attributes["unit_of_measurement"] == TEMP_CELSIUS + assert new_sensor.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS assert new_sensor.attributes["icon"] == "mdi:thermometer" @@ -160,7 +165,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"] == PERCENTAGE + assert updated_sensor.attributes[ATTR_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 18239550a85..0186d403245 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 PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, TEMP_CELSIUS from homeassistant.core import State from homeassistant.setup import async_setup_component @@ -35,7 +35,7 @@ async def test_one_sensor(hass, rfxtrx): state.attributes.get("friendly_name") == "WT260,WT260H,WT440H,WT450,WT450H 05:02 Temperature" ) - assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS @pytest.mark.parametrize( @@ -75,31 +75,31 @@ async def test_one_sensor_no_datatype(hass, rfxtrx): assert state assert state.state == "unknown" assert state.attributes.get("friendly_name") == f"{base_name} Temperature" - assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS state = hass.states.get(f"{base_id}_humidity") assert state assert state.state == "unknown" assert state.attributes.get("friendly_name") == f"{base_name} Humidity" - assert state.attributes.get("unit_of_measurement") == PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE state = hass.states.get(f"{base_id}_humidity_status") assert state assert state.state == "unknown" assert state.attributes.get("friendly_name") == f"{base_name} Humidity status" - assert state.attributes.get("unit_of_measurement") == "" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" state = hass.states.get(f"{base_id}_rssi_numeric") assert state assert state.state == "unknown" assert state.attributes.get("friendly_name") == f"{base_name} Rssi numeric" - assert state.attributes.get("unit_of_measurement") == "dBm" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "dBm" state = hass.states.get(f"{base_id}_battery_numeric") assert state assert state.state == "unknown" assert state.attributes.get("friendly_name") == f"{base_name} Battery numeric" - assert state.attributes.get("unit_of_measurement") == PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE async def test_several_sensors(hass, rfxtrx): @@ -127,7 +127,7 @@ async def test_several_sensors(hass, rfxtrx): state.attributes.get("friendly_name") == "WT260,WT260H,WT440H,WT450,WT450H 05:02 Temperature" ) - assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS state = hass.states.get("sensor.wt260_wt260h_wt440h_wt450_wt450h_06_01_temperature") assert state @@ -136,7 +136,7 @@ async def test_several_sensors(hass, rfxtrx): state.attributes.get("friendly_name") == "WT260,WT260H,WT440H,WT450,WT450H 06:01 Temperature" ) - assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS state = hass.states.get("sensor.wt260_wt260h_wt440h_wt450_wt450h_06_01_humidity") assert state @@ -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") == PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE async def test_discover_sensor(hass, rfxtrx_automatic): @@ -159,27 +159,27 @@ 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") == PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE state = hass.states.get(f"{base_id}_humidity_status") assert state assert state.state == "normal" - assert state.attributes.get("unit_of_measurement") == "" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" state = hass.states.get(f"{base_id}_rssi_numeric") assert state assert state.state == "-64" - assert state.attributes.get("unit_of_measurement") == "dBm" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "dBm" state = hass.states.get(f"{base_id}_temperature") assert state assert state.state == "18.4" - assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS state = hass.states.get(f"{base_id}_battery_numeric") assert state assert state.state == "90" - assert state.attributes.get("unit_of_measurement") == PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE # 2 await rfxtrx.signal("0a52080405020095240279") @@ -188,27 +188,27 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state assert state.state == "36" - assert state.attributes.get("unit_of_measurement") == PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE state = hass.states.get(f"{base_id}_humidity_status") assert state assert state.state == "normal" - assert state.attributes.get("unit_of_measurement") == "" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" state = hass.states.get(f"{base_id}_rssi_numeric") assert state assert state.state == "-64" - assert state.attributes.get("unit_of_measurement") == "dBm" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "dBm" state = hass.states.get(f"{base_id}_temperature") assert state assert state.state == "14.9" - assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS state = hass.states.get(f"{base_id}_battery_numeric") assert state assert state.state == "90" - assert state.attributes.get("unit_of_measurement") == PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE # 1 Update await rfxtrx.signal("0a52085e070100b31b0279") @@ -217,27 +217,27 @@ 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") == PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE state = hass.states.get(f"{base_id}_humidity_status") assert state assert state.state == "normal" - assert state.attributes.get("unit_of_measurement") == "" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" state = hass.states.get(f"{base_id}_rssi_numeric") assert state assert state.state == "-64" - assert state.attributes.get("unit_of_measurement") == "dBm" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "dBm" state = hass.states.get(f"{base_id}_temperature") assert state assert state.state == "17.9" - assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS state = hass.states.get(f"{base_id}_battery_numeric") assert state assert state.state == "90" - assert state.attributes.get("unit_of_measurement") == PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert len(hass.states.async_all()) == 10 @@ -314,13 +314,13 @@ async def test_rssi_sensor(hass, rfxtrx): assert state assert state.state == "unknown" assert state.attributes.get("friendly_name") == "PT2262 22670e Rssi numeric" - assert state.attributes.get("unit_of_measurement") == "dBm" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "dBm" state = hass.states.get("sensor.ac_213c7f2_48_rssi_numeric") assert state assert state.state == "unknown" assert state.attributes.get("friendly_name") == "AC 213c7f2:48 Rssi numeric" - assert state.attributes.get("unit_of_measurement") == "dBm" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "dBm" await rfxtrx.signal("0913000022670e013b70") await rfxtrx.signal("0b1100cd0213c7f230010f71") diff --git a/tests/components/sma/test_sensor.py b/tests/components/sma/test_sensor.py index 2c317520a7d..f12af1f3849 100644 --- a/tests/components/sma/test_sensor.py +++ b/tests/components/sma/test_sensor.py @@ -2,7 +2,7 @@ import logging from homeassistant.components.sensor import DOMAIN -from homeassistant.const import VOLT +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, VOLT from homeassistant.setup import async_setup_component from tests.common import assert_setup_component @@ -28,7 +28,7 @@ async def test_sma_config(hass): state = hass.states.get("sensor.current_consumption") assert state - assert "unit_of_measurement" in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT in state.attributes assert "current_consumption" not in state.attributes state = hass.states.get("sensor.my_sensor") diff --git a/tests/components/spaceapi/test_init.py b/tests/components/spaceapi/test_init.py index b831de56b05..0f33434254c 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 PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, TEMP_CELSIUS from homeassistant.setup import async_setup_component from tests.common import mock_coro @@ -76,13 +76,13 @@ def mock_client(hass, hass_client): hass.loop.run_until_complete(async_setup_component(hass, "spaceapi", CONFIG)) hass.states.async_set( - "test.temp1", 25, attributes={"unit_of_measurement": TEMP_CELSIUS} + "test.temp1", 25, attributes={ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} ) hass.states.async_set( - "test.temp2", 23, attributes={"unit_of_measurement": TEMP_CELSIUS} + "test.temp2", 23, attributes={ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} ) hass.states.async_set( - "test.hum1", 88, attributes={"unit_of_measurement": PERCENTAGE} + "test.hum1", 88, attributes={ATTR_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 e1d658d05b7..511061933cb 100644 --- a/tests/components/startca/test_sensor.py +++ b/tests/components/startca/test_sensor.py @@ -1,7 +1,12 @@ """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, PERCENTAGE +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + DATA_GIGABYTES, + HTTP_NOT_FOUND, + PERCENTAGE, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -53,51 +58,51 @@ 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") == PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "76.24" state = hass.states.get("sensor.start_ca_usage") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_data_limit") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "400" state = hass.states.get("sensor.start_ca_used_download") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_used_upload") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_used_total") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "311.43" state = hass.states.get("sensor.start_ca_grace_download") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_grace_upload") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_grace_total") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "311.43" state = hass.states.get("sensor.start_ca_total_download") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_total_upload") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_remaining") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "95.05" @@ -149,51 +154,51 @@ 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") == PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "0" state = hass.states.get("sensor.start_ca_usage") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "0.0" state = hass.states.get("sensor.start_ca_data_limit") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "inf" state = hass.states.get("sensor.start_ca_used_download") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "0.0" state = hass.states.get("sensor.start_ca_used_upload") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "0.0" state = hass.states.get("sensor.start_ca_used_total") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "0.0" state = hass.states.get("sensor.start_ca_grace_download") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_grace_upload") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_grace_total") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "311.43" state = hass.states.get("sensor.start_ca_total_download") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_total_upload") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_remaining") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "inf" diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index e60c5c2e9a5..24401963974 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -115,7 +115,7 @@ class TestStatisticsSensor(unittest.TestCase): assert self.mean == state.attributes.get("mean") assert self.count == state.attributes.get("count") assert self.total == state.attributes.get("total") - assert TEMP_CELSIUS == state.attributes.get("unit_of_measurement") + assert TEMP_CELSIUS == state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) assert self.change == state.attributes.get("change") assert self.average_change == state.attributes.get("average_change") diff --git a/tests/components/teksavvy/test_sensor.py b/tests/components/teksavvy/test_sensor.py index b0de95d72d1..aa20beeba8b 100644 --- a/tests/components/teksavvy/test_sensor.py +++ b/tests/components/teksavvy/test_sensor.py @@ -1,7 +1,12 @@ """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, PERCENTAGE +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + DATA_GIGABYTES, + HTTP_NOT_FOUND, + PERCENTAGE, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -47,43 +52,43 @@ async def test_capped_setup(hass, aioclient_mock): await hass.async_block_till_done() state = hass.states.get("sensor.teksavvy_data_limit") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "400" state = hass.states.get("sensor.teksavvy_off_peak_download") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "36.24" state = hass.states.get("sensor.teksavvy_off_peak_upload") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "1.58" state = hass.states.get("sensor.teksavvy_off_peak_total") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "37.82" state = hass.states.get("sensor.teksavvy_on_peak_download") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "226.75" state = hass.states.get("sensor.teksavvy_on_peak_upload") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "8.82" state = hass.states.get("sensor.teksavvy_on_peak_total") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "235.57" state = hass.states.get("sensor.teksavvy_usage_ratio") - assert state.attributes.get("unit_of_measurement") == PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "56.69" state = hass.states.get("sensor.teksavvy_usage") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "226.75" state = hass.states.get("sensor.teksavvy_remaining") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "173.25" @@ -129,43 +134,43 @@ async def test_unlimited_setup(hass, aioclient_mock): await hass.async_block_till_done() state = hass.states.get("sensor.teksavvy_data_limit") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "inf" state = hass.states.get("sensor.teksavvy_off_peak_download") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "36.24" state = hass.states.get("sensor.teksavvy_off_peak_upload") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "1.58" state = hass.states.get("sensor.teksavvy_off_peak_total") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "37.82" state = hass.states.get("sensor.teksavvy_on_peak_download") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "226.75" state = hass.states.get("sensor.teksavvy_on_peak_upload") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "8.82" state = hass.states.get("sensor.teksavvy_on_peak_total") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "235.57" state = hass.states.get("sensor.teksavvy_usage") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "226.75" state = hass.states.get("sensor.teksavvy_usage_ratio") - assert state.attributes.get("unit_of_measurement") == PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "0" state = hass.states.get("sensor.teksavvy_remaining") - assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES assert state.state == "inf" diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index 7116077177a..f946ce352a5 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -12,6 +12,7 @@ from homeassistant.components.utility_meter.const import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_UNIT_OF_MEASUREMENT, ENERGY_KILO_WATT_HOUR, EVENT_HOMEASSISTANT_START, ) @@ -41,7 +42,9 @@ async def test_services(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) entity_id = config[DOMAIN]["energy_bill"]["source"] - hass.states.async_set(entity_id, 1, {"unit_of_measurement": ENERGY_KILO_WATT_HOUR}) + hass.states.async_set( + entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + ) await hass.async_block_till_done() now = dt_util.utcnow() + timedelta(seconds=10) @@ -49,7 +52,7 @@ async def test_services(hass): hass.states.async_set( entity_id, 3, - {"unit_of_measurement": ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -70,7 +73,7 @@ async def test_services(hass): hass.states.async_set( entity_id, 4, - {"unit_of_measurement": ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -91,7 +94,7 @@ async def test_services(hass): hass.states.async_set( entity_id, 5, - {"unit_of_measurement": ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index af2bbdbf6a2..1fff168b748 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.utility_meter.const import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_UNIT_OF_MEASUREMENT, ENERGY_KILO_WATT_HOUR, EVENT_HOMEASSISTANT_START, ) @@ -52,7 +53,9 @@ async def test_state(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) entity_id = config[DOMAIN]["energy_bill"]["source"] - hass.states.async_set(entity_id, 2, {"unit_of_measurement": ENERGY_KILO_WATT_HOUR}) + hass.states.async_set( + entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + ) await hass.async_block_till_done() now = dt_util.utcnow() + timedelta(seconds=10) @@ -60,7 +63,7 @@ async def test_state(hass): hass.states.async_set( entity_id, 3, - {"unit_of_measurement": ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -91,7 +94,7 @@ async def test_state(hass): hass.states.async_set( entity_id, 6, - {"unit_of_measurement": ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -145,7 +148,9 @@ async def test_net_consumption(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) entity_id = config[DOMAIN]["energy_bill"]["source"] - hass.states.async_set(entity_id, 2, {"unit_of_measurement": ENERGY_KILO_WATT_HOUR}) + hass.states.async_set( + entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + ) await hass.async_block_till_done() now = dt_util.utcnow() + timedelta(seconds=10) @@ -153,7 +158,7 @@ async def test_net_consumption(hass): hass.states.async_set( entity_id, 1, - {"unit_of_measurement": ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -178,7 +183,9 @@ async def test_non_net_consumption(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) entity_id = config[DOMAIN]["energy_bill"]["source"] - hass.states.async_set(entity_id, 2, {"unit_of_measurement": ENERGY_KILO_WATT_HOUR}) + hass.states.async_set( + entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + ) await hass.async_block_till_done() now = dt_util.utcnow() + timedelta(seconds=10) @@ -186,7 +193,7 @@ async def test_non_net_consumption(hass): hass.states.async_set( entity_id, 1, - {"unit_of_measurement": ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -224,7 +231,7 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True): with alter_time(now): async_fire_time_changed(hass, now) hass.states.async_set( - entity_id, 1, {"unit_of_measurement": ENERGY_KILO_WATT_HOUR} + entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} ) await hass.async_block_till_done() @@ -234,7 +241,7 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True): hass.states.async_set( entity_id, 3, - {"unit_of_measurement": ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -246,7 +253,7 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True): hass.states.async_set( entity_id, 6, - {"unit_of_measurement": ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py index 58e91a5581b..58cedeee450 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 PERCENTAGE +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config @@ -44,7 +44,9 @@ async def run_sensor_test( state = hass.states.get(entity_id) assert state.state == state_value if assert_unit_of_measurement: - assert state.attributes["unit_of_measurement"] == assert_unit_of_measurement + assert ( + state.attributes[ATTR_UNIT_OF_MEASUREMENT] == assert_unit_of_measurement + ) async def test_temperature_sensor_f( diff --git a/tests/components/wunderground/test_sensor.py b/tests/components/wunderground/test_sensor.py index b4fb30d25c5..8709f5b6a46 100644 --- a/tests/components/wunderground/test_sensor.py +++ b/tests/components/wunderground/test_sensor.py @@ -3,7 +3,12 @@ import aiohttp from pytest import raises import homeassistant.components.wunderground.sensor as wunderground -from homeassistant.const import LENGTH_INCHES, STATE_UNKNOWN, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + LENGTH_INCHES, + STATE_UNKNOWN, + TEMP_CELSIUS, +) from homeassistant.exceptions import PlatformNotReady from homeassistant.setup import async_setup_component @@ -90,7 +95,7 @@ async def test_sensor(hass, aioclient_mock): state = hass.states.get("sensor.pws_weather") assert state.state == "Clear" assert state.name == "Weather Summary" - assert "unit_of_measurement" not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes assert ( state.attributes["entity_picture"] == "https://icons.wxug.com/i/c/k/clear.gif" ) @@ -114,7 +119,7 @@ async def test_sensor(hass, aioclient_mock): assert state.state == "40" assert state.name == "Feels Like" assert "entity_picture" not in state.attributes - assert state.attributes["unit_of_measurement"] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS state = hass.states.get("sensor.pws_weather_1d_metric") assert state.state == "Mostly Cloudy. Fog overnight." @@ -123,7 +128,7 @@ async def test_sensor(hass, aioclient_mock): state = hass.states.get("sensor.pws_precip_1d_in") assert state.state == "0.03" assert state.name == "Precipitation Intensity Today" - assert state.attributes["unit_of_measurement"] == LENGTH_INCHES + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == LENGTH_INCHES async def test_connect_failed(hass, aioclient_mock): diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 81deb46f928..ca36d8612d4 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -8,6 +8,7 @@ import pytz from homeassistant.components import group from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, LENGTH_METERS, MASS_GRAMS, MATCH_ALL, @@ -2271,7 +2272,7 @@ def test_jinja_namespace(hass): def test_state_with_unit(hass): """Test the state_with_unit property helper.""" - hass.states.async_set("sensor.test", "23", {"unit_of_measurement": "beers"}) + hass.states.async_set("sensor.test", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"}) hass.states.async_set("sensor.test2", "wow") tpl = template.Template("{{ states.sensor.test.state_with_unit }}", hass) From 7c344fa0cdfbef881be424441cbf58ae2a388ba8 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 22 Sep 2020 00:07:08 +0000 Subject: [PATCH 281/514] [ci skip] Translation update --- .../azure_devops/translations/pl.json | 7 +++ .../components/broadlink/translations/es.json | 1 + .../components/broadlink/translations/no.json | 1 + .../components/canary/translations/de.json | 11 +++++ .../components/canary/translations/es.json | 31 ++++++++++++ .../components/canary/translations/no.json | 31 ++++++++++++ .../components/canary/translations/pl.json | 19 ++++++++ .../components/hlk_sw16/translations/pl.json | 3 ++ .../huawei_lte/translations/pl.json | 1 + .../hvv_departures/translations/de.json | 47 +++++++++++++++++++ .../components/insteon/translations/pl.json | 6 +++ .../meteo_france/translations/pl.json | 5 ++ .../moon/translations/sensor.et.json | 2 +- .../nightscout/translations/pl.json | 1 + .../components/plugwise/translations/es.json | 3 +- .../components/plugwise/translations/no.json | 5 +- .../plugwise/translations/zh-Hant.json | 5 +- .../simplisafe/translations/pl.json | 8 +++- .../components/unifi/translations/es.json | 3 +- .../components/unifi/translations/no.json | 3 +- .../zodiac/translations/sensor.ca.json | 18 +++++++ .../zodiac/translations/sensor.de.json | 18 +++++++ .../zodiac/translations/sensor.en.json | 18 +++++++ .../zodiac/translations/sensor.ru.json | 18 +++++++ .../zoneminder/translations/pl.json | 12 +++++ 25 files changed, 268 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/canary/translations/de.json create mode 100644 homeassistant/components/canary/translations/es.json create mode 100644 homeassistant/components/canary/translations/no.json create mode 100644 homeassistant/components/canary/translations/pl.json create mode 100644 homeassistant/components/hvv_departures/translations/de.json create mode 100644 homeassistant/components/zodiac/translations/sensor.ca.json create mode 100644 homeassistant/components/zodiac/translations/sensor.de.json create mode 100644 homeassistant/components/zodiac/translations/sensor.en.json create mode 100644 homeassistant/components/zodiac/translations/sensor.ru.json create mode 100644 homeassistant/components/zoneminder/translations/pl.json diff --git a/homeassistant/components/azure_devops/translations/pl.json b/homeassistant/components/azure_devops/translations/pl.json index 93ec5bd3949..1dbff873a08 100644 --- a/homeassistant/components/azure_devops/translations/pl.json +++ b/homeassistant/components/azure_devops/translations/pl.json @@ -3,6 +3,13 @@ "abort": { "already_configured": "Konto jest ju\u017c skonfigurowane", "reauth_successful": "Token dost\u0119pu pomy\u015blnie zaktualizowany" + }, + "step": { + "user": { + "data": { + "project": "Projekt" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/es.json b/homeassistant/components/broadlink/translations/es.json index fdceeabd2cd..98c4cbdfb30 100644 --- a/homeassistant/components/broadlink/translations/es.json +++ b/homeassistant/components/broadlink/translations/es.json @@ -5,6 +5,7 @@ "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", + "not_supported": "Dispositivo no compatible", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/broadlink/translations/no.json b/homeassistant/components/broadlink/translations/no.json index beeae80745d..14b55c93a96 100644 --- a/homeassistant/components/broadlink/translations/no.json +++ b/homeassistant/components/broadlink/translations/no.json @@ -5,6 +5,7 @@ "already_in_progress": "Det p\u00e5g\u00e5r allerede en konfigurasjonsflyt for denne enheten", "cannot_connect": "Tilkobling mislyktes.", "invalid_host": "Ugyldig vertsnavn eller IP-adresse", + "not_supported": "Enheten st\u00f8ttes ikke", "unknown": "Uventet feil" }, "error": { diff --git a/homeassistant/components/canary/translations/de.json b/homeassistant/components/canary/translations/de.json new file mode 100644 index 00000000000..c495accb16f --- /dev/null +++ b/homeassistant/components/canary/translations/de.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timeout": "Anfrage-Timeout (Sekunden)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/es.json b/homeassistant/components/canary/translations/es.json new file mode 100644 index 00000000000..1b881d4dcd2 --- /dev/null +++ b/homeassistant/components/canary/translations/es.json @@ -0,0 +1,31 @@ +{ + "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" + }, + "flow_title": "Canary: {name}", + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "title": "Conectar a Canary" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Par\u00e1metros pasados a ffmpeg para c\u00e1maras", + "timeout": "Tiempo de espera de la solicitud (segundos)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/no.json b/homeassistant/components/canary/translations/no.json new file mode 100644 index 00000000000..1de0a59b206 --- /dev/null +++ b/homeassistant/components/canary/translations/no.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", + "unknown": "Uventet feil" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes." + }, + "flow_title": "Canary: {name}", + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "title": "Koble til Canary" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Argumenter sendt til ffmpeg for kameraer", + "timeout": "Be om tidsavbrudd (sekunder)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/pl.json b/homeassistant/components/canary/translations/pl.json new file mode 100644 index 00000000000..1d8bfdf512d --- /dev/null +++ b/homeassistant/components/canary/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/pl.json b/homeassistant/components/hlk_sw16/translations/pl.json index 063ff23f55c..25dab56796c 100644 --- a/homeassistant/components/hlk_sw16/translations/pl.json +++ b/homeassistant/components/hlk_sw16/translations/pl.json @@ -1,5 +1,8 @@ { "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", diff --git a/homeassistant/components/huawei_lte/translations/pl.json b/homeassistant/components/huawei_lte/translations/pl.json index e38188d134f..405ffdf0343 100644 --- a/homeassistant/components/huawei_lte/translations/pl.json +++ b/homeassistant/components/huawei_lte/translations/pl.json @@ -21,6 +21,7 @@ "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/hvv_departures/translations/de.json b/homeassistant/components/hvv_departures/translations/de.json new file mode 100644 index 00000000000..e0255ec4637 --- /dev/null +++ b/homeassistant/components/hvv_departures/translations/de.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen, bitte erneut versuchen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "no_results": "Keine Ergebnisse. Versuch es mit einer anderen Station/Adresse" + }, + "step": { + "station": { + "data": { + "station": "Station/Adresse" + }, + "title": "Station/Adresse eingeben" + }, + "station_select": { + "data": { + "station": "Station/Adresse" + }, + "title": "Station/Adresse ausw\u00e4hlen" + }, + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "title": "Mit der HVV-API verbinden" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "filter": "Linien ausw\u00e4hlen", + "offset": "Versatz (Minuten)", + "real_time": "Echtzeitdaten verwenden" + }, + "description": "Optionen f\u00fcr diesen Abfahrtssensor \u00e4ndern", + "title": "Optionen" + } + } + }, + "title": "HVV Abfahrten" +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/pl.json b/homeassistant/components/insteon/translations/pl.json index f04f2298732..baf2397d00e 100644 --- a/homeassistant/components/insteon/translations/pl.json +++ b/homeassistant/components/insteon/translations/pl.json @@ -8,6 +8,12 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { + "hub2": { + "data": { + "password": "[%key_id:common::config_flow::data::password%]", + "username": "[%key_id:common::config_flow::data::username%]" + } + }, "hubv1": { "data": { "host": "Adres IP", diff --git a/homeassistant/components/meteo_france/translations/pl.json b/homeassistant/components/meteo_france/translations/pl.json index 46f3c1fdc27..f90dcaa9f89 100644 --- a/homeassistant/components/meteo_france/translations/pl.json +++ b/homeassistant/components/meteo_france/translations/pl.json @@ -5,6 +5,11 @@ "unknown": "Nieznany b\u0142\u0105d: spr\u00f3buj ponownie p\u00f3\u017aniej." }, "step": { + "cities": { + "data": { + "city": "Miasto" + } + }, "user": { "data": { "city": "Miasto" diff --git a/homeassistant/components/moon/translations/sensor.et.json b/homeassistant/components/moon/translations/sensor.et.json index 926e20adbb4..e1477727868 100644 --- a/homeassistant/components/moon/translations/sensor.et.json +++ b/homeassistant/components/moon/translations/sensor.et.json @@ -5,7 +5,7 @@ "full_moon": "T\u00e4iskuu", "last_quarter": "Kahanev poolkuu", "new_moon": "Kuu loomine", - "waning_crescent": "Kahanev kuu", + "waning_crescent": "Vanakuu", "waning_gibbous": "Kahanev kuu", "waxing_crescent": "Noorkuu", "waxing_gibbous": "Kasvav kuu" diff --git a/homeassistant/components/nightscout/translations/pl.json b/homeassistant/components/nightscout/translations/pl.json index c931afdae8d..13841997094 100644 --- a/homeassistant/components/nightscout/translations/pl.json +++ b/homeassistant/components/nightscout/translations/pl.json @@ -4,6 +4,7 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown": "Nieoczekiwany b\u0142\u0105d" } } diff --git a/homeassistant/components/plugwise/translations/es.json b/homeassistant/components/plugwise/translations/es.json index 9236556b64d..9ab00348f81 100644 --- a/homeassistant/components/plugwise/translations/es.json +++ b/homeassistant/components/plugwise/translations/es.json @@ -13,7 +13,8 @@ "user": { "data": { "host": "Direcci\u00f3n IP de Smile", - "password": "ID Smile" + "password": "ID Smile", + "port": "N\u00famero de puerto de Smile" }, "description": "Detalles", "title": "Conectarse a Smile" diff --git a/homeassistant/components/plugwise/translations/no.json b/homeassistant/components/plugwise/translations/no.json index 2ee14f0e152..4902ada06c2 100644 --- a/homeassistant/components/plugwise/translations/no.json +++ b/homeassistant/components/plugwise/translations/no.json @@ -13,9 +13,10 @@ "user": { "data": { "host": "Smile IP-adresse", - "password": "" + "password": "", + "port": "Smil portnummer" }, - "description": "Detaljer", + "description": "Vennligst skriv inn:", "title": "Koble til Smile" } } diff --git a/homeassistant/components/plugwise/translations/zh-Hant.json b/homeassistant/components/plugwise/translations/zh-Hant.json index ee6966c1bdb..58dd71266b3 100644 --- a/homeassistant/components/plugwise/translations/zh-Hant.json +++ b/homeassistant/components/plugwise/translations/zh-Hant.json @@ -13,9 +13,10 @@ "user": { "data": { "host": "Smile IP \u4f4d\u5740", - "password": "Smile ID" + "password": "Smile ID", + "port": "Smile \u901a\u8a0a\u57e0" }, - "description": "\u8a73\u7d30\u8cc7\u8a0a", + "description": "\u8acb\u8f38\u5165\u8cc7\u8a0a\uff1a", "title": "\u9023\u7dda\u81f3 Smile" } } diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json index 0562222eea3..955dc5e311e 100644 --- a/homeassistant/components/simplisafe/translations/pl.json +++ b/homeassistant/components/simplisafe/translations/pl.json @@ -5,9 +5,15 @@ }, "error": { "identifier_exists": "Konto jest ju\u017c zarejestrowane.", - "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + } + }, "user": { "data": { "code": "Kod (u\u017cywany w interfejsie Home Assistanta)", diff --git a/homeassistant/components/unifi/translations/es.json b/homeassistant/components/unifi/translations/es.json index da67cd1bcaf..35b1d3d6e29 100644 --- a/homeassistant/components/unifi/translations/es.json +++ b/homeassistant/components/unifi/translations/es.json @@ -54,7 +54,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Sensores de uso de ancho de banda para los clientes de la red" + "allow_bandwidth_sensors": "Sensores de uso de ancho de banda para los clientes de la red", + "allow_uptime_sensors": "Sensores de tiempo de actividad para clientes de la red" }, "description": "Configurar estad\u00edsticas de los sensores", "title": "Opciones UniFi 3/3" diff --git a/homeassistant/components/unifi/translations/no.json b/homeassistant/components/unifi/translations/no.json index a861790ba8d..fa0d29c5a88 100644 --- a/homeassistant/components/unifi/translations/no.json +++ b/homeassistant/components/unifi/translations/no.json @@ -60,7 +60,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "B\u00e5ndbreddebrukssensorer for nettverksklienter" + "allow_bandwidth_sensors": "B\u00e5ndbreddebrukssensorer for nettverksklienter", + "allow_uptime_sensors": "Oppetidssensorer for nettverksklienter" }, "description": "Konfigurer statistikk sensorer", "title": "UniFi-alternativ 3/3" diff --git a/homeassistant/components/zodiac/translations/sensor.ca.json b/homeassistant/components/zodiac/translations/sensor.ca.json new file mode 100644 index 00000000000..f4699838cde --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.ca.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Aquari", + "aries": "\u00c0ries", + "cancer": "C\u00e0ncer", + "capricorn": "Capricorn", + "gemini": "Bessons", + "leo": "Lle\u00f3", + "libra": "Balan\u00e7a", + "pisces": "Peixos", + "sagittarius": "Sagitari", + "scorpio": "Escorp\u00ed", + "taurus": "Taure", + "virgo": "Verge" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/sensor.de.json b/homeassistant/components/zodiac/translations/sensor.de.json new file mode 100644 index 00000000000..d60bd068b89 --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.de.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Wassermann", + "aries": "Widder", + "cancer": "Krebs", + "capricorn": "Steinbock", + "gemini": "Zwillinge", + "leo": "L\u00f6we", + "libra": "Waage", + "pisces": "Fische", + "sagittarius": "Sch\u00fctze", + "scorpio": "Skorpion", + "taurus": "Stier", + "virgo": "Jungfrau" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/sensor.en.json b/homeassistant/components/zodiac/translations/sensor.en.json new file mode 100644 index 00000000000..cd671e146ed --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.en.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Aquarius", + "aries": "Aries", + "cancer": "Cancer", + "capricorn": "Capricorn", + "gemini": "Gemini", + "leo": "Leo", + "libra": "Libra", + "pisces": "Pisces", + "sagittarius": "Sagittarius", + "scorpio": "Scorpio", + "taurus": "Taurus", + "virgo": "Virgo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/sensor.ru.json b/homeassistant/components/zodiac/translations/sensor.ru.json new file mode 100644 index 00000000000..3a314918428 --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.ru.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "\u0412\u043e\u0434\u043e\u043b\u0435\u0439", + "aries": "\u041e\u0432\u0435\u043d", + "cancer": "\u0420\u0430\u043a", + "capricorn": "\u041a\u043e\u0437\u0435\u0440\u043e\u0433", + "gemini": "\u0411\u043b\u0438\u0437\u043d\u0435\u0446\u044b", + "leo": "\u041b\u0435\u0432", + "libra": "\u0412\u0435\u0441\u044b", + "pisces": "\u0420\u044b\u0431\u044b", + "sagittarius": "\u0421\u0442\u0440\u0435\u043b\u0435\u0446", + "scorpio": "\u0421\u043a\u043e\u0440\u043f\u0438\u043e\u043d", + "taurus": "\u0422\u0435\u043b\u0435\u0446", + "virgo": "\u0414\u0435\u0432\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/pl.json b/homeassistant/components/zoneminder/translations/pl.json new file mode 100644 index 00000000000..b8b737c37a3 --- /dev/null +++ b/homeassistant/components/zoneminder/translations/pl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file From b3691d5d907cafabf537d54c61789f2e513089e0 Mon Sep 17 00:00:00 2001 From: Brett Date: Tue, 22 Sep 2020 11:01:52 +1000 Subject: [PATCH 282/514] Improve timeout error handling for Splunk (#40384) * Bugfix to catch asyncio.timeout * Better asyncio.timeout warning message --- homeassistant/components/splunk/__init__.py | 3 +++ homeassistant/components/splunk/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/splunk/__init__.py b/homeassistant/components/splunk/__init__.py index ec1471a2272..a3ec307d67b 100644 --- a/homeassistant/components/splunk/__init__.py +++ b/homeassistant/components/splunk/__init__.py @@ -1,4 +1,5 @@ """Support to send data to a Splunk instance.""" +import asyncio import json import logging import time @@ -116,6 +117,8 @@ async def async_setup(hass, config): _LOGGER.warning(err) except ClientConnectionError as err: _LOGGER.warning(err) + except asyncio.TimeoutError: + _LOGGER.warning("Connection to %s:%s timed out", host, port) except ClientResponseError as err: _LOGGER.error(err.message) diff --git a/homeassistant/components/splunk/manifest.json b/homeassistant/components/splunk/manifest.json index aaddac2609d..d51d6c712de 100644 --- a/homeassistant/components/splunk/manifest.json +++ b/homeassistant/components/splunk/manifest.json @@ -3,7 +3,7 @@ "name": "Splunk", "documentation": "https://www.home-assistant.io/integrations/splunk", "requirements": [ - "hass_splunk==0.1.0" + "hass_splunk==0.1.1" ], "codeowners": [ "@Bre77" diff --git a/requirements_all.txt b/requirements_all.txt index 23c7a950fe3..ba2c86a00eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -723,7 +723,7 @@ hangups==0.4.11 hass-nabucasa==0.37.0 # homeassistant.components.splunk -hass_splunk==0.1.0 +hass_splunk==0.1.1 # homeassistant.components.jewish_calendar hdate==0.9.5 From f34455b6c24bc21d95749d89c4c65725168e617b Mon Sep 17 00:00:00 2001 From: Eugene Prystupa Date: Mon, 21 Sep 2020 22:38:43 -0400 Subject: [PATCH 283/514] Add debug logging to Bond fireplace entity (#40318) * Add logging to Bond fireplace entity for troubleshooting turn off problem * Update homeassistant/components/bond/light.py Co-authored-by: Chris Talkington * Update homeassistant/components/bond/light.py Co-authored-by: Chris Talkington Co-authored-by: Chris Talkington --- homeassistant/components/bond/light.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index 7dec44dbb38..af308891a06 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -1,4 +1,5 @@ """Support for Bond lights.""" +import logging from typing import Any, Callable, List, Optional from bond_api import Action, DeviceType @@ -17,6 +18,8 @@ from .const import DOMAIN from .entity import BondEntity from .utils import BondDevice +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistant, @@ -96,7 +99,7 @@ class BondFireplace(BondEntity, LightEntity): """Representation of a Bond-controlled fireplace.""" def __init__(self, hub: BondHub, device: BondDevice): - """Create HA entity representing Bond fan.""" + """Create HA entity representing Bond fireplace.""" super().__init__(hub, device) self._power: Optional[bool] = None @@ -119,6 +122,8 @@ class BondFireplace(BondEntity, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the fireplace on.""" + _LOGGER.debug("fireplace async_turn_on called with: %s", kwargs) + brightness = kwargs.get(ATTR_BRIGHTNESS) if brightness: flame = round((brightness * 100) / 255) @@ -128,6 +133,8 @@ class BondFireplace(BondEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the fireplace off.""" + _LOGGER.debug("fireplace async_turn_off called with: %s", kwargs) + await self._hub.bond.action(self._device.device_id, Action.turn_off()) @property From 8ad7b68c9e72ab7ad6bedcefdc4edb74ffe3fb06 Mon Sep 17 00:00:00 2001 From: Harrison Pace Date: Tue, 22 Sep 2020 17:01:58 +1000 Subject: [PATCH 284/514] Use Cloud State as alternative state if condition unknown (#37121) --- homeassistant/components/bom/weather.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bom/weather.py b/homeassistant/components/bom/weather.py index 94b9960c851..9229d0c11d4 100644 --- a/homeassistant/components/bom/weather.py +++ b/homeassistant/components/bom/weather.py @@ -54,7 +54,9 @@ class BOMWeather(WeatherEntity): @property def condition(self): """Return the current condition.""" - return self.bom_data.get_reading("weather") + return self.bom_data.get_reading("weather") or self.bom_data.get_reading( + "cloud" + ) # Now implement the WeatherEntity interface From 50b727ba8310c2543d7be5778e2834fe39a13657 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 22 Sep 2020 09:32:09 +0200 Subject: [PATCH 285/514] Add PLAY and PAUSE to webos button service description (#40353) --- 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 5719e339793..430916f7c71 100644 --- a/homeassistant/components/webostv/services.yaml +++ b/homeassistant/components/webostv/services.yaml @@ -11,7 +11,7 @@ button: Name of the button to press. Known possible values are 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 + PLAY, PAUSE, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 example: "LEFT" command: From 0582bf7746e728d0b7633937cb632e4dc6bc9179 Mon Sep 17 00:00:00 2001 From: Perry Naseck Date: Tue, 22 Sep 2020 03:44:16 -0400 Subject: [PATCH 286/514] Firmata analog input, PWM/analog output, deprecate arduino (#40369) * firmata analog input * firmata pwm/analog out, use more HA const * firmata update pymata to 1.19 * deprecate arduino, firmata supersedes it * firmata sensor diff min, pull review quality changes * firmata condense platform setup into loop --- .coveragerc | 2 + homeassistant/components/arduino/__init__.py | 6 + homeassistant/components/firmata/__init__.py | 79 ++++++++---- .../components/firmata/binary_sensor.py | 6 +- homeassistant/components/firmata/board.py | 22 +++- homeassistant/components/firmata/const.py | 23 +++- homeassistant/components/firmata/light.py | 98 ++++++++++++++ .../components/firmata/manifest.json | 2 +- homeassistant/components/firmata/pin.py | 120 +++++++++++++++++- homeassistant/components/firmata/sensor.py | 59 +++++++++ homeassistant/components/firmata/switch.py | 15 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 13 files changed, 379 insertions(+), 57 deletions(-) create mode 100644 homeassistant/components/firmata/light.py create mode 100644 homeassistant/components/firmata/sensor.py diff --git a/.coveragerc b/.coveragerc index c1fe30c6f42..773bc9fb0f6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -269,7 +269,9 @@ omit = homeassistant/components/firmata/board.py homeassistant/components/firmata/const.py homeassistant/components/firmata/entity.py + homeassistant/components/firmata/light.py homeassistant/components/firmata/pin.py + homeassistant/components/firmata/sensor.py homeassistant/components/firmata/switch.py homeassistant/components/fitbit/sensor.py homeassistant/components/fixer/sensor.py diff --git a/homeassistant/components/arduino/__init__.py b/homeassistant/components/arduino/__init__.py index e87a625522e..2890fd4abda 100644 --- a/homeassistant/components/arduino/__init__.py +++ b/homeassistant/components/arduino/__init__.py @@ -23,6 +23,12 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Arduino component.""" + _LOGGER.warning( + "The %s integration has been deprecated. Please move your " + "configuration to the firmata integration. " + "https://www.home-assistant.io/integrations/firmata", + DOMAIN, + ) port = config[DOMAIN][CONF_PORT] diff --git a/homeassistant/components/firmata/__init__.py b/homeassistant/components/firmata/__init__.py index b64a88cbf57..c0394a95a49 100644 --- a/homeassistant/components/firmata/__init__.py +++ b/homeassistant/components/firmata/__init__.py @@ -6,7 +6,17 @@ import logging import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + CONF_BINARY_SENSORS, + CONF_LIGHTS, + CONF_MAXIMUM, + CONF_MINIMUM, + CONF_NAME, + CONF_PIN, + CONF_SENSORS, + CONF_SWITCHES, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, device_registry as dr @@ -14,21 +24,22 @@ from .board import FirmataBoard from .const import ( CONF_ARDUINO_INSTANCE_ID, CONF_ARDUINO_WAIT, - CONF_BINARY_SENSORS, + CONF_DIFFERENTIAL, CONF_INITIAL_STATE, CONF_NEGATE_STATE, - CONF_PIN, CONF_PIN_MODE, + CONF_PLATFORM_MAP, CONF_SAMPLING_INTERVAL, CONF_SERIAL_BAUD_RATE, CONF_SERIAL_PORT, CONF_SLEEP_TUNE, - CONF_SWITCHES, DOMAIN, FIRMATA_MANUFACTURER, + PIN_MODE_ANALOG, PIN_MODE_INPUT, PIN_MODE_OUTPUT, PIN_MODE_PULLUP, + PIN_MODE_PWM, ) _LOGGER = logging.getLogger(__name__) @@ -40,8 +51,8 @@ ANALOG_PIN_SCHEMA = vol.All(cv.string, vol.Match(r"^A[0-9]+$")) SWITCH_SCHEMA = vol.Schema( { vol.Required(CONF_NAME): cv.string, + # Both digital and analog pins may be used as digital output vol.Required(CONF_PIN): vol.Any(cv.positive_int, ANALOG_PIN_SCHEMA), - # will be analog mode in future too vol.Required(CONF_PIN_MODE): PIN_MODE_OUTPUT, vol.Optional(CONF_INITIAL_STATE, default=False): cv.boolean, vol.Optional(CONF_NEGATE_STATE, default=False): cv.boolean, @@ -49,17 +60,45 @@ SWITCH_SCHEMA = vol.Schema( required=True, ) +LIGHT_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + # Both digital and analog pins may be used as PWM/analog output + vol.Required(CONF_PIN): vol.Any(cv.positive_int, ANALOG_PIN_SCHEMA), + vol.Required(CONF_PIN_MODE): PIN_MODE_PWM, + vol.Optional(CONF_INITIAL_STATE, default=0): cv.positive_int, + vol.Optional(CONF_MINIMUM, default=0): cv.positive_int, + vol.Optional(CONF_MAXIMUM, default=255): cv.positive_int, + }, + required=True, +) + BINARY_SENSOR_SCHEMA = vol.Schema( { vol.Required(CONF_NAME): cv.string, + # Both digital and analog pins may be used as digital input vol.Required(CONF_PIN): vol.Any(cv.positive_int, ANALOG_PIN_SCHEMA), - # will be analog mode in future too vol.Required(CONF_PIN_MODE): vol.Any(PIN_MODE_INPUT, PIN_MODE_PULLUP), vol.Optional(CONF_NEGATE_STATE, default=False): cv.boolean, }, required=True, ) +SENSOR_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + # Currently only analog input sensor is implemented + vol.Required(CONF_PIN): ANALOG_PIN_SCHEMA, + vol.Required(CONF_PIN_MODE): PIN_MODE_ANALOG, + # Default differential is 40 to avoid a flood of messages on initial setup + # in case pin is unplugged. Firmata responds really really fast + vol.Optional(CONF_DIFFERENTIAL, default=40): vol.All( + cv.positive_int, vol.Range(min=1) + ), + }, + required=True, +) + BOARD_CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_SERIAL_PORT): cv.string, @@ -71,7 +110,9 @@ BOARD_CONFIG_SCHEMA = vol.Schema( ), vol.Optional(CONF_SAMPLING_INTERVAL): cv.positive_int, vol.Optional(CONF_SWITCHES): [SWITCH_SCHEMA], + vol.Optional(CONF_LIGHTS): [LIGHT_SCHEMA], vol.Optional(CONF_BINARY_SENSORS): [BINARY_SENSOR_SCHEMA], + vol.Optional(CONF_SENSORS): [SENSOR_SCHEMA], }, required=True, ) @@ -155,14 +196,11 @@ async def async_setup_entry( sw_version=board.firmware_version, ) - if CONF_BINARY_SENSORS in config_entry.data: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") - ) - if CONF_SWITCHES in config_entry.data: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "switch") - ) + for (conf, platform) in CONF_PLATFORM_MAP.items(): + if conf in config_entry.data: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, platform) + ) return True @@ -173,16 +211,11 @@ async def async_unload_entry( _LOGGER.debug("Closing Firmata board %s", config_entry.data[CONF_NAME]) unload_entries = [] - if CONF_BINARY_SENSORS in config_entry.data: - unload_entries.append( - hass.config_entries.async_forward_entry_unload( - config_entry, "binary_sensor" + for (conf, platform) in CONF_PLATFORM_MAP.items(): + if conf in config_entry.data: + unload_entries.append( + hass.config_entries.async_forward_entry_unload(config_entry, platform) ) - ) - if CONF_SWITCHES in config_entry.data: - unload_entries.append( - hass.config_entries.async_forward_entry_unload(config_entry, "switch") - ) results = [] if unload_entries: results = await asyncio.gather(*unload_entries) diff --git a/homeassistant/components/firmata/binary_sensor.py b/homeassistant/components/firmata/binary_sensor.py index 4576b8dc69e..c2708fc2753 100644 --- a/homeassistant/components/firmata/binary_sensor.py +++ b/homeassistant/components/firmata/binary_sensor.py @@ -4,10 +4,10 @@ import logging from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PIN from homeassistant.core import HomeAssistant -from .const import CONF_NEGATE_STATE, CONF_PIN, CONF_PIN_MODE, DOMAIN +from .const import CONF_NEGATE_STATE, CONF_PIN_MODE, DOMAIN from .entity import FirmataPinEntity from .pin import FirmataBinaryDigitalInput, FirmataPinUsedException @@ -30,7 +30,7 @@ async def async_setup_entry( api.setup() except FirmataPinUsedException: _LOGGER.error( - "Could not setup binary sensor on pin %s since pin already in use.", + "Could not setup binary sensor on pin %s since pin already in use", binary_sensor[CONF_PIN], ) continue diff --git a/homeassistant/components/firmata/board.py b/homeassistant/components/firmata/board.py index bae30014d63..73e3c004cb9 100644 --- a/homeassistant/components/firmata/board.py +++ b/homeassistant/components/firmata/board.py @@ -5,17 +5,23 @@ from typing import Union from pymata_express.pymata_express import PymataExpress from pymata_express.pymata_express_serial import serial -from homeassistant.const import CONF_NAME +from homeassistant.const import ( + CONF_BINARY_SENSORS, + CONF_LIGHTS, + CONF_NAME, + CONF_SENSORS, + CONF_SWITCHES, +) from .const import ( CONF_ARDUINO_INSTANCE_ID, CONF_ARDUINO_WAIT, - CONF_BINARY_SENSORS, CONF_SAMPLING_INTERVAL, CONF_SERIAL_BAUD_RATE, CONF_SERIAL_PORT, CONF_SLEEP_TUNE, - CONF_SWITCHES, + PIN_TYPE_ANALOG, + PIN_TYPE_DIGITAL, ) _LOGGER = logging.getLogger(__name__) @@ -34,13 +40,19 @@ class FirmataBoard: self.protocol_version = None self.name = self.config[CONF_NAME] self.switches = [] + self.lights = [] self.binary_sensors = [] + self.sensors = [] self.used_pins = [] if CONF_SWITCHES in self.config: self.switches = self.config[CONF_SWITCHES] + if CONF_LIGHTS in self.config: + self.lights = self.config[CONF_LIGHTS] if CONF_BINARY_SENSORS in self.config: self.binary_sensors = self.config[CONF_BINARY_SENSORS] + if CONF_SENSORS in self.config: + self.sensors = self.config[CONF_SENSORS] async def async_setup(self, tries=0) -> bool: """Set up a Firmata instance.""" @@ -109,11 +121,11 @@ board %s: %s", def get_pin_type(self, pin: FirmataPinType) -> tuple: """Return the type and Firmata location of a pin on the board.""" if isinstance(pin, str): - pin_type = "analog" + pin_type = PIN_TYPE_ANALOG firmata_pin = int(pin[1:]) firmata_pin += self.api.first_analog_pin else: - pin_type = "digital" + pin_type = PIN_TYPE_DIGITAL firmata_pin = pin return (pin_type, firmata_pin) diff --git a/homeassistant/components/firmata/const.py b/homeassistant/components/firmata/const.py index 1ad3cbb8423..6259582b5f7 100644 --- a/homeassistant/components/firmata/const.py +++ b/homeassistant/components/firmata/const.py @@ -1,24 +1,35 @@ """Constants for the Firmata component.""" -import logging - -LOGGER = logging.getLogger(__package__) +from homeassistant.const import ( + CONF_BINARY_SENSORS, + CONF_LIGHTS, + CONF_SENSORS, + CONF_SWITCHES, +) CONF_ARDUINO_INSTANCE_ID = "arduino_instance_id" CONF_ARDUINO_WAIT = "arduino_wait" -CONF_BINARY_SENSORS = "binary_sensors" +CONF_DIFFERENTIAL = "differential" CONF_INITIAL_STATE = "initial" CONF_NAME = "name" CONF_NEGATE_STATE = "negate" -CONF_PIN = "pin" CONF_PINS = "pins" CONF_PIN_MODE = "pin_mode" +PIN_MODE_ANALOG = "ANALOG" PIN_MODE_OUTPUT = "OUTPUT" +PIN_MODE_PWM = "PWM" PIN_MODE_INPUT = "INPUT" PIN_MODE_PULLUP = "PULLUP" +PIN_TYPE_ANALOG = 1 +PIN_TYPE_DIGITAL = 0 CONF_SAMPLING_INTERVAL = "sampling_interval" CONF_SERIAL_BAUD_RATE = "serial_baud_rate" CONF_SERIAL_PORT = "serial_port" CONF_SLEEP_TUNE = "sleep_tune" -CONF_SWITCHES = "switches" DOMAIN = "firmata" FIRMATA_MANUFACTURER = "Firmata" +CONF_PLATFORM_MAP = { + CONF_BINARY_SENSORS: "binary_sensor", + CONF_LIGHTS: "light", + CONF_SENSORS: "sensor", + CONF_SWITCHES: "switch", +} diff --git a/homeassistant/components/firmata/light.py b/homeassistant/components/firmata/light.py new file mode 100644 index 00000000000..e95b5101413 --- /dev/null +++ b/homeassistant/components/firmata/light.py @@ -0,0 +1,98 @@ +"""Support for Firmata light output.""" + +import logging +from typing import Type + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_MAXIMUM, CONF_MINIMUM, CONF_NAME, CONF_PIN +from homeassistant.core import HomeAssistant + +from .board import FirmataPinType +from .const import CONF_INITIAL_STATE, CONF_PIN_MODE, DOMAIN +from .entity import FirmataPinEntity +from .pin import FirmataBoardPin, FirmataPinUsedException, FirmataPWMOutput + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up the Firmata lights.""" + new_entities = [] + + board = hass.data[DOMAIN][config_entry.entry_id] + for light in board.lights: + pin = light[CONF_PIN] + pin_mode = light[CONF_PIN_MODE] + initial = light[CONF_INITIAL_STATE] + minimum = light[CONF_MINIMUM] + maximum = light[CONF_MAXIMUM] + api = FirmataPWMOutput(board, pin, pin_mode, initial, minimum, maximum) + try: + api.setup() + except FirmataPinUsedException: + _LOGGER.error( + "Could not setup light on pin %s since pin already in use", + light[CONF_PIN], + ) + continue + name = light[CONF_NAME] + light_entity = FirmataLight(api, config_entry, name, pin) + new_entities.append(light_entity) + + if new_entities: + async_add_entities(new_entities) + + +class FirmataLight(FirmataPinEntity, LightEntity): + """Representation of a light on a Firmata board.""" + + def __init__( + self, + api: Type[FirmataBoardPin], + config_entry: ConfigEntry, + name: str, + pin: FirmataPinType, + ): + """Initialize the light pin entity.""" + super().__init__(api, config_entry, name, pin) + + # Default first turn on to max + self._last_on_level = 255 + + async def async_added_to_hass(self) -> None: + """Set up a light.""" + await self._api.start_pin() + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return self._api.state > 0 + + @property + def brightness(self) -> int: + """Return the brightness of the light.""" + return self._api.state + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + async def async_turn_on(self, **kwargs) -> None: + """Turn on light.""" + level = kwargs.get(ATTR_BRIGHTNESS, self._last_on_level) + await self._api.set_level(level) + self.async_write_ha_state() + self._last_on_level = level + + async def async_turn_off(self, **kwargs) -> None: + """Turn off light.""" + await self._api.set_level(0) + self.async_write_ha_state() diff --git a/homeassistant/components/firmata/manifest.json b/homeassistant/components/firmata/manifest.json index d894c0a440b..8b283c4f81d 100644 --- a/homeassistant/components/firmata/manifest.json +++ b/homeassistant/components/firmata/manifest.json @@ -4,7 +4,7 @@ "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/firmata", "requirements": [ - "pymata-express==1.13" + "pymata-express==1.19" ], "codeowners": [ "@DaAwesomeP" diff --git a/homeassistant/components/firmata/pin.py b/homeassistant/components/firmata/pin.py index 644986fb66c..3259d76cbb3 100644 --- a/homeassistant/components/firmata/pin.py +++ b/homeassistant/components/firmata/pin.py @@ -2,10 +2,8 @@ import logging from typing import Callable -from homeassistant.core import callback - from .board import FirmataBoard, FirmataPinType -from .const import PIN_MODE_INPUT, PIN_MODE_PULLUP +from .const import PIN_MODE_INPUT, PIN_MODE_PULLUP, PIN_TYPE_ANALOG _LOGGER = logging.getLogger(__name__) @@ -25,6 +23,10 @@ class FirmataBoardPin: self._pin_type, self._firmata_pin = self.board.get_pin_type(self._pin) self._state = None + if self._pin_type == PIN_TYPE_ANALOG: + # Pymata wants the analog pin formatted as the # from "A#" + self._analog_pin = int(self._pin[1:]) + def setup(self): """Set up a pin and make sure it is valid.""" if not self.board.mark_pin_used(self._pin): @@ -85,6 +87,53 @@ class FirmataBinaryDigitalOutput(FirmataBoardPin): self._state = False +class FirmataPWMOutput(FirmataBoardPin): + """Representation of a Firmata PWM/analog Output Pin.""" + + def __init__( + self, + board: FirmataBoard, + pin: FirmataPinType, + pin_mode: str, + initial: bool, + minimum: int, + maximum: int, + ): + """Initialize the PWM/analog output pin.""" + self._initial = initial + self._min = minimum + self._max = maximum + self._range = self._max - self._min + super().__init__(board, pin, pin_mode) + + async def start_pin(self) -> None: + """Set initial state on a pin.""" + _LOGGER.debug( + "Setting initial state for PWM/analog output pin %s on board %s to %d", + self._pin, + self.board.name, + self._initial, + ) + api = self.board.api + await api.set_pin_mode_pwm_output(self._firmata_pin) + + new_pin_state = round((self._initial * self._range) / 255) + self._min + await api.pwm_write(self._firmata_pin, new_pin_state) + self._state = self._initial + + @property + def state(self) -> int: + """Return PWM/analog state.""" + return self._state + + async def set_level(self, level: int) -> None: + """Set PWM/analog output.""" + _LOGGER.debug("Setting PWM/analog output on pin %s to %d", self._pin, level) + new_pin_state = round((level * self._range) / 255) + self._min + await self.board.api.pwm_write(self._firmata_pin, new_pin_state) + self._state = level + + class FirmataBinaryDigitalInput(FirmataBoardPin): """Representation of a Firmata Digital Input Pin.""" @@ -99,7 +148,7 @@ class FirmataBinaryDigitalInput(FirmataBoardPin): async def start_pin(self, forward_callback: Callable[[], None]) -> None: """Get initial state and start reporting a pin.""" _LOGGER.debug( - "Starting reporting updates for input pin %s on board %s", + "Starting reporting updates for digital input pin %s on board %s", self._pin, self.board.name, ) @@ -133,7 +182,6 @@ class FirmataBinaryDigitalInput(FirmataBoardPin): """Return true if digital input is on.""" return self._state - @callback async def latch_callback(self, data: list) -> None: """Update pin state on callback.""" if data[1] != self._firmata_pin: @@ -151,3 +199,65 @@ class FirmataBinaryDigitalInput(FirmataBoardPin): return self._state = new_state self._forward_callback() + + +class FirmataAnalogInput(FirmataBoardPin): + """Representation of a Firmata Analog Input Pin.""" + + def __init__( + self, board: FirmataBoard, pin: FirmataPinType, pin_mode: str, differential: int + ): + """Initialize the analog input pin.""" + self._differential = differential + self._forward_callback = None + super().__init__(board, pin, pin_mode) + + async def start_pin(self, forward_callback: Callable[[], None]) -> None: + """Get initial state and start reporting a pin.""" + _LOGGER.debug( + "Starting reporting updates for analog input pin %s on board %s", + self._pin, + self.board.name, + ) + self._forward_callback = forward_callback + api = self.board.api + # Only PIN_MODE_ANALOG_INPUT mode is supported as sensor input + await api.set_pin_mode_analog_input( + self._analog_pin, self.latch_callback, self._differential + ) + + self._state = (await self.board.api.analog_read(self._analog_pin))[0] + + self._forward_callback() + + async def stop_pin(self) -> None: + """Stop reporting analog input pin.""" + _LOGGER.debug( + "Stopping reporting updates for analog input pin %s on board %s", + self._pin, + self.board.name, + ) + api = self.board.api + await api.disable_analog_reporting(self._analog_pin) + + @property + def state(self) -> int: + """Return sensor state.""" + return self._state + + async def latch_callback(self, data: list) -> None: + """Update pin state on callback.""" + if data[1] != self._analog_pin: + return + _LOGGER.debug( + "Received latch %d for analog input pin %s on board %s", + data[2], + self._pin, + self.board.name, + ) + new_state = data[2] + if self._state == new_state: + _LOGGER.debug("stopping") + return + self._state = new_state + self._forward_callback() diff --git a/homeassistant/components/firmata/sensor.py b/homeassistant/components/firmata/sensor.py new file mode 100644 index 00000000000..cb9db1f11e5 --- /dev/null +++ b/homeassistant/components/firmata/sensor.py @@ -0,0 +1,59 @@ +"""Support for Firmata sensor input.""" + +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME, CONF_PIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity + +from .const import CONF_DIFFERENTIAL, CONF_PIN_MODE, DOMAIN +from .entity import FirmataPinEntity +from .pin import FirmataAnalogInput, FirmataPinUsedException + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up the Firmata sensors.""" + new_entities = [] + + board = hass.data[DOMAIN][config_entry.entry_id] + for sensor in board.sensors: + pin = sensor[CONF_PIN] + pin_mode = sensor[CONF_PIN_MODE] + differential = sensor[CONF_DIFFERENTIAL] + api = FirmataAnalogInput(board, pin, pin_mode, differential) + try: + api.setup() + except FirmataPinUsedException: + _LOGGER.error( + "Could not setup sensor on pin %s since pin already in use", + sensor[CONF_PIN], + ) + continue + name = sensor[CONF_NAME] + sensor_entity = FirmataSensor(api, config_entry, name, pin) + new_entities.append(sensor_entity) + + if new_entities: + async_add_entities(new_entities) + + +class FirmataSensor(FirmataPinEntity, Entity): + """Representation of a sensor on a Firmata board.""" + + async def async_added_to_hass(self) -> None: + """Set up a sensor.""" + await self._api.start_pin(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Stop reporting a sensor.""" + await self._api.stop_pin() + + @property + def state(self) -> int: + """Return sensor state.""" + return self._api.state diff --git a/homeassistant/components/firmata/switch.py b/homeassistant/components/firmata/switch.py index ab67a6d6840..f1aaf3357c0 100644 --- a/homeassistant/components/firmata/switch.py +++ b/homeassistant/components/firmata/switch.py @@ -4,16 +4,10 @@ import logging from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PIN from homeassistant.core import HomeAssistant -from .const import ( - CONF_INITIAL_STATE, - CONF_NEGATE_STATE, - CONF_PIN, - CONF_PIN_MODE, - DOMAIN, -) +from .const import CONF_INITIAL_STATE, CONF_NEGATE_STATE, CONF_PIN_MODE, DOMAIN from .entity import FirmataPinEntity from .pin import FirmataBinaryDigitalOutput, FirmataPinUsedException @@ -37,7 +31,7 @@ async def async_setup_entry( api.setup() except FirmataPinUsedException: _LOGGER.error( - "Could not setup switch on pin %s since pin already in use.", + "Could not setup switch on pin %s since pin already in use", switch[CONF_PIN], ) continue @@ -55,7 +49,6 @@ class FirmataSwitch(FirmataPinEntity, SwitchEntity): async def async_added_to_hass(self) -> None: """Set up a switch.""" await self._api.start_pin() - self.async_write_ha_state() @property def is_on(self) -> bool: @@ -64,12 +57,10 @@ class FirmataSwitch(FirmataPinEntity, SwitchEntity): async def async_turn_on(self, **kwargs) -> None: """Turn on switch.""" - _LOGGER.debug("Turning switch %s on", self._name) await self._api.turn_on() self.async_write_ha_state() async def async_turn_off(self, **kwargs) -> None: """Turn off switch.""" - _LOGGER.debug("Turning switch %s off", self._name) await self._api.turn_off() self.async_write_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index ba2c86a00eb..c630670b08a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1470,7 +1470,7 @@ pylutron==0.2.5 pymailgunner==1.4 # homeassistant.components.firmata -pymata-express==1.13 +pymata-express==1.19 # homeassistant.components.mediaroom pymediaroom==0.6.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b8e96dc2b0..4ba319cd732 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -713,7 +713,7 @@ pylutron-caseta==0.6.1 pymailgunner==1.4 # homeassistant.components.firmata -pymata-express==1.13 +pymata-express==1.19 # homeassistant.components.melcloud pymelcloud==2.5.2 From d5741a5ba4e14fec2cd65c90644780f4c9a0c5a9 Mon Sep 17 00:00:00 2001 From: Pedro Lamas Date: Tue, 22 Sep 2020 09:49:44 +0100 Subject: [PATCH 287/514] Fix webostv supported features for "external_speaker" sound output (#40435) --- homeassistant/components/webostv/media_player.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index e53e3185651..66645ee1fb9 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -305,7 +305,9 @@ class LgWebOSMediaPlayerEntity(MediaPlayerEntity): """Flag media player features that are supported.""" supported = SUPPORT_WEBOSTV - if self._client.sound_output == "external_arc": + if (self._client.sound_output == "external_arc") or ( + self._client.sound_output == "external_speaker" + ): supported = supported | SUPPORT_WEBOSTV_VOLUME elif self._client.sound_output != "lineout": supported = supported | SUPPORT_WEBOSTV_VOLUME | SUPPORT_VOLUME_SET From 8bff25feddafc754109b712ac880a7c799d9f229 Mon Sep 17 00:00:00 2001 From: cagnulein Date: Tue, 22 Sep 2020 10:53:46 +0200 Subject: [PATCH 288/514] Fix luci device_tracker incorrectly reporting devices status (#40409) --- homeassistant/components/luci/device_tracker.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index fe64c90bf4c..40b111d0d83 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -94,7 +94,11 @@ class LuciDeviceScanner(DeviceScanner): last_results = [] for device in result: - if device.reachable: + if ( + not hasattr(self.router.router.owrt_version, "release") + or self.router.router.owrt_version.release[0] < 19 + or device.reachable + ): last_results.append(device) self.last_results = last_results From 430275ac88f96efcf508c7fc4a2713d17a7f3619 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 22 Sep 2020 10:57:26 +0200 Subject: [PATCH 289/514] Axis - Fix list applications breaks if empty response (#40360) --- homeassistant/components/axis/manifest.json | 8 ++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index ceb926f326e..959d53a01ae 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -3,11 +3,11 @@ "name": "Axis", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/axis", - "requirements": ["axis==35"], + "requirements": ["axis==37"], "zeroconf": [ - {"type":"_axis-video._tcp.local.","macaddress":"00408C*"}, - {"type":"_axis-video._tcp.local.","macaddress":"ACCC8E*"}, - {"type":"_axis-video._tcp.local.","macaddress":"B8A44F*"} + { "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/requirements_all.txt b/requirements_all.txt index c630670b08a..b5f3d17d316 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==35 +axis==37 # homeassistant.components.azure_event_hub azure-eventhub==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ba319cd732..f23316bba42 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -177,7 +177,7 @@ av==8.0.2 avri-api==0.1.7 # homeassistant.components.axis -axis==35 +axis==37 # homeassistant.components.azure_event_hub azure-eventhub==5.1.0 From 2f9afd3a2c32613ae1d20c4d3f0ffee2bdfd921f Mon Sep 17 00:00:00 2001 From: MeIchthys <10717998+meichthys@users.noreply.github.com> Date: Tue, 22 Sep 2020 04:58:51 -0400 Subject: [PATCH 290/514] Fix regression in Nextcloud component (#40438) --- homeassistant/components/nextcloud/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py index ff94fd708db..1a773040980 100644 --- a/homeassistant/components/nextcloud/__init__.py +++ b/homeassistant/components/nextcloud/__init__.py @@ -100,6 +100,7 @@ def setup(hass, config): _LOGGER.error("Nextcloud setup failed - Check configuration") hass.data[DOMAIN] = get_data_points(ncm.data) + hass.data[DOMAIN]["instance"] = conf[CONF_URL] def nextcloud_update(event_time): """Update data from nextcloud api.""" From 02a19d012379ffab34aac54079d890cbc5bb1670 Mon Sep 17 00:00:00 2001 From: Adam Belebczuk Date: Tue, 22 Sep 2020 02:24:22 -0700 Subject: [PATCH 291/514] Bump pywemo to 0.5.0 (#40439) --- 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 bc9755414c6..357d9d95483 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.46"], + "requirements": ["pywemo==0.5.0"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index b5f3d17d316..67d925f5f65 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1849,7 +1849,7 @@ pyvolumio==0.1.2 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.4.46 +pywemo==0.5.0 # homeassistant.components.wilight pywilight==0.0.65 From d82b97fbe12a7954fcb5d654d45d0f746fa72864 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Tue, 22 Sep 2020 15:35:44 +0200 Subject: [PATCH 292/514] Update xknx to 0.14.3 (#40430) --- homeassistant/components/knx/manifest.json | 5 +++-- requirements_all.txt | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index af9f99677f3..2eb11511cdd 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,6 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.14.2"], - "codeowners": ["@Julius2342", "@farmio", "@marvin-w"] + "requirements": ["xknx==0.14.3"], + "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], + "quality_scale": "silver" } diff --git a/requirements_all.txt b/requirements_all.txt index 67d925f5f65..73f2ae1a07d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2265,7 +2265,7 @@ xboxapi==2.0.1 xfinity-gateway==0.0.4 # homeassistant.components.knx -xknx==0.14.2 +xknx==0.14.3 # homeassistant.components.bluesound # homeassistant.components.rest From f0f817c361ea7a9ea19c23dbf97332ef2b463432 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 22 Sep 2020 08:47:04 -0500 Subject: [PATCH 293/514] Serialize websocket event message once (#40453) Since most of the json serialize work for the websocket was done multiple times for the same message, we can avoid the overhead of serializing the same message many times (once per websocket client) with a cache. --- .../components/websocket_api/commands.py | 4 +- .../components/websocket_api/http.py | 31 ++------- .../components/websocket_api/messages.py | 47 +++++++++++++- homeassistant/core.py | 5 ++ .../components/websocket_api/test_messages.py | 65 +++++++++++++++++++ 5 files changed, 120 insertions(+), 32 deletions(-) create mode 100644 tests/components/websocket_api/test_messages.py diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 036cd690da2..8995f075f32 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -77,7 +77,7 @@ def handle_subscribe_events(hass, connection, msg): ): return - connection.send_message(messages.event_message(msg["id"], event)) + connection.send_message(messages.cached_event_message(msg["id"], event)) else: @@ -87,7 +87,7 @@ def handle_subscribe_events(hass, connection, msg): if event.event_type == EVENT_TIME_CHANGED: return - connection.send_message(messages.event_message(msg["id"], event.as_dict())) + connection.send_message(messages.cached_event_message(msg["id"], event)) connection.subscriptions[msg["id"]] = hass.bus.async_listen( event_type, forward_events diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 7c56fcbc606..27dac62791e 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -11,17 +11,11 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.helpers.event import async_call_later -from homeassistant.util.json import ( - find_paths_unserializable_data, - format_unserializable_data, -) from .auth import AuthPhase, auth_required_message from .const import ( CANCELLATION_ERRORS, DATA_CONNECTIONS, - ERR_UNKNOWN_ERROR, - JSON_DUMP, MAX_PENDING_MSG, PENDING_MSG_PEAK, PENDING_MSG_PEAK_TIME, @@ -30,7 +24,7 @@ from .const import ( URL, ) from .error import Disconnect -from .messages import error_message +from .messages import message_to_json # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -72,27 +66,10 @@ class WebSocketHandler: self._logger.debug("Sending %s", message) - if isinstance(message, str): - await self.wsock.send_str(message) - continue + if not isinstance(message, str): + message = message_to_json(message) - try: - dumped = JSON_DUMP(message) - except (ValueError, TypeError): - await self.wsock.send_json( - error_message( - message["id"], ERR_UNKNOWN_ERROR, "Invalid JSON in response" - ) - ) - self._logger.error( - "Unable to serialize to JSON. Bad data found at %s", - format_unserializable_data( - find_paths_unserializable_data(message, dump=JSON_DUMP) - ), - ) - continue - - await self.wsock.send_str(dumped) + await self.wsock.send_str(message) # Clean up the peaker checker when we shut down the writer if self._peak_checker_unsub: diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index 27d557e8110..52e97b60ccf 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -1,11 +1,21 @@ """Message templates for websocket commands.""" +from functools import lru_cache +import logging +from typing import Any, Dict + import voluptuous as vol +from homeassistant.core import Event from homeassistant.helpers import config_validation as cv +from homeassistant.util.json import ( + find_paths_unserializable_data, + format_unserializable_data, +) from . import const +_LOGGER = logging.getLogger(__name__) # mypy: allow-untyped-defs # Minimal requirements of a message @@ -18,12 +28,12 @@ MINIMAL_MESSAGE_SCHEMA = vol.Schema( BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({vol.Required("id"): cv.positive_int}) -def result_message(iden, result=None): +def result_message(iden: int, result: Any = None) -> Dict: """Return a success result message.""" return {"id": iden, "type": const.TYPE_RESULT, "success": True, "result": result} -def error_message(iden, code, message): +def error_message(iden: int, code: str, message: str) -> Dict: """Return an error result message.""" return { "id": iden, @@ -33,6 +43,37 @@ def error_message(iden, code, message): } -def event_message(iden, event): +def event_message(iden: int, event: Any) -> Dict: """Return an event message.""" return {"id": iden, "type": "event", "event": event} + + +@lru_cache(maxsize=128) +def cached_event_message(iden: int, event: Event) -> str: + """Return an event message. + + Serialize to json once per message. + + Since we can have many clients connected that are + all getting many of the same events (mostly state changed) + we can avoid serializing the same data for each connection. + """ + return message_to_json(event_message(iden, event)) + + +def message_to_json(message: Any) -> str: + """Serialize a websocket message to json.""" + try: + return const.JSON_DUMP(message) + except (ValueError, TypeError): + _LOGGER.error( + "Unable to serialize to JSON. Bad data found at %s", + format_unserializable_data( + find_paths_unserializable_data(message, dump=const.JSON_DUMP) + ), + ) + return const.JSON_DUMP( + error_message( + message["id"], const.ERR_UNKNOWN_ERROR, "Invalid JSON in response" + ) + ) diff --git a/homeassistant/core.py b/homeassistant/core.py index f230fef01eb..fd34032112b 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -548,6 +548,11 @@ class Event: self.time_fired = time_fired or dt_util.utcnow() self.context: Context = context or Context() + def __hash__(self) -> int: + """Make hashable.""" + # The only event type that shares context are the TIME_CHANGED + return hash((self.event_type, self.context.id, self.time_fired)) + def as_dict(self) -> Dict: """Create a dict representation of this Event. diff --git a/tests/components/websocket_api/test_messages.py b/tests/components/websocket_api/test_messages.py new file mode 100644 index 00000000000..832b72c5c1c --- /dev/null +++ b/tests/components/websocket_api/test_messages.py @@ -0,0 +1,65 @@ +"""Test Websocket API messages module.""" + +from homeassistant.components.websocket_api.messages import ( + cached_event_message, + message_to_json, +) +from homeassistant.const import EVENT_STATE_CHANGED +from homeassistant.core import callback + + +async def test_cached_event_message(hass): + """Test that we cache event messages.""" + + events = [] + + @callback + def _event_listener(event): + events.append(event) + + hass.bus.async_listen(EVENT_STATE_CHANGED, _event_listener) + + hass.states.async_set("light.window", "on") + hass.states.async_set("light.window", "off") + await hass.async_block_till_done() + + assert len(events) == 2 + + msg0 = cached_event_message(2, events[0]) + assert msg0 == cached_event_message(2, events[0]) + + msg1 = cached_event_message(2, events[1]) + assert msg1 == cached_event_message(2, events[1]) + + assert msg0 != msg1 + + cache_info = cached_event_message.cache_info() + assert cache_info.hits == 2 + assert cache_info.misses == 2 + assert cache_info.currsize == 2 + + cached_event_message(2, events[1]) + cache_info = cached_event_message.cache_info() + assert cache_info.hits == 3 + assert cache_info.misses == 2 + assert cache_info.currsize == 2 + + +async def test_message_to_json(caplog): + """Test we can serialize websocket messages.""" + + json_str = message_to_json({"id": 1, "message": "xyz"}) + + assert json_str == '{"id": 1, "message": "xyz"}' + + json_str2 = message_to_json({"id": 1, "message": _Unserializeable()}) + + assert ( + json_str2 + == '{"id": 1, "type": "result", "success": false, "error": {"code": "unknown_error", "message": "Invalid JSON in response"}}' + ) + assert "Unable to serialize to JSON" in caplog.text + + +class _Unserializeable: + """A class that cannot be serialized.""" From 7029345b9d0ac61a0ca078061ac91883a1fecc3e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 22 Sep 2020 09:27:04 -0500 Subject: [PATCH 294/514] Add support for selecting multiple entity ids from logbook (#40075) --- homeassistant/components/logbook/__init__.py | 295 +++++++++++-------- tests/components/logbook/test_init.py | 88 ++++++ 2 files changed, 267 insertions(+), 116 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 476aa62501a..29068c1e261 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -7,6 +7,7 @@ import re import sqlalchemy from sqlalchemy.orm import aliased +from sqlalchemy.sql.expression import literal import voluptuous as vol from homeassistant.components import sun @@ -39,12 +40,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import ( - DOMAIN as HA_DOMAIN, - callback, - split_entity_id, - valid_entity_id, -) +from homeassistant.core import DOMAIN as HA_DOMAIN, callback, split_entity_id from homeassistant.exceptions import InvalidEntityFormatError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( @@ -78,6 +74,8 @@ GROUP_BY_MINUTES = 15 EMPTY_JSON_OBJECT = "{}" UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' +HA_DOMAIN_ENTITY_ID = f"{HA_DOMAIN}." + CONFIG_SCHEMA = vol.Schema( {DOMAIN: INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA}, extra=vol.ALLOW_EXTRA ) @@ -87,13 +85,25 @@ HOMEASSISTANT_EVENTS = [ EVENT_HOMEASSISTANT_STOP, ] -ALL_EVENT_TYPES = [ - EVENT_STATE_CHANGED, +ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED = [ EVENT_LOGBOOK_ENTRY, EVENT_CALL_SERVICE, *HOMEASSISTANT_EVENTS, ] +ALL_EVENT_TYPES = [ + EVENT_STATE_CHANGED, + *ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, +] + +EVENT_COLUMNS = [ + Events.event_type, + Events.event_data, + Events.time_fired, + Events.context_id, + Events.context_user_id, +] + SCRIPT_AUTOMATION_EVENTS = [EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED] LOG_MESSAGE_SCHEMA = vol.Schema( @@ -208,7 +218,15 @@ class LogbookView(HomeAssistantView): else: period = int(period) - entity_id = request.query.get("entity") + entity_ids = request.query.get("entity") + if entity_ids: + try: + entity_ids = cv.entity_ids(entity_ids) + except vol.Invalid: + raise InvalidEntityFormatError( + f"Invalid entity id(s) encountered: {entity_ids}. " + "Format should be ." + ) from vol.Invalid end_time = request.query.get("end_time") if end_time is None: @@ -231,7 +249,7 @@ class LogbookView(HomeAssistantView): hass, start_day, end_day, - entity_id, + entity_ids, self.filters, self.entities_filter, entity_matches_only, @@ -409,143 +427,188 @@ def _get_events( hass, start_day, end_day, - entity_id=None, + entity_ids=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.""" for row in query.yield_per(1000): event = LazyEventPartialState(row) context_lookup.setdefault(event.context_id, event) - if _keep_event(hass, event, entities_filter): + if event.event_type == EVENT_CALL_SERVICE: + continue + if event.event_type == EVENT_STATE_CHANGED or _keep_event( + hass, event, entities_filter + ): yield event - 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 + if entity_ids is not None: + entities_filter = generate_filter([], entity_ids, [], []) with session_scope(hass=hass) as session: old_state = aliased(States, name="old_state") - query = ( - session.query( - Events.event_type, - Events.event_data, - Events.time_fired, - Events.context_id, - Events.context_user_id, - States.state, - States.entity_id, - States.domain, - States.attributes, + if entity_ids is not None: + query = _generate_events_query_without_states(session) + query = _apply_event_time_filter(query, start_day, end_day) + query = _apply_event_types_filter( + hass, query, ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED ) - .order_by(Events.time_fired) - .outerjoin(States, (Events.event_id == States.event_id)) - .outerjoin(old_state, (States.old_state_id == old_state.state_id)) - # The below filter, removes state change events that do not have - # and old_state, new_state, or the old and - # new state. - # - .filter( - (Events.event_type != EVENT_STATE_CHANGED) - | ( - (States.state_id.isnot(None)) - & (old_state.state_id.isnot(None)) - & (States.state.isnot(None)) - & (States.state != old_state.state) - ) - ) - # - # Prefilter out continuous domains that have - # ATTR_UNIT_OF_MEASUREMENT as its much faster in sql. - # - .filter( - (Events.event_type != EVENT_STATE_CHANGED) - | sqlalchemy.not_(States.domain.in_(CONTINUOUS_DOMAINS)) - | sqlalchemy.not_(States.attributes.contains(UNIT_OF_MEASUREMENT_JSON)) - ) - .filter( - Events.event_type.in_(ALL_EVENT_TYPES + list(hass.data.get(DOMAIN, {}))) - ) - .filter((Events.time_fired > start_day) & (Events.time_fired < end_day)) - ) - - 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)) - ) - else: - query = query.filter( - (States.last_updated == States.last_changed) - | (States.state_id.is_(None)) - ) + # contain the entity_ids are not included in the logbook response. + query = _apply_event_entity_id_matchers(query, entity_ids) - if apply_sql_entities_filter and filters: - entity_filter = filters.entity_filter() - if entity_filter is not None: - query = query.filter( - entity_filter | (Events.event_type != EVENT_STATE_CHANGED) + query = query.union_all( + _generate_states_query( + session, start_day, end_day, old_state, entity_ids ) + ) + else: + query = _generate_events_query(session) + query = _apply_event_time_filter(query, start_day, end_day) + query = _apply_events_types_and_states_filter( + hass, query, old_state + ).filter( + (States.last_updated == States.last_changed) + | (Events.event_type != EVENT_STATE_CHANGED) + ) + if filters: + query = query.filter( + filters.entity_filter() | (Events.event_type != EVENT_STATE_CHANGED) + ) + + query = query.order_by(Events.time_fired) return list( humanify(hass, yield_events(query), entity_attr_cache, context_lookup) ) -def _keep_event(hass, event, entities_filter): - if event.event_type == EVENT_STATE_CHANGED: - entity_id = event.entity_id - elif event.event_type in HOMEASSISTANT_EVENTS: - entity_id = f"{HA_DOMAIN}." - elif event.event_type == EVENT_CALL_SERVICE: - return False - else: - entity_id = event.data_entity_id - if not entity_id: - 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}." +def _generate_events_query(session): + return session.query( + *EVENT_COLUMNS, + States.state, + States.entity_id, + States.domain, + States.attributes, + ) - return entities_filter is None or entities_filter(entity_id) + +def _generate_events_query_without_states(session): + return session.query( + *EVENT_COLUMNS, + literal(None).label("state"), + literal(None).label("entity_id"), + literal(None).label("domain"), + literal(None).label("attributes"), + ) + + +def _generate_states_query(session, start_day, end_day, old_state, entity_ids): + return ( + _generate_events_query(session) + .outerjoin(Events, (States.event_id == Events.event_id)) + .outerjoin(old_state, (States.old_state_id == old_state.state_id)) + .filter(_missing_state_matcher(old_state)) + .filter(_continuous_entity_matcher()) + .filter((States.last_updated > start_day) & (States.last_updated < end_day)) + .filter( + (States.last_updated == States.last_changed) + & States.entity_id.in_(entity_ids) + ) + ) + + +def _apply_events_types_and_states_filter(hass, query, old_state): + events_query = ( + query.outerjoin(States, (Events.event_id == States.event_id)) + .outerjoin(old_state, (States.old_state_id == old_state.state_id)) + .filter( + (Events.event_type != EVENT_STATE_CHANGED) + | _missing_state_matcher(old_state) + ) + .filter( + (Events.event_type != EVENT_STATE_CHANGED) | _continuous_entity_matcher() + ) + ) + return _apply_event_types_filter(hass, events_query, ALL_EVENT_TYPES) + + +def _missing_state_matcher(old_state): + # The below removes state change events that do not have + # and old_state or the old_state is missing (newly added entities) + # or the new_state is missing (removed entities) + return sqlalchemy.and_( + old_state.state_id.isnot(None), + (States.state != old_state.state), + States.state.isnot(None), + ) + + +def _continuous_entity_matcher(): + # + # Prefilter out continuous domains that have + # ATTR_UNIT_OF_MEASUREMENT as its much faster in sql. + # + return sqlalchemy.or_( + sqlalchemy.not_(States.domain.in_(CONTINUOUS_DOMAINS)), + sqlalchemy.not_(States.attributes.contains(UNIT_OF_MEASUREMENT_JSON)), + ) + + +def _apply_event_time_filter(events_query, start_day, end_day): + return events_query.filter( + (Events.time_fired > start_day) & (Events.time_fired < end_day) + ) + + +def _apply_event_types_filter(hass, query, event_types): + return query.filter( + Events.event_type.in_(event_types + list(hass.data.get(DOMAIN, {}))) + ) + + +def _apply_event_entity_id_matchers(events_query, entity_ids): + return events_query.filter( + sqlalchemy.or_( + *[ + Events.event_data.contains(ENTITY_ID_JSON_TEMPLATE.format(entity_id)) + for entity_id in entity_ids + ] + ) + ) + + +def _keep_event(hass, event, entities_filter): + if event.event_type in HOMEASSISTANT_EVENTS: + return entities_filter is None or entities_filter(HA_DOMAIN_ENTITY_ID) + + if event.event_type == EVENT_STATE_CHANGED: + return entities_filter is None or entities_filter(event.entity_id) + + entity_id = event.data_entity_id + if entity_id: + return entities_filter is None or entities_filter(entity_id) + + 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 + + return entities_filter is None or entities_filter(f"{domain}.") def _entry_message_from_event(entity_id, domain, event, entity_attr_cache): diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 745d809ca7f..d805eb40ec1 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -2157,6 +2157,94 @@ async def test_logbook_entity_matches_only(hass, hass_client): assert json_dict[1]["message"] == "turned on" +async def test_logbook_entity_matches_only_multiple(hass, hass_client): + """Test the logbook view with a multiple entities 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) + hass.states.async_set("light.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) + hass.states.async_set("light.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 + ) + hass.states.async_set("light.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,light.test_state&entity_matches_only" + ) + assert response.status == 200 + json_dict = await response.json() + + assert len(json_dict) == 4 + + assert json_dict[0]["entity_id"] == "switch.test_state" + assert json_dict[0]["message"] == "turned off" + + assert json_dict[1]["entity_id"] == "light.test_state" + assert json_dict[1]["message"] == "turned off" + + assert json_dict[2]["entity_id"] == "switch.test_state" + assert json_dict[2]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" + assert json_dict[2]["message"] == "turned on" + + assert json_dict[3]["entity_id"] == "light.test_state" + assert json_dict[3]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" + assert json_dict[3]["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) From f837da6fe389beeaf991ff40247f086fb1161d9a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 22 Sep 2020 09:28:02 -0500 Subject: [PATCH 295/514] Defer template tracking setup until template entity start (#40388) --- .../components/template/template_entity.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 632eeea8926..53d9d2f1a33 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -213,7 +213,6 @@ class TemplateEntity(Entity): attribute = _TemplateAttribute( self, attribute, template, validator, on_update, none_on_template_error ) - attribute.async_setup() self._template_attrs.setdefault(template, []) self._template_attrs[template].append(attribute) @@ -252,22 +251,21 @@ class TemplateEntity(Entity): event, update.template, update.last_result, update.result ) - if self._async_update: - self.async_write_ha_state() + self.async_write_ha_state() async def _async_template_startup(self, *_) -> None: - # _handle_results will not write state until "_async_update" is set - template_var_tups = [ - TrackTemplate(template, None) for template in self._template_attrs - ] + template_var_tups = [] + for template, attributes in self._template_attrs.items(): + template_var_tups.append(TrackTemplate(template, None)) + for attribute in attributes: + attribute.async_setup() 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 + result_info.async_refresh() async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" From 1ed8e41e9024af6d7ce3d9b1b691f00872052788 Mon Sep 17 00:00:00 2001 From: square99 Date: Wed, 23 Sep 2020 00:34:23 +0900 Subject: [PATCH 296/514] Fix proxy camera conversion with PNG Alpha(RGBA) (#40446) --- homeassistant/components/proxy/camera.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 754f09fa199..c3f7151431a 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -77,6 +77,8 @@ def _precheck_image(image, opts): if imgfmt not in ("PNG", "JPEG"): _LOGGER.warning("Image is of unsupported type: %s", imgfmt) raise ValueError() + if not img.mode == "RGB": + img = img.convert("RGB") return img From 040075427040cd60bceb2aa0c589730e3c79e251 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 22 Sep 2020 17:36:44 +0200 Subject: [PATCH 297/514] Bump accuweather library to version 0.0.11 (#40458) --- 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 a383c49f348..6ccd6a4f10b 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.10"], + "requirements": ["accuweather==0.0.11"], "codeowners": ["@bieniu"], "config_flow": true, "quality_scale": "platinum" diff --git a/requirements_all.txt b/requirements_all.txt index 73f2ae1a07d..94498a689ec 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.10 +accuweather==0.0.11 # homeassistant.components.mcp23017 adafruit-blinka==3.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f23316bba42..2dd8bed4c23 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.10 +accuweather==0.0.11 # homeassistant.components.androidtv adb-shell[async]==0.2.1 From 6e8e4eedb5728dd70fbcf8543c696e42b077002b Mon Sep 17 00:00:00 2001 From: Tom Schneider Date: Tue, 22 Sep 2020 17:42:50 +0200 Subject: [PATCH 298/514] Add binary_sensor for elevator states to hvv_departures (#36822) Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + .../components/hvv_departures/__init__.py | 3 +- .../hvv_departures/binary_sensor.py | 201 ++++++++++++++++++ .../hvv_departures/test_config_flow.py | 6 +- 4 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/hvv_departures/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 773bc9fb0f6..a5ff56b8d04 100644 --- a/.coveragerc +++ b/.coveragerc @@ -374,6 +374,7 @@ omit = homeassistant/components/hunterdouglas_powerview/sensor.py homeassistant/components/hunterdouglas_powerview/cover.py homeassistant/components/hunterdouglas_powerview/entity.py + homeassistant/components/hvv_departures/binary_sensor.py homeassistant/components/hvv_departures/sensor.py homeassistant/components/hvv_departures/__init__.py homeassistant/components/hydrawise/* diff --git a/homeassistant/components/hvv_departures/__init__.py b/homeassistant/components/hvv_departures/__init__.py index 853ed9460c8..e003b25ea85 100644 --- a/homeassistant/components/hvv_departures/__init__.py +++ b/homeassistant/components/hvv_departures/__init__.py @@ -1,6 +1,7 @@ """The HVV integration.""" import asyncio +from homeassistant.components.binary_sensor import DOMAIN as DOMAIN_BINARY_SENSOR from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -10,7 +11,7 @@ from homeassistant.helpers import aiohttp_client from .const import DOMAIN from .hub import GTIHub -PLATFORMS = [DOMAIN_SENSOR] +PLATFORMS = [DOMAIN_SENSOR, DOMAIN_BINARY_SENSOR] async def async_setup(hass: HomeAssistant, config: dict): diff --git a/homeassistant/components/hvv_departures/binary_sensor.py b/homeassistant/components/hvv_departures/binary_sensor.py new file mode 100644 index 00000000000..7d19fcc8fdf --- /dev/null +++ b/homeassistant/components/hvv_departures/binary_sensor.py @@ -0,0 +1,201 @@ +"""Binary sensor platform for hvv_departures.""" +from datetime import timedelta +import logging + +from aiohttp import ClientConnectorError +import async_timeout +from pygti.exceptions import InvalidAuth + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_PROBLEM, + BinarySensorEntity, +) +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ATTRIBUTION, CONF_STATION, DOMAIN, MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the binary_sensor platform.""" + hub = hass.data[DOMAIN][entry.entry_id] + station_name = entry.data[CONF_STATION]["name"] + station = entry.data[CONF_STATION] + + def get_elevator_entities_from_station_information( + station_name, station_information + ): + """Convert station information into a list of elevators.""" + elevators = {} + + if station_information is None: + return {} + + for partial_station in station_information.get("partialStations", []): + for elevator in partial_station.get("elevators", []): + + state = elevator.get("state") != "READY" + available = elevator.get("state") != "UNKNOWN" + label = elevator.get("label") + description = elevator.get("description") + + if label is not None: + name = f"Elevator {label} at {station_name}" + else: + name = f"Unknown elevator at {station_name}" + + if description is not None: + name += f" ({description})" + + lines = elevator.get("lines") + + idx = f"{station_name}-{label}-{lines}" + + elevators[idx] = { + "state": state, + "name": name, + "available": available, + "attributes": { + "cabin_width": elevator.get("cabinWidth"), + "cabin_length": elevator.get("cabinLength"), + "door_width": elevator.get("doorWidth"), + "elevator_type": elevator.get("elevatorType"), + "button_type": elevator.get("buttonType"), + "cause": elevator.get("cause"), + "lines": lines, + ATTR_ATTRIBUTION: ATTRIBUTION, + }, + } + return elevators + + async def async_update_data(): + """Fetch data from API endpoint. + + This is the place to pre-process the data to lookup tables + so entities can quickly look up their data. + """ + + payload = {"station": station} + + try: + async with async_timeout.timeout(10): + return get_elevator_entities_from_station_information( + station_name, await hub.gti.stationInformation(payload) + ) + except InvalidAuth as err: + raise UpdateFailed(f"Authentication failed: {err}") from err + except ClientConnectorError as err: + raise UpdateFailed(f"Network not available: {err}") from err + except Exception as err: # pylint: disable=broad-except + raise UpdateFailed(f"Error occurred while fetching data: {err}") from err + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="hvv_departures.binary_sensor", + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(hours=1), + ) + + # Fetch initial data so we have data when entities subscribe + await coordinator.async_refresh() + + async_add_entities( + HvvDepartureBinarySensor(coordinator, idx, entry) + for (idx, ent) in coordinator.data.items() + ) + + +class HvvDepartureBinarySensor(CoordinatorEntity, BinarySensorEntity): + """HVVDepartureBinarySensor class.""" + + def __init__(self, coordinator, idx, config_entry): + """Initialize.""" + super().__init__(coordinator) + self.coordinator = coordinator + self.idx = idx + self.config_entry = config_entry + + @property + def is_on(self): + """Return entity state.""" + return self.coordinator.data[self.idx]["state"] + + @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 + and self.coordinator.data[self.idx]["available"] + ) + + @property + def device_info(self): + """Return the device info for this sensor.""" + return { + "identifiers": { + ( + DOMAIN, + self.config_entry.entry_id, + self.config_entry.data[CONF_STATION]["id"], + self.config_entry.data[CONF_STATION]["type"], + ) + }, + "name": f"Departures at {self.config_entry.data[CONF_STATION]['name']}", + "manufacturer": MANUFACTURER, + } + + @property + def name(self): + """Return the name of the sensor.""" + return self.coordinator.data[self.idx]["name"] + + @property + def unique_id(self): + """Return a unique ID to use for this sensor.""" + return self.idx + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_PROBLEM + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if not ( + self.coordinator.last_update_success + and self.coordinator.data[self.idx]["available"] + ): + return None + return { + k: v + for k, v in self.coordinator.data[self.idx]["attributes"].items() + if v is not None + } + + 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 entity. + + Only used by the generic entity update service. + """ + await self.coordinator.async_request_refresh() diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index 1646ee73dbd..bd3e955d2d8 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -267,7 +267,7 @@ async def test_options_flow(hass): ) config_entry.add_to_hass(hass) - with patch( + with patch("homeassistant.components.hvv_departures.PLATFORMS", new=[]), patch( "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True, ), patch( @@ -318,7 +318,7 @@ async def test_options_flow_invalid_auth(hass): ) config_entry.add_to_hass(hass) - with patch( + with patch("homeassistant.components.hvv_departures.PLATFORMS", new=[]), patch( "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True ), patch( "homeassistant.components.hvv_departures.hub.GTI.departureList", @@ -359,7 +359,7 @@ async def test_options_flow_cannot_connect(hass): ) config_entry.add_to_hass(hass) - with patch( + with patch("homeassistant.components.hvv_departures.PLATFORMS", new=[]), patch( "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True ), patch( "homeassistant.components.hvv_departures.hub.GTI.departureList", From fb7fb0ea78ee335cd23f3647223a675718ccf048 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 22 Sep 2020 19:55:10 +0200 Subject: [PATCH 299/514] deCONZ - move event handling (#40424) * Working draft * Remove event references in sensor --- .../components/deconz/deconz_event.py | 42 ++++++++++++++++++- homeassistant/components/deconz/gateway.py | 7 ++-- homeassistant/components/deconz/sensor.py | 16 ++----- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index d1325bcddd6..bde2db90663 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -1,14 +1,54 @@ """Representation of a deCONZ remote.""" +from pydeconz.sensor import Switch + from homeassistant.const import CONF_EVENT, CONF_ID, CONF_UNIQUE_ID from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import slugify -from .const import CONF_ANGLE, CONF_GESTURE, CONF_XY, LOGGER +from .const import CONF_ANGLE, CONF_GESTURE, CONF_XY, LOGGER, NEW_SENSOR from .deconz_device import DeconzBase CONF_DECONZ_EVENT = "deconz_event" +async def async_setup_events(gateway) -> None: + """Set up the deCONZ events.""" + + @callback + def async_add_sensor(sensors, new=True): + """Create DeconzEvent.""" + for sensor in sensors: + + if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): + continue + + if not new or sensor.type not in Switch.ZHATYPE: + continue + + new_event = DeconzEvent(sensor, gateway) + gateway.hass.async_create_task(new_event.async_update_device_registry()) + gateway.events.append(new_event) + + gateway.listeners.append( + async_dispatcher_connect( + gateway.hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor + ) + ) + + async_add_sensor( + [gateway.api.sensors[key] for key in sorted(gateway.api.sensors, key=int)] + ) + + +async def async_unload_events(gateway) -> None: + """Unload all deCONZ events.""" + for event in gateway.events: + event.async_will_remove_from_hass() + + gateway.events.clear() + + class DeconzEvent(DeconzBase): """When you want signals instead of entities. diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 828f65c9811..d3a781302e5 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -25,6 +25,7 @@ from .const import ( NEW_SENSOR, SUPPORTED_PLATFORMS, ) +from .deconz_event import async_setup_events, async_unload_events from .errors import AuthenticationRequired, CannotConnect @@ -112,6 +113,8 @@ class DeconzGateway: ) ) + self.hass.async_create_task(async_setup_events(self)) + self.api.start() self.config_entry.add_update_listener(self.async_config_entry_updated) @@ -227,9 +230,7 @@ class DeconzGateway: unsub_dispatcher() self.listeners = [] - for event in self.events: - event.async_will_remove_from_hass() - self.events.clear() + await async_unload_events(self) self.deconz_ids = {} return True diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 4ebed981e2d..0c9ecb032df 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -35,7 +35,6 @@ from homeassistant.helpers.dispatcher import ( from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice -from .deconz_event import DeconzEvent from .gateway import get_gateway_from_config_entry ATTR_CURRENT = "current" @@ -82,7 +81,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): def async_add_sensor(sensors, new=True): """Add sensors from deCONZ. - Create DeconzEvent if part of ZHAType list. Create DeconzSensor if not a ZHAType and not a binary sensor. Create DeconzBattery if sensor has a battery attribute. If new is false it means an existing sensor has got a battery state reported. @@ -91,19 +89,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for sensor in sensors: - if new and sensor.type in Switch.ZHATYPE: - - if gateway.option_allow_clip_sensor or not sensor.type.startswith( - "CLIP" - ): - new_event = DeconzEvent(sensor, gateway) - hass.async_create_task(new_event.async_update_device_registry()) - gateway.events.append(new_event) - - elif ( + if ( new and sensor.BINARY is False - and sensor.type not in Battery.ZHATYPE + Thermostat.ZHATYPE + and sensor.type + not in Battery.ZHATYPE + Switch.ZHATYPE + Thermostat.ZHATYPE and ( gateway.option_allow_clip_sensor or not sensor.type.startswith("CLIP") From a40d853682a08a0cc2d1e79d0ded3956ffccf917 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Tue, 22 Sep 2020 16:00:27 -0700 Subject: [PATCH 300/514] Increase gogogate2 request timeout (#40461) --- homeassistant/components/gogogate2/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index a12058d38d0..893294da25e 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.2"], + "requirements": ["gogogate2-api==2.0.3"], "codeowners": ["@vangorra"], "homekit": { "models": [ diff --git a/requirements_all.txt b/requirements_all.txt index 94498a689ec..0b1ebc29ae0 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.2 +gogogate2-api==2.0.3 # homeassistant.components.google google-api-python-client==1.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2dd8bed4c23..4dc6bad66cf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,7 +337,7 @@ gios==0.1.4 glances_api==0.2.0 # homeassistant.components.gogogate2 -gogogate2-api==2.0.2 +gogogate2-api==2.0.3 # homeassistant.components.google google-api-python-client==1.6.4 From 75659ff78704169afebe633365525f15c1f20810 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 23 Sep 2020 00:05:01 +0000 Subject: [PATCH 301/514] [ci skip] Translation update --- .../accuweather/translations/et.json | 2 +- .../components/airly/translations/et.json | 12 +++++ .../components/airvisual/translations/et.json | 5 ++ .../alarm_control_panel/translations/et.json | 16 ++++-- .../alarmdecoder/translations/et.json | 10 ++-- .../alarmdecoder/translations/lb.json | 20 ++++++++ .../azure_devops/translations/pl.json | 4 +- .../components/broadlink/translations/lb.json | 1 + .../components/canary/translations/lb.json | 5 ++ .../components/cover/translations/et.json | 37 ++++++++------ .../components/deconz/translations/et.json | 24 +++++++++ .../components/dexcom/translations/et.json | 11 ++++ .../flunearyou/translations/et.json | 12 +++++ .../forked_daapd/translations/pl.json | 1 + .../components/hangouts/translations/et.json | 2 + .../homekit_controller/translations/et.json | 22 ++++++++ .../homekit_controller/translations/lb.json | 14 ++++++ .../homematicip_cloud/translations/lb.json | 1 + .../components/hue/translations/et.json | 21 ++++++++ .../humidifier/translations/et.json | 21 ++++++++ .../components/ifttt/translations/et.json | 7 +++ .../components/ipma/translations/et.json | 13 +++++ .../components/kodi/translations/et.json | 8 +++ .../components/light/translations/et.json | 3 ++ .../meteo_france/translations/pl.json | 3 +- .../components/metoffice/translations/et.json | 13 +++++ .../components/mqtt/translations/et.json | 8 +-- .../components/netatmo/translations/et.json | 27 ++++++++++ .../components/nws/translations/et.json | 13 +++++ .../components/openuv/translations/et.json | 12 +++++ .../openweathermap/translations/lb.json | 16 ++++++ .../components/remote/translations/et.json | 15 ++++++ .../components/remote/translations/lb.json | 13 +++++ .../components/sensor/translations/et.json | 12 ++++- .../components/smappee/translations/pl.json | 5 ++ .../smartthings/translations/et.json | 11 ++++ .../components/smhi/translations/et.json | 6 ++- .../components/switch/translations/et.json | 9 ++++ .../components/vacuum/translations/et.json | 2 +- .../components/yeelight/translations/lb.json | 3 ++ .../components/zha/translations/et.json | 50 +++++++++++++++++++ .../zodiac/translations/sensor.lb.json | 18 +++++++ .../zodiac/translations/sensor.no.json | 18 +++++++ .../zodiac/translations/sensor.pl.json | 18 +++++++ .../zodiac/translations/sensor.zh-Hant.json | 18 +++++++ .../components/zone/translations/et.json | 9 ++-- .../zoneminder/translations/lb.json | 11 ++++ 47 files changed, 544 insertions(+), 38 deletions(-) create mode 100644 homeassistant/components/airly/translations/et.json create mode 100644 homeassistant/components/alarmdecoder/translations/lb.json create mode 100644 homeassistant/components/canary/translations/lb.json create mode 100644 homeassistant/components/dexcom/translations/et.json create mode 100644 homeassistant/components/flunearyou/translations/et.json create mode 100644 homeassistant/components/homekit_controller/translations/et.json create mode 100644 homeassistant/components/humidifier/translations/et.json create mode 100644 homeassistant/components/ifttt/translations/et.json create mode 100644 homeassistant/components/ipma/translations/et.json create mode 100644 homeassistant/components/kodi/translations/et.json create mode 100644 homeassistant/components/metoffice/translations/et.json create mode 100644 homeassistant/components/netatmo/translations/et.json create mode 100644 homeassistant/components/nws/translations/et.json create mode 100644 homeassistant/components/openuv/translations/et.json create mode 100644 homeassistant/components/openweathermap/translations/lb.json create mode 100644 homeassistant/components/smartthings/translations/et.json create mode 100644 homeassistant/components/yeelight/translations/lb.json create mode 100644 homeassistant/components/zodiac/translations/sensor.lb.json create mode 100644 homeassistant/components/zodiac/translations/sensor.no.json create mode 100644 homeassistant/components/zodiac/translations/sensor.pl.json create mode 100644 homeassistant/components/zodiac/translations/sensor.zh-Hant.json create mode 100644 homeassistant/components/zoneminder/translations/lb.json diff --git a/homeassistant/components/accuweather/translations/et.json b/homeassistant/components/accuweather/translations/et.json index 501f55001de..51cf2050ba7 100644 --- a/homeassistant/components/accuweather/translations/et.json +++ b/homeassistant/components/accuweather/translations/et.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u00dchendus eba\u00f5nnestus", "invalid_api_key": "API v\u00f5ti on vale", - "requests_exceeded": "Accuweatheri API-le esitatud taotluste lubatud arv on \u00fcletatud. Peate ootama v\u00f5i muutma API v\u00f5tit." + "requests_exceeded": "Accuweatheri API-le esitatud p\u00e4ringute piirarv on \u00fcletatud. Peate ootama (v\u00f5i muutma API v\u00f5tit)." }, "step": { "user": { diff --git a/homeassistant/components/airly/translations/et.json b/homeassistant/components/airly/translations/et.json new file mode 100644 index 00000000000..aae3ef835bb --- /dev/null +++ b/homeassistant/components/airly/translations/et.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/et.json b/homeassistant/components/airvisual/translations/et.json index 5bbc2c47689..51bc3faf3f2 100644 --- a/homeassistant/components/airvisual/translations/et.json +++ b/homeassistant/components/airvisual/translations/et.json @@ -6,6 +6,11 @@ "latitude": "Laiuskraad", "longitude": "Pikkuskraad" } + }, + "user": { + "data": { + "cloud_api": "Geograafiline asukoht" + } } } } diff --git a/homeassistant/components/alarm_control_panel/translations/et.json b/homeassistant/components/alarm_control_panel/translations/et.json index cb87bc6ed04..76b0c845d01 100644 --- a/homeassistant/components/alarm_control_panel/translations/et.json +++ b/homeassistant/components/alarm_control_panel/translations/et.json @@ -7,11 +7,19 @@ "disarm": "V\u00f5ta {entity_name} valvest maha", "trigger": "K\u00e4ivita {entity_name}" }, + "condition_type": { + "is_armed_away": "{entity_name} on valvestatud", + "is_armed_home": "{entity_name} on valvestatud kodure\u017eiimis", + "is_armed_night": "{entity_name} on valvestatud \u00f6\u00f6re\u017eiimis", + "is_disarmed": "{entity_name} on valve alt maas", + "is_triggered": "{entity_name} on h\u00e4iret andnud" + }, "trigger_type": { - "armed_away": "{entity_name} on valvestatud", - "armed_home": "{entity_name} on valvestatud kodure\u017eiimis", - "armed_night": "{entity_name} on valvestatud \u00f6\u00f6re\u017eiimis", - "disarmed": "{entity_name} v\u00f5eti valvest maha" + "armed_away": "{entity_name} valvestatus", + "armed_home": "{entity_name} valvestatus kodure\u017eiimis", + "armed_night": "{entity_name} valvestatus \u00f6\u00f6re\u017eiimis", + "disarmed": "{entity_name} v\u00f5eti valvest maha", + "triggered": "{entity_name} andis h\u00e4iret" } }, "state": { diff --git a/homeassistant/components/alarmdecoder/translations/et.json b/homeassistant/components/alarmdecoder/translations/et.json index c88a63eadb6..4837483dee3 100644 --- a/homeassistant/components/alarmdecoder/translations/et.json +++ b/homeassistant/components/alarmdecoder/translations/et.json @@ -28,20 +28,20 @@ "zone_details": { "data": { "zone_loop": "RF silmus", - "zone_name": "Tsooni nimi", + "zone_name": "Ala nimi", "zone_relayaddr": "Relee aadress", "zone_relaychan": "Relee kanalinumber", "zone_rfid": "RF jada\u00fchendus", - "zone_type": "Tsooni t\u00fc\u00fcp" + "zone_type": "Ala t\u00fc\u00fcp" }, - "description": "Sisestage tsooni {zone_number} \u00fcksikasjad. Tsooni {zone_number} kustutamiseks j\u00e4tke tsooni nimi t\u00fchjaks.", + "description": "Sisestage ala {zone_number} \u00fcksikasjad. Ala {zone_number} kustutamiseks j\u00e4tke ala nimi t\u00fchjaks.", "title": "Seadista AlarmDecoder" }, "zone_select": { "data": { - "zone_number": "Tsooni number" + "zone_number": "Ala number" }, - "description": "Sisestage tsooni number mida soovite lisada, muuta v\u00f5i eemaldada.", + "description": "Sisestage ala number mida soovite lisada, muuta v\u00f5i eemaldada.", "title": "Seadista AlarmDecoder" } } diff --git a/homeassistant/components/alarmdecoder/translations/lb.json b/homeassistant/components/alarmdecoder/translations/lb.json new file mode 100644 index 00000000000..49cc4cd37f3 --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "protocol": "Protokoll" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "edit_select": "\u00c4nneren" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_devops/translations/pl.json b/homeassistant/components/azure_devops/translations/pl.json index 1dbff873a08..868cbe84645 100644 --- a/homeassistant/components/azure_devops/translations/pl.json +++ b/homeassistant/components/azure_devops/translations/pl.json @@ -7,9 +7,11 @@ "step": { "user": { "data": { + "organization": "Organizacja", "project": "Projekt" } } } - } + }, + "title": "Azure DevOps" } \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/lb.json b/homeassistant/components/broadlink/translations/lb.json index 0872c01b608..e4f60a3eac9 100644 --- a/homeassistant/components/broadlink/translations/lb.json +++ b/homeassistant/components/broadlink/translations/lb.json @@ -5,6 +5,7 @@ "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.", + "not_supported": "Apparat net \u00ebnnerst\u00ebtzt.", "unknown": "Onerwaarte Feeler" }, "error": { diff --git a/homeassistant/components/canary/translations/lb.json b/homeassistant/components/canary/translations/lb.json new file mode 100644 index 00000000000..4f17a03ff4b --- /dev/null +++ b/homeassistant/components/canary/translations/lb.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "Canary: {name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/translations/et.json b/homeassistant/components/cover/translations/et.json index 8b48976a9e7..baca6feeca5 100644 --- a/homeassistant/components/cover/translations/et.json +++ b/homeassistant/components/cover/translations/et.json @@ -1,29 +1,38 @@ { "device_automation": { + "action_type": { + "close": "Sule aknakate {entity_name}", + "close_tilt": "Sule aknakatte {entity_name} kaldribid", + "open": "Ava aknakate {entity_name}", + "open_tilt": "Ava aknakatte {entity_name} kaldribid", + "set_position": "M\u00e4\u00e4ra aknakatte {entity_name} asend", + "set_tilt_position": "M\u00e4\u00e4ra aknakatte {entity_name} kaldribide asend", + "stop": "Peata aknakatte {entity_name} liikumine" + }, "condition_type": { - "is_closed": "{entity_name} on suletud", - "is_closing": "{entity_name} sulgub", - "is_open": "{entity_name} on avatud", - "is_opening": "{entity_name} avaneb", - "is_position": "Praegune {entity_name} asend on", - "is_tilt_position": "Praegune {entity_name} kalle on" + "is_closed": "Aknakate {entity_name} on suletud", + "is_closing": "Aknakate {entity_name} sulgub", + "is_open": "Aknakate {entity_name} on avatud", + "is_opening": "Aknakate {entity_name} avaneb", + "is_position": "Aknakatte {entity_name} praegune asend on", + "is_tilt_position": "Aknakatte {entity_name} praegune kalle on" }, "trigger_type": { - "closed": "{entity_name} sulgus", - "closing": "{entity_name} sulgub", - "opened": "{entity_name} avanes", - "opening": "{entity_name} avaneb", - "position": "{entity_name} asend muutub", - "tilt_position": "{entity_name} kalle muutub" + "closed": "Aknakate {entity_name} sulgus", + "closing": "Aknakate {entity_name} sulgub", + "opened": "Aknakate {entity_name} avanes", + "opening": "Aknakate {entity_name} avaneb", + "position": "Aknakatte {entity_name} asend muutub", + "tilt_position": "Aknakatte {entity_name} kalle muutub" } }, "state": { "_": { "closed": "Suletud", - "closing": "Sulgub", + "closing": "Aknakate sulgub", "open": "Avatud", "opening": "Avaneb", - "stopped": "Peatatud" + "stopped": "Aknakate peatatus" } }, "title": "Kardin" diff --git a/homeassistant/components/deconz/translations/et.json b/homeassistant/components/deconz/translations/et.json index db8be2a955c..636c75de471 100644 --- a/homeassistant/components/deconz/translations/et.json +++ b/homeassistant/components/deconz/translations/et.json @@ -1,6 +1,30 @@ { "device_automation": { + "trigger_subtype": { + "both_buttons": "M\u00f5lemad nupud", + "bottom_buttons": "Alumised nupud", + "button_1": "Esimene nupp", + "button_2": "Teine nupp", + "button_3": "Kolmas nupp", + "button_4": "Neljas nupp", + "close": "Sulge", + "dim_down": "H\u00e4marda", + "dim_up": "Tee heledamaks", + "left": "Vasakpoolne", + "open": "Ava", + "right": "Parempoolne", + "side_1": "1. k\u00fclg", + "side_2": "2. k\u00fclg", + "side_3": "3. k\u00fclg", + "side_4": "4. k\u00fclg", + "side_5": "5. k\u00fclg", + "side_6": "6. k\u00fclg", + "top_buttons": "\u00dclemised nupud", + "turn_off": "L\u00fclita v\u00e4lja", + "turn_on": "L\u00fclita sisse" + }, "trigger_type": { + "remote_awakened": "Seade \u00e4rkas", "remote_button_rotation_stopped": "Nupu \" {subtype} \" p\u00f6\u00f6ramine peatus" } } diff --git a/homeassistant/components/dexcom/translations/et.json b/homeassistant/components/dexcom/translations/et.json new file mode 100644 index 00000000000..b1967bf2695 --- /dev/null +++ b/homeassistant/components/dexcom/translations/et.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "M\u00f5\u00f5t\u00fchik" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/et.json b/homeassistant/components/flunearyou/translations/et.json new file mode 100644 index 00000000000..aae3ef835bb --- /dev/null +++ b/homeassistant/components/flunearyou/translations/et.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/pl.json b/homeassistant/components/forked_daapd/translations/pl.json index d40e9b282aa..7f17565c2cd 100644 --- a/homeassistant/components/forked_daapd/translations/pl.json +++ b/homeassistant/components/forked_daapd/translations/pl.json @@ -5,6 +5,7 @@ }, "error": { "unknown_error": "Nieznany b\u0142\u0105d.", + "wrong_host_or_port": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia. Sprawd\u017a adres hosta i port.", "wrong_password": "Nieprawid\u0142owe has\u0142o" }, "step": { diff --git a/homeassistant/components/hangouts/translations/et.json b/homeassistant/components/hangouts/translations/et.json index b1c29f3577b..e8293aff79f 100644 --- a/homeassistant/components/hangouts/translations/et.json +++ b/homeassistant/components/hangouts/translations/et.json @@ -1,6 +1,8 @@ { "config": { "error": { + "invalid_2fa": "Vale 2-teguriline autentimine, proovige uuesti.", + "invalid_2fa_method": "Kehtetu 2FA meetod (kontrollige telefoni teel).", "invalid_login": "Vale Kasutajanimi, palun proovige uuesti." }, "step": { diff --git a/homeassistant/components/homekit_controller/translations/et.json b/homeassistant/components/homekit_controller/translations/et.json new file mode 100644 index 00000000000..31788215005 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/et.json @@ -0,0 +1,22 @@ +{ + "device_automation": { + "trigger_subtype": { + "button1": "Nupp 1", + "button10": "Nupp 10", + "button2": "Nupp 2", + "button3": "Nupp 3", + "button4": "Nupp 4", + "button5": "Nupp 5", + "button6": "Nupp 6", + "button7": "Nupp 7", + "button8": "Nupp 8", + "button9": "Nupp 9", + "doorbell": "Uksekell" + }, + "trigger_type": { + "double_press": "\" {subtype} \" tehtud topeltkl\u00f5ps", + "long_press": "\" {subtype} \" on pikalt alla vajutatud", + "single_press": "\" {subtype} \" on vajutatud" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/lb.json b/homeassistant/components/homekit_controller/translations/lb.json index 5431b3b30d1..c4c0d41117a 100644 --- a/homeassistant/components/homekit_controller/translations/lb.json +++ b/homeassistant/components/homekit_controller/translations/lb.json @@ -53,5 +53,19 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button1": "Kn\u00e4ppchen 1", + "button10": "Kn\u00e4ppchen 10", + "button2": "Kn\u00e4ppchen 2", + "button3": "Kn\u00e4ppchen 3", + "button4": "Kn\u00e4ppchen 4", + "button5": "Kn\u00e4ppchen 5", + "button6": "Kn\u00e4ppchen 6", + "button7": "Kn\u00e4ppchen 7", + "button8": "Kn\u00e4ppchen 8", + "button9": "Kn\u00e4ppchen 9" + } + }, "title": "HomeKit Kontroller" } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/lb.json b/homeassistant/components/homematicip_cloud/translations/lb.json index 80892a3e282..92487c12ea6 100644 --- a/homeassistant/components/homematicip_cloud/translations/lb.json +++ b/homeassistant/components/homematicip_cloud/translations/lb.json @@ -6,6 +6,7 @@ "unknown": "Onbekannten Feeler opgetrueden" }, "error": { + "invalid_pin": "Ong\u00ebltege Pin, prob\u00e9ier 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.", diff --git a/homeassistant/components/hue/translations/et.json b/homeassistant/components/hue/translations/et.json index 842040251dd..0aeea7286d9 100644 --- a/homeassistant/components/hue/translations/et.json +++ b/homeassistant/components/hue/translations/et.json @@ -21,5 +21,26 @@ "description": "Vajutage silla nuppu, et registreerida Philips Hue Home Assistant abil. \n\n ! [Nupu asukoht sillal] (/ static / images / config_philips_hue.jpg)" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Esimene nupp", + "button_2": "Teine nupp", + "button_3": "Kolmas nupp", + "button_4": "Neljas nupp", + "dim_down": "H\u00e4marda", + "dim_up": "Tee heledamaks", + "double_buttons_1_3": "Esimene ja kolmas nupp", + "double_buttons_2_4": "Teine ja neljas nupp", + "turn_off": "L\u00fclita v\u00e4lja", + "turn_on": "L\u00fclita sisse" + }, + "trigger_type": { + "remote_button_long_release": "\"{subtype}\" nupp vabastatati p\u00e4rast pikka vajutust", + "remote_button_short_press": "\"{subtype}\" nupp on vajutatud", + "remote_button_short_release": "\"{subtype}\" nupp vabastati", + "remote_double_button_long_press": "M\u00f5lemad \"{subtype}\" nupud vabastatati p\u00e4rast pikka vajutust", + "remote_double_button_short_press": "M\u00f5lemad \"{subtype}\" nupud vabastatati" + } } } \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/et.json b/homeassistant/components/humidifier/translations/et.json new file mode 100644 index 00000000000..303edb781b6 --- /dev/null +++ b/homeassistant/components/humidifier/translations/et.json @@ -0,0 +1,21 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "M\u00e4\u00e4ra {entity_name} niiskus", + "set_mode": "Muuda {entity_name} t\u00f6\u00f6re\u017eiimi", + "toggle": "Muuda {entity_name} olekut", + "turn_off": "L\u00fclita {entity_name} v\u00e4lja", + "turn_on": "L\u00fclita {entity_name} sisse" + }, + "condition_type": { + "is_mode": "{entity_name} on seatud kindlale t\u00f6\u00f6re\u017eiimile", + "is_off": "{entity_name} on v\u00e4lja l\u00fclitatud", + "is_on": "{entity_name} on sisse l\u00fclitatud" + }, + "trigger_type": { + "target_humidity_changed": "{entity_name} eelseatud niiskus muutus", + "turned_off": "{entity_name} l\u00fclitus v\u00e4lja", + "turned_on": "{entity_name} l\u00fclitus sisse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/translations/et.json b/homeassistant/components/ifttt/translations/et.json new file mode 100644 index 00000000000..6425b216b9e --- /dev/null +++ b/homeassistant/components/ifttt/translations/et.json @@ -0,0 +1,7 @@ +{ + "config": { + "create_entry": { + "default": "S\u00fcndmuste saatmiseks Home Assistantile peate kasutama toimingut \"Make a web request\" [IFTTT Webhooki apletilt] ({applet_url}).\n\nSisestage j\u00e4rgmine teave:\n\n- URL: {webhook_url}.\n- Method: POST\n- Content Type: application/json\n\nVaadake [dokumentatsiooni]({docs_url}) kuidas seadistada sissetulevate andmete t\u00f6\u00f6tlemiseks automatiseerimisi." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/et.json b/homeassistant/components/ipma/translations/et.json new file mode 100644 index 00000000000..32fab1be8df --- /dev/null +++ b/homeassistant/components/ipma/translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad" + }, + "title": "Asukoht" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/et.json b/homeassistant/components/kodi/translations/et.json new file mode 100644 index 00000000000..9d7c8e2a028 --- /dev/null +++ b/homeassistant/components/kodi/translations/et.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_off": "{entity_name} paluti v\u00e4lja l\u00fclitada", + "turn_on": "{entity_name} paluti sisse l\u00fclitada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/translations/et.json b/homeassistant/components/light/translations/et.json index 59e0b904637..f137faa0bf7 100644 --- a/homeassistant/components/light/translations/et.json +++ b/homeassistant/components/light/translations/et.json @@ -1,6 +1,9 @@ { "device_automation": { "action_type": { + "brightness_decrease": "V\u00e4henda {entity_name} heledust", + "brightness_increase": "Suurenda{entity_name} heledust", + "flash": "Vilguta {entity_name}", "toggle": "Muuda {entity_name} olekut", "turn_off": "L\u00fclita {entity_name} v\u00e4lja", "turn_on": "L\u00fclita {entity_name} sisse" diff --git a/homeassistant/components/meteo_france/translations/pl.json b/homeassistant/components/meteo_france/translations/pl.json index f90dcaa9f89..8ff70e51de5 100644 --- a/homeassistant/components/meteo_france/translations/pl.json +++ b/homeassistant/components/meteo_france/translations/pl.json @@ -8,7 +8,8 @@ "cities": { "data": { "city": "Miasto" - } + }, + "description": "Wybierz swoje miasto z listy" }, "user": { "data": { diff --git a/homeassistant/components/metoffice/translations/et.json b/homeassistant/components/metoffice/translations/et.json new file mode 100644 index 00000000000..c6ad082c40e --- /dev/null +++ b/homeassistant/components/metoffice/translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad" + }, + "description": "Laius- ja pikkuskraadi kasutatakse l\u00e4hima ilmajaama leidmiseks." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/et.json b/homeassistant/components/mqtt/translations/et.json index 78e88cdc854..e8a9fac81d7 100644 --- a/homeassistant/components/mqtt/translations/et.json +++ b/homeassistant/components/mqtt/translations/et.json @@ -40,12 +40,12 @@ "trigger_type": { "button_double_press": "\" {subtype} \" on topeltkl\u00f5psatud", "button_long_press": "\" {subtype} \" on pikalt alla vajutatud", - "button_long_release": "\"{alamt\u00fc\u00fcp}\" vabastatati p\u00e4rast pikka vajutust", - "button_quadruple_press": "\"{alamt\u00fc\u00fcp}\" on neljakordselt kl\u00f5psatud", - "button_quintuple_press": "\"{alamt\u00fc\u00fcp}\" on viiekordselt kl\u00f5psatud", + "button_long_release": "\"{subtype}\" vabastatati p\u00e4rast pikka vajutust", + "button_quadruple_press": "\"{subtype}\" on neljakordselt kl\u00f5psatud", + "button_quintuple_press": "\"{subtype}\" on viiekordselt kl\u00f5psatud", "button_short_press": "\u201e {subtype} \u201d on vajutatud", "button_short_release": "\" {subtype} \" vabastati", - "button_triple_press": "\"{alamt\u00fc\u00fcp}\" on kolmekordselt kl\u00f5psatud" + "button_triple_press": "\"{subtype}\" on kolmekordselt kl\u00f5psatud" } }, "options": { diff --git a/homeassistant/components/netatmo/translations/et.json b/homeassistant/components/netatmo/translations/et.json new file mode 100644 index 00000000000..cf0944dbe0e --- /dev/null +++ b/homeassistant/components/netatmo/translations/et.json @@ -0,0 +1,27 @@ +{ + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "Ala nimi", + "lat_ne": "Kirdenurga laiuskraad", + "lat_sw": "Edelanurga laiuskraad", + "lon_ne": "Kirdenurga pikkuskraad", + "lon_sw": "Edelanurga pikkuskraad", + "mode": "Arvutamine", + "show_on_map": "Kuva kaardil" + }, + "description": "Seadista selle ala avalik ilmaandur.", + "title": "Netatmo avalik ilmaandur" + }, + "public_weather_areas": { + "data": { + "new_area": "Ala nimi", + "weather_areas": "Ilmaandmete alad" + }, + "description": "Seadista avalikke ilmastikuandureid.", + "title": "Netatmo avalik ilmaandur" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/et.json b/homeassistant/components/nws/translations/et.json new file mode 100644 index 00000000000..a9607835b43 --- /dev/null +++ b/homeassistant/components/nws/translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad" + }, + "title": "\u00dchendu riikliku ilmateenistusega (USA)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/et.json b/homeassistant/components/openuv/translations/et.json new file mode 100644 index 00000000000..aae3ef835bb --- /dev/null +++ b/homeassistant/components/openuv/translations/et.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/lb.json b/homeassistant/components/openweathermap/translations/lb.json new file mode 100644 index 00000000000..0f1669a5050 --- /dev/null +++ b/homeassistant/components/openweathermap/translations/lb.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "auth": "Api Schl\u00ebssel ass net korrekt." + }, + "step": { + "user": { + "data": { + "language": "Sproch", + "latitude": "Breedegrad", + "longitude": "L\u00e4ngegrad" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/remote/translations/et.json b/homeassistant/components/remote/translations/et.json index 6bcfbf7f4cf..458704b9999 100644 --- a/homeassistant/components/remote/translations/et.json +++ b/homeassistant/components/remote/translations/et.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "Muuda {entity_name} olekut", + "turn_off": "L\u00fclita {entity_name} v\u00e4lja", + "turn_on": "L\u00fclita {entity_name} sisse" + }, + "condition_type": { + "is_off": "{entity_name} on v\u00e4lja l\u00fclitatud", + "is_on": "{entity_name} on sisse l\u00fclitatud" + }, + "trigger_type": { + "turned_off": "{entity_name} l\u00fclitus v\u00e4lja", + "turned_on": "{entity_name} l\u00fclitus sisse" + } + }, "state": { "_": { "off": "V\u00e4ljas", diff --git a/homeassistant/components/remote/translations/lb.json b/homeassistant/components/remote/translations/lb.json index b81e82470fc..b5d8f7f6ecf 100644 --- a/homeassistant/components/remote/translations/lb.json +++ b/homeassistant/components/remote/translations/lb.json @@ -1,4 +1,17 @@ { + "device_automation": { + "action_type": { + "turn_on": "{entity_name} uschalten" + }, + "condition_type": { + "is_off": "{entity_name} ass ausgeschalt", + "is_on": "{entity_name} ass un" + }, + "trigger_type": { + "turned_off": "{entity_name} gouf ausgeschalt", + "turned_on": "{entity_name} gouf ugeschalt" + } + }, "state": { "_": { "off": "Aus", diff --git a/homeassistant/components/sensor/translations/et.json b/homeassistant/components/sensor/translations/et.json index d45ba964d70..450f5b60537 100644 --- a/homeassistant/components/sensor/translations/et.json +++ b/homeassistant/components/sensor/translations/et.json @@ -2,25 +2,33 @@ "device_automation": { "condition_type": { "is_battery_level": "Praegune {entity_name} aku tase", + "is_current": "Praegune {entity_name} voolutugevus", + "is_energy": "Praegune {entity_name} v\u00f5imsus", "is_humidity": "Praegune {entity_name} niiskus", "is_illuminance": "Praegune {entity_name} valgustatus", "is_power": "Praegune {entity_name} toide (v\u00f5imsus)", + "is_power_factor": "Praegune {entity_name} v\u00f5imsusfaktor", "is_pressure": "Praegune {entity_name} r\u00f5hk", "is_signal_strength": "Praegune {entity_name} signaali tugevus", "is_temperature": "Praegune {entity_name} temperatuur", "is_timestamp": "Praegune {entity_name} aeg", - "is_value": "Praegune {entity_name} v\u00e4\u00e4rtus" + "is_value": "Praegune {entity_name} v\u00e4\u00e4rtus", + "is_voltage": "Praegune {entity_name}pinge" }, "trigger_type": { "battery_level": "{entity_name} aku tase muutub", + "current": "{entity_name} voolutugevus muutub", + "energy": "{entity_name} v\u00f5imsus muutub", "humidity": "{entity_name} niiskus muutub", "illuminance": "{entity_name} valgustustugevus muutub", "power": "{entity_name} energiare\u017eiimi muutub", + "power_factor": "{entity_name} v\u00f5imsus muutub", "pressure": "{entity_name} r\u00f5hk muutub", "signal_strength": "{entity_name} signaalitugevus muutub", "temperature": "{entity_name} temperatuur muutub", "timestamp": "{entity_name} aeg muutub", - "value": "{entity_name} v\u00e4\u00e4rtus muutub" + "value": "{entity_name} v\u00e4\u00e4rtus muutub", + "voltage": "{entity_name} pingemuutub" } }, "state": { diff --git a/homeassistant/components/smappee/translations/pl.json b/homeassistant/components/smappee/translations/pl.json index 82d13f6f5e5..921f80323f9 100644 --- a/homeassistant/components/smappee/translations/pl.json +++ b/homeassistant/components/smappee/translations/pl.json @@ -6,6 +6,11 @@ "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." }, "step": { + "environment": { + "data": { + "environment": "\u015arodowisko" + } + }, "local": { "data": { "host": "Nazwa hosta lub adres IP" diff --git a/homeassistant/components/smartthings/translations/et.json b/homeassistant/components/smartthings/translations/et.json new file mode 100644 index 00000000000..91299004ed3 --- /dev/null +++ b/homeassistant/components/smartthings/translations/et.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "select_location": { + "data": { + "location_id": "Asukoht" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/translations/et.json b/homeassistant/components/smhi/translations/et.json index e972172e9d8..984b43015d7 100644 --- a/homeassistant/components/smhi/translations/et.json +++ b/homeassistant/components/smhi/translations/et.json @@ -1,12 +1,16 @@ { "config": { + "error": { + "wrong_location": "Asukoht saab olla ainult Rootsis" + }, "step": { "user": { "data": { "latitude": "Laiuskraad", "longitude": "Pikkuskraad", "name": "Nimi" - } + }, + "title": "Asukoht Rootsis" } } } diff --git a/homeassistant/components/switch/translations/et.json b/homeassistant/components/switch/translations/et.json index 404f9c961bf..d68938ddda0 100644 --- a/homeassistant/components/switch/translations/et.json +++ b/homeassistant/components/switch/translations/et.json @@ -1,8 +1,17 @@ { "device_automation": { + "action_type": { + "toggle": "Muuda {entity_name} olekut", + "turn_off": "L\u00fclita {entity_name} v\u00e4lja", + "turn_on": "L\u00fclita {entity_name} sisse" + }, "condition_type": { "is_off": "{entity_name} on v\u00e4lja l\u00fclitatud", "is_on": "{entity_name} on sisse l\u00fclitatud" + }, + "trigger_type": { + "turned_off": "{entity_name} l\u00fclitus v\u00e4lja", + "turned_on": "{entity_name} l\u00fclitus sisse" } }, "state": { diff --git a/homeassistant/components/vacuum/translations/et.json b/homeassistant/components/vacuum/translations/et.json index 32d361151b8..fbdbe330b83 100644 --- a/homeassistant/components/vacuum/translations/et.json +++ b/homeassistant/components/vacuum/translations/et.json @@ -25,5 +25,5 @@ "returning": "P\u00f6\u00f6rdun tagasi laadimisjaama" } }, - "title": "T\u00fchjenda" + "title": "Tolmuimeja" } \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/lb.json b/homeassistant/components/yeelight/translations/lb.json new file mode 100644 index 00000000000..c7cf37887ef --- /dev/null +++ b/homeassistant/components/yeelight/translations/lb.json @@ -0,0 +1,3 @@ +{ + "title": "Yeelight" +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index e72c3dea7b0..27a7113e3d2 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -1,7 +1,57 @@ { + "config": { + "step": { + "port_config": { + "title": "Seaded" + } + } + }, "device_automation": { "action_type": { "warn": "Hoiata" + }, + "trigger_subtype": { + "both_buttons": "M\u00f5lemad nupud", + "button_1": "Esimene nupp", + "button_2": "Teine nupp", + "button_3": "Kolmas nupp", + "button_4": "Neljas nupp", + "button_5": "Viies nupp", + "button_6": "Kuues nupp", + "close": "Sulge", + "dim_down": "H\u00e4marda", + "dim_up": "Tee heledamaks", + "left": "Vasakule", + "open": "Ava", + "right": "Paremale", + "turn_off": "L\u00fclita v\u00e4lja", + "turn_on": "L\u00fclita sisse" + }, + "trigger_type": { + "device_dropped": "Seade kukkus", + "device_flipped": "Seade \" {subtype} \" p\u00f6\u00f6rati \u00fcmber", + "device_knocked": "Seadet \" {subtype} \" koputati", + "device_offline": "Seade on v\u00f5rgu\u00fchenduseta", + "device_rotated": "Seadet \" {subtype} \" keerati", + "device_shaken": "Seadet raputati", + "device_slid": "Seade \" {subtype} \" libises", + "device_tilted": "Seadet kallutati", + "remote_button_alt_double_press": "\"{subtype}\" on topeltkl\u00f5psatud (alternatiivre\u017eiim)", + "remote_button_alt_long_press": "\"{subtype}\" nuppu vajutati pikalt (alternatiivre\u017eiim)", + "remote_button_alt_long_release": "\"{subtype}\" nupp vabastati peale pikka vajutust (alternatiivre\u017eiim)", + "remote_button_alt_quadruple_press": "\"{subtype}\" on neljakordselt kl\u00f5psatud (alternatiivre\u017eiim)", + "remote_button_alt_quintuple_press": "\"{subtype}\" on neljakordselt kl\u00f5psatud (alternatiivre\u017eiim)", + "remote_button_alt_short_press": "\"{subtype}\" nuppu vajutati (alternatiivre\u017eiim)", + "remote_button_alt_short_release": "\"{subtype}\" nupp vabastati (alternatiivre\u017eiim)", + "remote_button_alt_triple_press": "\"{subtype}\" on kolmekordselt kl\u00f5psatud (alternatiivre\u017eiim)", + "remote_button_double_press": "\" {subtype} \" on topeltkl\u00f5psatud", + "remote_button_long_press": "\" {subtype} \" on pikalt alla vajutatud", + "remote_button_long_release": "\"{subtype}\" nupp vabastatati p\u00e4rast pikka vajutust", + "remote_button_quadruple_press": "\"{subtype}\" nuppu on neljakordselt kl\u00f5psatud", + "remote_button_quintuple_press": "\"{subtype}\" nuppu on viiekordselt kl\u00f5psatud", + "remote_button_short_press": "\"{subtype}\" nupp on vajutatud", + "remote_button_short_release": "\"{subtype}\" nupp vabastati", + "remote_button_triple_press": "Nuppu \"{subtype}\" kl\u00f5psati kolm korda" } } } \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/sensor.lb.json b/homeassistant/components/zodiac/translations/sensor.lb.json new file mode 100644 index 00000000000..65ae5095c39 --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.lb.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Waassermann", + "aries": "Widder", + "cancer": "Kriibs", + "capricorn": "Steebock", + "gemini": "Zwillinge", + "leo": "L\u00e9iw", + "libra": "Wo", + "pisces": "F\u00ebsch", + "sagittarius": "Sch\u00ebtz", + "scorpio": "Skorpioun", + "taurus": "St\u00e9ier", + "virgo": "Jongfra" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/sensor.no.json b/homeassistant/components/zodiac/translations/sensor.no.json new file mode 100644 index 00000000000..dea02eb8ce7 --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.no.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Vannmannen", + "aries": "V\u00e6ren", + "cancer": "Kreft", + "capricorn": "Steinbukken", + "gemini": "Tvillingene", + "leo": "L\u00f8ven", + "libra": "Vekten", + "pisces": "Fiskene", + "sagittarius": "Skytten", + "scorpio": "Skorpionen", + "taurus": "Tyren", + "virgo": "Jomfruen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/sensor.pl.json b/homeassistant/components/zodiac/translations/sensor.pl.json new file mode 100644 index 00000000000..7aecf4724a1 --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.pl.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Wodnik", + "aries": "Baran", + "cancer": "Rak", + "capricorn": "Kozioro\u017cec", + "gemini": "Bli\u017ani\u0119ta", + "leo": "Lew", + "libra": "Waga", + "pisces": "Ryby", + "sagittarius": "Strzelec", + "scorpio": "Skorpion", + "taurus": "Byk", + "virgo": "Panna" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/sensor.zh-Hant.json b/homeassistant/components/zodiac/translations/sensor.zh-Hant.json new file mode 100644 index 00000000000..938a5b6cbe5 --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.zh-Hant.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "\u6c34\u74f6\u5ea7", + "aries": "\u7261\u7f8a\u5ea7", + "cancer": "\u5de8\u87f9\u5ea7", + "capricorn": "\u6469\u7faf\u5ea7", + "gemini": "\u96d9\u5b50\u5ea7", + "leo": "\u7345\u5b50\u5ea7", + "libra": "\u5929\u79e4\u5ea7", + "pisces": "\u96d9\u9b5a\u5ea7", + "sagittarius": "\u5c04\u624b\u5ea7", + "scorpio": "\u5929\u880d\u5ea7", + "taurus": "\u91d1\u725b\u5ea7", + "virgo": "\u8655\u5973\u5ea7" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/translations/et.json b/homeassistant/components/zone/translations/et.json index aa921f376e7..8a319fb7c1d 100644 --- a/homeassistant/components/zone/translations/et.json +++ b/homeassistant/components/zone/translations/et.json @@ -4,13 +4,14 @@ "init": { "data": { "icon": "Ikoon", - "latitude": "Laius", - "longitude": "Pikkus", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", "name": "Nimi", "radius": "Raadius" }, - "title": "M\u00e4\u00e4ra tsooni parameetrid" + "title": "M\u00e4\u00e4ra ala parameetrid" } - } + }, + "title": "Ala" } } \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/lb.json b/homeassistant/components/zoneminder/translations/lb.json new file mode 100644 index 00000000000..29b4bffe466 --- /dev/null +++ b/homeassistant/components/zoneminder/translations/lb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "verify_ssl": "SSL Zertifikat iwwerpr\u00e9iwen" + } + } + } + } +} \ No newline at end of file From 511ea09c9936c501219e7697bfb8ffee760e287c Mon Sep 17 00:00:00 2001 From: Markus Haack Date: Wed, 23 Sep 2020 04:04:01 +0200 Subject: [PATCH 302/514] Guard SolarEdge for inverters without batteries (#40295) --- homeassistant/components/solaredge/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 3888b8bf536..2b085d1ba40 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -267,7 +267,8 @@ class SolarEdgeStorageLevelSensor(SolarEdgeSensor): """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"] + if attr and "soc" in attr: + self._state = attr["soc"] class SolarEdgeDataService: From 06a133c3e9a47db3f5bfbafbe0ad65c9c360d0ac Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 23 Sep 2020 04:09:17 +0200 Subject: [PATCH 303/514] Add and use length millimeters constant (#40116) * Add and use length millimeters constant * Fix pylint error * Fix broken accuweather sensor test --- homeassistant/components/accuweather/const.py | 4 +- homeassistant/components/bom/sensor.py | 3 +- homeassistant/components/buienradar/sensor.py | 45 ++++++++++--------- homeassistant/components/darksky/sensor.py | 17 +++---- homeassistant/components/homematic/sensor.py | 3 +- .../components/homematicip_cloud/sensor.py | 3 +- homeassistant/components/isy994/const.py | 7 +-- .../components/meteo_france/const.py | 3 +- homeassistant/components/netatmo/sensor.py | 7 +-- .../components/openweathermap/const.py | 5 ++- .../components/tellduslive/sensor.py | 10 ++++- homeassistant/components/tof/sensor.py | 4 +- .../trafikverket_weatherstation/sensor.py | 3 +- .../components/wunderground/sensor.py | 33 +++++++++++--- homeassistant/const.py | 1 + tests/components/accuweather/test_sensor.py | 4 +- .../homematicip_cloud/test_sensor.py | 3 +- 17 files changed, 98 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index e572feafcf8..5696a35ea2f 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -5,6 +5,7 @@ from homeassistant.const import ( LENGTH_FEET, LENGTH_INCHES, LENGTH_METERS, + LENGTH_MILLIMETERS, PERCENTAGE, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR, @@ -24,7 +25,6 @@ ATTR_UNIT_METRIC = "Metric" CONCENTRATION_PARTS_PER_CUBIC_METER = f"p/{VOLUME_CUBIC_METERS}" COORDINATOR = "coordinator" DOMAIN = "accuweather" -LENGTH_MILIMETERS = "mm" MANUFACTURER = "AccuWeather, Inc." NAME = "AccuWeather" UNDO_UPDATE_LISTENER = "undo_update_listener" @@ -238,7 +238,7 @@ SENSOR_TYPES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:weather-rainy", ATTR_LABEL: "Precipitation", - ATTR_UNIT_METRIC: LENGTH_MILIMETERS, + ATTR_UNIT_METRIC: LENGTH_MILLIMETERS, ATTR_UNIT_IMPERIAL: LENGTH_INCHES, }, "PressureTendency": { diff --git a/homeassistant/components/bom/sensor.py b/homeassistant/components/bom/sensor.py index 848be51cc8f..12f43430829 100644 --- a/homeassistant/components/bom/sensor.py +++ b/homeassistant/components/bom/sensor.py @@ -21,6 +21,7 @@ from homeassistant.const import ( CONF_NAME, LENGTH_KILOMETERS, LENGTH_METERS, + LENGTH_MILLIMETERS, PERCENTAGE, PRESSURE_MBAR, SPEED_KILOMETERS_PER_HOUR, @@ -71,7 +72,7 @@ SENSOR_TYPES = { "press_qnh": ["Pressure qnh", "qnh"], "press_msl": ["Pressure msl", "msl"], "press_tend": ["Pressure Tend", None], - "rain_trace": ["Rain Today", "mm"], + "rain_trace": ["Rain Today", LENGTH_MILLIMETERS], "rel_hum": ["Relative Humidity", PERCENTAGE], "sea_state": ["Sea State", None], "swell_dir_worded": ["Swell Direction", None], diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index d1d1f0cf632..b1e41122dc0 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -30,6 +30,7 @@ from homeassistant.const import ( DEGREE, IRRADIATION_WATTS_PER_SQUARE_METER, LENGTH_KILOMETERS, + LENGTH_MILLIMETERS, PERCENTAGE, PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, @@ -82,22 +83,26 @@ SENSOR_TYPES = { "pressure": ["Pressure", PRESSURE_HPA, "mdi:gauge"], "visibility": ["Visibility", LENGTH_KILOMETERS, None], "windgust": ["Wind gust", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy"], - "precipitation": ["Precipitation", f"mm/{TIME_HOURS}", "mdi:weather-pouring"], + "precipitation": [ + "Precipitation", + f"{LENGTH_MILLIMETERS}/{TIME_HOURS}", + "mdi:weather-pouring", + ], "irradiance": ["Irradiance", IRRADIATION_WATTS_PER_SQUARE_METER, "mdi:sunglasses"], "precipitation_forecast_average": [ "Precipitation forecast average", - f"mm/{TIME_HOURS}", + f"{LENGTH_MILLIMETERS}/{TIME_HOURS}", "mdi:weather-pouring", ], "precipitation_forecast_total": [ "Precipitation forecast total", - "mm", + LENGTH_MILLIMETERS, "mdi:weather-pouring", ], # new in json api (>1.0.0): - "rainlast24hour": ["Rain last 24h", "mm", "mdi:weather-pouring"], + "rainlast24hour": ["Rain last 24h", LENGTH_MILLIMETERS, "mdi:weather-pouring"], # new in json api (>1.0.0): - "rainlasthour": ["Rain last hour", "mm", "mdi:weather-pouring"], + "rainlasthour": ["Rain last hour", LENGTH_MILLIMETERS, "mdi:weather-pouring"], "temperature_1d": ["Temperature 1d", TEMP_CELSIUS, "mdi:thermometer"], "temperature_2d": ["Temperature 2d", TEMP_CELSIUS, "mdi:thermometer"], "temperature_3d": ["Temperature 3d", TEMP_CELSIUS, "mdi:thermometer"], @@ -108,23 +113,23 @@ SENSOR_TYPES = { "mintemp_3d": ["Minimum temperature 3d", TEMP_CELSIUS, "mdi:thermometer"], "mintemp_4d": ["Minimum temperature 4d", TEMP_CELSIUS, "mdi:thermometer"], "mintemp_5d": ["Minimum temperature 5d", TEMP_CELSIUS, "mdi:thermometer"], - "rain_1d": ["Rain 1d", "mm", "mdi:weather-pouring"], - "rain_2d": ["Rain 2d", "mm", "mdi:weather-pouring"], - "rain_3d": ["Rain 3d", "mm", "mdi:weather-pouring"], - "rain_4d": ["Rain 4d", "mm", "mdi:weather-pouring"], - "rain_5d": ["Rain 5d", "mm", "mdi:weather-pouring"], + "rain_1d": ["Rain 1d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], + "rain_2d": ["Rain 2d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], + "rain_3d": ["Rain 3d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], + "rain_4d": ["Rain 4d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], + "rain_5d": ["Rain 5d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], # new in json api (>1.0.0): - "minrain_1d": ["Minimum rain 1d", "mm", "mdi:weather-pouring"], - "minrain_2d": ["Minimum rain 2d", "mm", "mdi:weather-pouring"], - "minrain_3d": ["Minimum rain 3d", "mm", "mdi:weather-pouring"], - "minrain_4d": ["Minimum rain 4d", "mm", "mdi:weather-pouring"], - "minrain_5d": ["Minimum rain 5d", "mm", "mdi:weather-pouring"], + "minrain_1d": ["Minimum rain 1d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], + "minrain_2d": ["Minimum rain 2d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], + "minrain_3d": ["Minimum rain 3d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], + "minrain_4d": ["Minimum rain 4d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], + "minrain_5d": ["Minimum rain 5d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], # new in json api (>1.0.0): - "maxrain_1d": ["Maximum rain 1d", "mm", "mdi:weather-pouring"], - "maxrain_2d": ["Maximum rain 2d", "mm", "mdi:weather-pouring"], - "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"], + "maxrain_1d": ["Maximum rain 1d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], + "maxrain_2d": ["Maximum rain 2d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], + "maxrain_3d": ["Maximum rain 3d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], + "maxrain_4d": ["Maximum rain 4d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], + "maxrain_5d": ["Maximum rain 5d", LENGTH_MILLIMETERS, "mdi:weather-pouring"], "rainchance_1d": ["Rainchance 1d", PERCENTAGE, "mdi:weather-pouring"], "rainchance_2d": ["Rainchance 2d", PERCENTAGE, "mdi:weather-pouring"], "rainchance_3d": ["Rainchance 3d", PERCENTAGE, "mdi:weather-pouring"], diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index 48ceffbac50..49b5c6c69f6 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import ( DEGREE, LENGTH_CENTIMETERS, LENGTH_KILOMETERS, + LENGTH_MILLIMETERS, PERCENTAGE, PRESSURE_MBAR, SPEED_KILOMETERS_PER_HOUR, @@ -110,11 +111,11 @@ SENSOR_TYPES = { ], "precip_intensity": [ "Precip Intensity", - f"mm/{TIME_HOURS}", + f"{LENGTH_MILLIMETERS}/{TIME_HOURS}", "in", - f"mm/{TIME_HOURS}", - f"mm/{TIME_HOURS}", - f"mm/{TIME_HOURS}", + f"{LENGTH_MILLIMETERS}/{TIME_HOURS}", + f"{LENGTH_MILLIMETERS}/{TIME_HOURS}", + f"{LENGTH_MILLIMETERS}/{TIME_HOURS}", "mdi:weather-rainy", ["currently", "minutely", "hourly", "daily"], ], @@ -330,11 +331,11 @@ SENSOR_TYPES = { ], "precip_intensity_max": [ "Daily Max Precip Intensity", - f"mm/{TIME_HOURS}", + f"{LENGTH_MILLIMETERS}/{TIME_HOURS}", "in", - f"mm/{TIME_HOURS}", - f"mm/{TIME_HOURS}", - f"mm/{TIME_HOURS}", + f"{LENGTH_MILLIMETERS}/{TIME_HOURS}", + f"{LENGTH_MILLIMETERS}/{TIME_HOURS}", + f"{LENGTH_MILLIMETERS}/{TIME_HOURS}", "mdi:thermometer", ["daily"], ], diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 121b3af5d81..09ceb7c1da2 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -9,6 +9,7 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, ENERGY_WATT_HOUR, FREQUENCY_HERTZ, + LENGTH_MILLIMETERS, PERCENTAGE, POWER_WATT, PRESSURE_HPA, @@ -55,7 +56,7 @@ HM_UNIT_HA_CAST = { "AVERAGE_ILLUMINATION": "lx", "LOWEST_ILLUMINATION": "lx", "HIGHEST_ILLUMINATION": "lx", - "RAIN_COUNTER": "mm", + "RAIN_COUNTER": LENGTH_MILLIMETERS, "WIND_SPEED": SPEED_KILOMETERS_PER_HOUR, "WIND_DIRECTION": DEGREE, "WIND_DIRECTION_RANGE": DEGREE, diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 86881f565ce..f307fb9274e 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -30,6 +30,7 @@ from homeassistant.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + LENGTH_MILLIMETERS, PERCENTAGE, POWER_WATT, SPEED_KILOMETERS_PER_HOUR, @@ -367,7 +368,7 @@ class HomematicipTodayRainSensor(HomematicipGenericEntity): @property def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" - return "mm" + return LENGTH_MILLIMETERS class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity): diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 7724420f391..7bef122f7d3 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -57,6 +57,7 @@ from homeassistant.const import ( LENGTH_KILOMETERS, LENGTH_METERS, LENGTH_MILES, + LENGTH_MILLIMETERS, MASS_KILOGRAMS, MASS_POUNDS, PERCENTAGE, @@ -361,7 +362,7 @@ UOM_FRIENDLY_NAME = { "43": "mV", "44": TIME_MINUTES, "45": TIME_MINUTES, - "46": f"mm/{TIME_HOURS}", + "46": f"{LENGTH_MILLIMETERS}/{TIME_HOURS}", "47": TIME_MONTHS, "48": SPEED_MILES_PER_HOUR, "49": SPEED_METERS_PER_SECOND, @@ -388,7 +389,7 @@ UOM_FRIENDLY_NAME = { "75": "weekday", "76": DEGREE, "77": TIME_YEARS, - "82": "mm", + "82": LENGTH_MILLIMETERS, "83": LENGTH_KILOMETERS, "85": "Ω", "86": "kΩ", @@ -404,7 +405,7 @@ UOM_FRIENDLY_NAME = { "103": CURRENCY_DOLLAR, "104": CURRENCY_CENT, "105": LENGTH_INCHES, - "106": f"mm/{TIME_DAYS}", + "106": f"{LENGTH_MILLIMETERS}/{TIME_DAYS}", "107": "", # raw 1-byte unsigned value "108": "", # raw 2-byte unsigned value "109": "", # raw 3-byte unsigned value diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index 8b4d3a33501..59524ed1a80 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -4,6 +4,7 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + LENGTH_MILLIMETERS, PERCENTAGE, PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, @@ -108,7 +109,7 @@ SENSOR_TYPES = { }, "precipitation": { ENTITY_NAME: "Daily precipitation", - ENTITY_UNIT: "mm", + ENTITY_UNIT: LENGTH_MILLIMETERS, ENTITY_ICON: "mdi:cup-water", ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 2368d54efdf..af41e01c7df 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, + LENGTH_MILLIMETERS, PERCENTAGE, PRESSURE_MBAR, SPEED_KILOMETERS_PER_HOUR, @@ -52,9 +53,9 @@ SENSOR_TYPES = { "pressure": ["Pressure", PRESSURE_MBAR, None, DEVICE_CLASS_PRESSURE], "noise": ["Noise", "dB", "mdi:volume-high", None], "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], + "rain": ["Rain", LENGTH_MILLIMETERS, "mdi:weather-rainy", None], + "sum_rain_1": ["Rain last hour", LENGTH_MILLIMETERS, "mdi:weather-rainy", None], + "sum_rain_24": ["Rain last 24h", LENGTH_MILLIMETERS, "mdi:weather-rainy", None], "battery_vp": ["Battery", "", "mdi:battery", None], "battery_lvl": ["Battery Level", "", "mdi:battery", None], "battery_percent": ["Battery Percent", PERCENTAGE, None, DEVICE_CLASS_BATTERY], diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index bc7a428f366..d2ea7f34d50 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -14,6 +14,7 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + LENGTH_MILLIMETERS, PERCENTAGE, PRESSURE_PA, SPEED_METERS_PER_SECOND, @@ -112,8 +113,8 @@ WEATHER_SENSOR_TYPES = { SENSOR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, }, 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_RAIN: {SENSOR_NAME: "Rain", SENSOR_UNIT: LENGTH_MILLIMETERS}, + ATTR_API_SNOW: {SENSOR_NAME: "Snow", SENSOR_UNIT: LENGTH_MILLIMETERS}, ATTR_API_CONDITION: {SENSOR_NAME: "Condition"}, ATTR_API_WEATHER_CODE: {SENSOR_NAME: "Weather Code"}, } diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py index c8f27a9412a..7b785a808e8 100644 --- a/homeassistant/components/tellduslive/sensor.py +++ b/homeassistant/components/tellduslive/sensor.py @@ -6,6 +6,7 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + LENGTH_MILLIMETERS, PERCENTAGE, POWER_WATT, SPEED_METERS_PER_SECOND, @@ -40,8 +41,13 @@ SENSOR_TYPES = { DEVICE_CLASS_TEMPERATURE, ], 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_RAINRATE: [ + "Rain rate", + f"{LENGTH_MILLIMETERS}/{TIME_HOURS}", + "mdi:water", + None, + ], + SENSOR_TYPE_RAINTOTAL: ["Rain total", LENGTH_MILLIMETERS, "mdi:water", None], SENSOR_TYPE_WINDDIRECTION: ["Wind direction", "", "", None], SENSOR_TYPE_WINDAVERAGE: ["Wind average", SPEED_METERS_PER_SECOND, "", None], SENSOR_TYPE_WINDGUST: ["Wind gust", SPEED_METERS_PER_SECOND, "", None], diff --git a/homeassistant/components/tof/sensor.py b/homeassistant/components/tof/sensor.py index 58f50f4899e..d9ad178cab2 100644 --- a/homeassistant/components/tof/sensor.py +++ b/homeassistant/components/tof/sensor.py @@ -9,14 +9,12 @@ import voluptuous as vol from homeassistant.components import rpi_gpio from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, LENGTH_MILLIMETERS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -LENGTH_MILLIMETERS = "mm" - CONF_I2C_ADDRESS = "i2c_address" CONF_I2C_BUS = "i2c_bus" CONF_XSHUT = "xshut" diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 958adfd6915..bb1bad67f82 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -17,6 +17,7 @@ from homeassistant.const import ( DEGREE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + LENGTH_MILLIMETERS, PERCENTAGE, SPEED_METERS_PER_SECOND, TEMP_CELSIUS, @@ -97,7 +98,7 @@ SENSOR_TYPES = { ], "precipitation_amount": [ "Precipitation amount", - "mm", + LENGTH_MILLIMETERS, "precipitation_amount", "mdi:cup-water", None, diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py index 6699c2f7e1d..e1bd79b7ea0 100644 --- a/homeassistant/components/wunderground/sensor.py +++ b/homeassistant/components/wunderground/sensor.py @@ -23,6 +23,7 @@ from homeassistant.const import ( LENGTH_INCHES, LENGTH_KILOMETERS, LENGTH_MILES, + LENGTH_MILLIMETERS, PERCENTAGE, PRESSURE_INHG, SPEED_KILOMETERS_PER_HOUR, @@ -392,7 +393,7 @@ SENSOR_TYPES = { "Precipitation 1hr", "precip_1hr_in", "mdi:umbrella", LENGTH_INCHES ), "precip_1hr_metric": WUCurrentConditionsSensorConfig( - "Precipitation 1hr", "precip_1hr_metric", "mdi:umbrella", "mm" + "Precipitation 1hr", "precip_1hr_metric", "mdi:umbrella", LENGTH_MILLIMETERS ), "precip_1hr_string": WUCurrentConditionsSensorConfig( "Precipitation 1hr", "precip_1hr_string", "mdi:umbrella" @@ -401,7 +402,7 @@ SENSOR_TYPES = { "Precipitation Today", "precip_today_in", "mdi:umbrella", LENGTH_INCHES ), "precip_today_metric": WUCurrentConditionsSensorConfig( - "Precipitation Today", "precip_today_metric", "mdi:umbrella", "mm" + "Precipitation Today", "precip_today_metric", "mdi:umbrella", LENGTH_MILLIMETERS ), "precip_today_string": WUCurrentConditionsSensorConfig( "Precipitation Today", "precip_today_string", "mdi:umbrella" @@ -879,16 +880,36 @@ SENSOR_TYPES = { "mdi:weather-windy", ), "precip_1d_mm": WUDailySimpleForecastSensorConfig( - "Precipitation Intensity Today", 0, "qpf_allday", "mm", "mm", "mdi:umbrella" + "Precipitation Intensity Today", + 0, + "qpf_allday", + LENGTH_MILLIMETERS, + LENGTH_MILLIMETERS, + "mdi:umbrella", ), "precip_2d_mm": WUDailySimpleForecastSensorConfig( - "Precipitation Intensity Tomorrow", 1, "qpf_allday", "mm", "mm", "mdi:umbrella" + "Precipitation Intensity Tomorrow", + 1, + "qpf_allday", + LENGTH_MILLIMETERS, + LENGTH_MILLIMETERS, + "mdi:umbrella", ), "precip_3d_mm": WUDailySimpleForecastSensorConfig( - "Precipitation Intensity in 3 Days", 2, "qpf_allday", "mm", "mm", "mdi:umbrella" + "Precipitation Intensity in 3 Days", + 2, + "qpf_allday", + LENGTH_MILLIMETERS, + LENGTH_MILLIMETERS, + "mdi:umbrella", ), "precip_4d_mm": WUDailySimpleForecastSensorConfig( - "Precipitation Intensity in 4 Days", 3, "qpf_allday", "mm", "mm", "mdi:umbrella" + "Precipitation Intensity in 4 Days", + 3, + "qpf_allday", + LENGTH_MILLIMETERS, + LENGTH_MILLIMETERS, + "mdi:umbrella", ), "precip_1d_in": WUDailySimpleForecastSensorConfig( "Precipitation Intensity Today", diff --git a/homeassistant/const.py b/homeassistant/const.py index f3bb26d3ef0..e1738cf6425 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -403,6 +403,7 @@ TIME_MONTHS = "m" TIME_YEARS = "y" # Length units +LENGTH_MILLIMETERS: str = "mm" LENGTH_CENTIMETERS: str = "cm" LENGTH_METERS: str = "m" LENGTH_KILOMETERS: str = "km" diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index b94d17066c8..4ce34dfebe9 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -6,7 +6,6 @@ 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 ( @@ -17,6 +16,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_TEMPERATURE, LENGTH_METERS, + LENGTH_MILLIMETERS, PERCENTAGE, SPEED_KILOMETERS_PER_HOUR, STATE_UNAVAILABLE, @@ -52,7 +52,7 @@ async def test_sensor_without_forecast(hass): 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_UNIT_OF_MEASUREMENT) == LENGTH_MILLIMETERS assert state.attributes.get(ATTR_ICON) == "mdi:weather-rainy" assert state.attributes.get("type") is None diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index 55d1e32bf2b..de9fc276795 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -24,6 +24,7 @@ from homeassistant.components.homematicip_cloud.sensor import ( from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + LENGTH_MILLIMETERS, PERCENTAGE, POWER_WATT, SPEED_KILOMETERS_PER_HOUR, @@ -337,7 +338,7 @@ async def test_hmip_today_rain_sensor(hass, default_mock_hap_factory): ) assert ha_state.state == "3.9" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "mm" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == LENGTH_MILLIMETERS await async_manipulate_test_data(hass, hmip_device, "todayRainCounter", 14.2) ha_state = hass.states.get(entity_id) assert ha_state.state == "14.2" From 72a7f69a088d919285a5b168e04627fba12d7d1d Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Wed, 23 Sep 2020 04:10:37 +0200 Subject: [PATCH 304/514] Update xknx to version 0.14.4 (#40472) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 2eb11511cdd..f231d11fa9f 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.14.3"], + "requirements": ["xknx==0.14.4"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver" } diff --git a/requirements_all.txt b/requirements_all.txt index 0b1ebc29ae0..4b6f5451abd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2265,7 +2265,7 @@ xboxapi==2.0.1 xfinity-gateway==0.0.4 # homeassistant.components.knx -xknx==0.14.3 +xknx==0.14.4 # homeassistant.components.bluesound # homeassistant.components.rest From 5ebce075a1b60579dfa68d6a0784b20087a44e1e Mon Sep 17 00:00:00 2001 From: lamiskin Date: Wed, 23 Sep 2020 12:43:21 +1000 Subject: [PATCH 305/514] Improve DOODS folder handling and add process time attribute (#40344) * Updates to DOODS * Fix import order --- homeassistant/components/doods/image_processing.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 4130f67ec13..f4180ffcffa 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -1,6 +1,7 @@ """Support for the DOODS service.""" import io import logging +import os import time from PIL import Image, ImageDraw, UnidentifiedImageError @@ -26,6 +27,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_MATCHES = "matches" ATTR_SUMMARY = "summary" ATTR_TOTAL_MATCHES = "total_matches" +ATTR_PROCESS_TIME = "process_time" CONF_URL = "url" CONF_AUTH_KEY = "auth_key" @@ -203,6 +205,7 @@ class Doods(ImageProcessingEntity): self._matches = {} self._total_matches = 0 self._last_image = None + self._process_time = 0 @property def camera_entity(self): @@ -228,6 +231,7 @@ class Doods(ImageProcessingEntity): label: len(values) for label, values in self._matches.items() }, ATTR_TOTAL_MATCHES: self._total_matches, + ATTR_PROCESS_TIME: self._process_time, } def _save_image(self, image, matches, paths): @@ -270,6 +274,8 @@ class Doods(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): @@ -308,6 +314,7 @@ class Doods(ImageProcessingEntity): _LOGGER.error(response["error"]) self._matches = matches self._total_matches = total_matches + self._process_time = time.monotonic() - start return for detection in response["detections"]: @@ -380,3 +387,4 @@ class Doods(ImageProcessingEntity): self._matches = matches self._total_matches = total_matches + self._process_time = time.monotonic() - start From c6a48d3b611fb0797b25cbf2152a2c93243def58 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Wed, 23 Sep 2020 00:09:19 -0300 Subject: [PATCH 306/514] Support learning different command types with remote (#39670) --- homeassistant/components/remote/__init__.py | 2 ++ homeassistant/components/remote/services.yaml | 3 +++ tests/components/remote/common.py | 5 +++++ tests/components/remote/test_init.py | 1 + 4 files changed, 11 insertions(+) diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index ca9de0bfb62..2b481925b0f 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -30,6 +30,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_ACTIVITY = "activity" ATTR_COMMAND = "command" +ATTR_COMMAND_TYPE = "command_type" ATTR_DEVICE = "device" ATTR_NUM_REPEATS = "num_repeats" ATTR_DELAY_SECS = "delay_secs" @@ -103,6 +104,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: { vol.Optional(ATTR_DEVICE): cv.string, vol.Optional(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_COMMAND_TYPE): cv.string, vol.Optional(ATTR_ALTERNATIVE): cv.boolean, vol.Optional(ATTR_TIMEOUT): cv.positive_int, }, diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index fc9d170726e..3244f018fbd 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -58,6 +58,9 @@ learn_command: command: description: A single command or a list of commands to learn. example: "Turn on" + command_type: + description: The type of command to be learned. + example: "rf" alternative: description: If code must be stored as alternative (useful for discrete remotes). example: "True" diff --git a/tests/components/remote/common.py b/tests/components/remote/common.py index 1f4a5268440..6560b7bf7f7 100644 --- a/tests/components/remote/common.py +++ b/tests/components/remote/common.py @@ -7,6 +7,7 @@ from homeassistant.components.remote import ( ATTR_ACTIVITY, ATTR_ALTERNATIVE, ATTR_COMMAND, + ATTR_COMMAND_TYPE, ATTR_DELAY_SECS, ATTR_DEVICE, ATTR_NUM_REPEATS, @@ -81,6 +82,7 @@ def learn_command( device=None, command=None, alternative=None, + command_type=None, timeout=None, ): """Learn a command from a device.""" @@ -94,6 +96,9 @@ def learn_command( if command: data[ATTR_COMMAND] = command + if command_type: + data[ATTR_COMMAND_TYPE] = command_type + if alternative: data[ATTR_ALTERNATIVE] = alternative diff --git a/tests/components/remote/test_init.py b/tests/components/remote/test_init.py index eb47d365f83..467187985db 100644 --- a/tests/components/remote/test_init.py +++ b/tests/components/remote/test_init.py @@ -101,6 +101,7 @@ class TestRemote(unittest.TestCase): entity_id="entity_id_val", device="test_device", command=["test_command"], + command_type="rf", alternative=True, timeout=20, ) From df9634a41fc23fc4c3eb895923c993fd725c89ee Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 22 Sep 2020 22:14:41 -0500 Subject: [PATCH 307/514] Refactor Plex tests using fixtures (#40260) * Refactor Plex tests using fixtures * Avoid unnecessary coroutine declaration --- tests/components/plex/conftest.py | 59 ++++++++ tests/components/plex/test_browse_media.py | 29 +--- tests/components/plex/test_config_flow.py | 93 ++----------- tests/components/plex/test_init.py | 145 +++----------------- tests/components/plex/test_media_players.py | 38 +---- tests/components/plex/test_playback.py | 23 +--- tests/components/plex/test_server.py | 125 +++-------------- tests/components/plex/test_services.py | 48 +------ 8 files changed, 121 insertions(+), 439 deletions(-) create mode 100644 tests/components/plex/conftest.py diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py new file mode 100644 index 00000000000..4e59b551574 --- /dev/null +++ b/tests/components/plex/conftest.py @@ -0,0 +1,59 @@ +"""Fixtures for Plex tests.""" +import pytest + +from homeassistant.components.plex.const import DOMAIN + +from .const import DEFAULT_DATA, DEFAULT_OPTIONS +from .mock_classes import MockPlexAccount, MockPlexServer + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +@pytest.fixture(name="entry") +def mock_config_entry(): + """Return the default mocked config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) + + +@pytest.fixture +def mock_plex_account(): + """Mock the PlexAccount class and return the used instance.""" + plex_account = MockPlexAccount() + with patch("plexapi.myplex.MyPlexAccount", return_value=plex_account): + yield plex_account + + +@pytest.fixture +def mock_websocket(): + """Mock the PlexWebsocket class.""" + with patch("homeassistant.components.plex.PlexWebsocket", autospec=True) as ws: + yield ws + + +@pytest.fixture +def setup_plex_server(hass, entry, mock_plex_account, mock_websocket): + """Set up and return a mocked Plex server instance.""" + + async def _wrapper(**kwargs): + """Wrap the fixture to allow passing arguments to the MockPlexServer instance.""" + config_entry = kwargs.get("config_entry", entry) + plex_server = MockPlexServer(**kwargs) + with patch("plexapi.server.PlexServer", return_value=plex_server): + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + return plex_server + + return _wrapper + + +@pytest.fixture +async def mock_plex_server(entry, setup_plex_server): + """Init from a config entry and return a mocked PlexServer instance.""" + return await setup_plex_server(config_entry=entry) diff --git a/tests/components/plex/test_browse_media.py b/tests/components/plex/test_browse_media.py index 9cf6d7a7332..d96fdd4a00b 100644 --- a/tests/components/plex/test_browse_media.py +++ b/tests/components/plex/test_browse_media.py @@ -3,39 +3,16 @@ 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.plex.const import CONF_SERVER_IDENTIFIER 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 +from .const import DEFAULT_DATA 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): +async def test_browse_media(hass, hass_ws_client, mock_plex_server, mock_websocket): """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) diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 1bd2ce82863..6b64b2f8571 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -34,7 +34,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) -from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN +from .const import DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN from .helpers import trigger_plex_update from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer, MockResource @@ -77,8 +77,6 @@ async def test_bad_credentials(hass): async def test_bad_hostname(hass): """Test when an invalid address is provided.""" - mock_plex_account = MockPlexAccount() - await async_process_ha_core_config( hass, {"internal_url": "http://example.local:8123"}, @@ -91,7 +89,7 @@ async def test_bad_hostname(hass): assert result["step_id"] == "user" with patch( - "plexapi.myplex.MyPlexAccount", return_value=mock_plex_account + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() ), patch.object( MockResource, "connect", side_effect=requests.exceptions.ConnectionError ), patch( @@ -363,24 +361,8 @@ async def test_all_available_servers_configured(hass): assert result["reason"] == "all_configured" -async def test_option_flow(hass): +async def test_option_flow(hass, entry, mock_plex_server): """Test config options flow selection.""" - mock_plex_server = MockPlexServer() - - entry = MockConfigEntry( - domain=DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "plexapi.myplex.MyPlexAccount", 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 len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state == ENTRY_STATE_LOADED @@ -411,24 +393,8 @@ async def test_option_flow(hass): } -async def test_missing_option_flow(hass): +async def test_missing_option_flow(hass, entry, mock_plex_server): """Test config options flow selection when no options stored.""" - mock_plex_server = MockPlexServer() - - entry = MockConfigEntry( - domain=DOMAIN, - data=DEFAULT_DATA, - options=None, - unique_id=DEFAULT_DATA["server_id"], - ) - - 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 len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state == ENTRY_STATE_LOADED @@ -459,29 +425,15 @@ async def test_missing_option_flow(hass): } -async def test_option_flow_new_users_available(hass, caplog): +async def test_option_flow_new_users_available( + hass, caplog, entry, mock_websocket, setup_plex_server +): """Test config options multiselect defaults when new Plex users are seen.""" - OPTIONS_OWNER_ONLY = copy.deepcopy(DEFAULT_OPTIONS) OPTIONS_OWNER_ONLY[MP_DOMAIN][CONF_MONITORED_USERS] = {"Owner": {"enabled": True}} + entry.options = OPTIONS_OWNER_ONLY - entry = MockConfigEntry( - domain=DOMAIN, - data=DEFAULT_DATA, - options=OPTIONS_OWNER_ONLY, - 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 - ) as mock_websocket: - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + mock_plex_server = await setup_plex_server(config_entry=entry) trigger_plex_update(mock_websocket) await hass.async_block_till_done() @@ -734,29 +686,12 @@ async def test_manual_config_with_token(hass): assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN -async def test_setup_with_limited_credentials(hass): +async def test_setup_with_limited_credentials(hass, entry, setup_plex_server): """Test setup with a user with limited permissions.""" - mock_plex_server = MockPlexServer() - - entry = MockConfigEntry( - domain=DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) - - with patch( - "plexapi.server.PlexServer", return_value=mock_plex_server - ), patch.object( - mock_plex_server, "systemAccounts", side_effect=plexapi.exceptions.Unauthorized - ) as mock_accounts, patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() - ), patch( - "homeassistant.components.plex.PlexWebsocket", autospec=True - ): - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + with patch.object( + MockPlexServer, "systemAccounts", side_effect=plexapi.exceptions.Unauthorized + ) as mock_accounts: + mock_plex_server = await setup_plex_server() assert mock_accounts.called diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 666a819e8ca..3c4f9031fad 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -24,27 +24,8 @@ from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed -async def test_set_config_entry_unique_id(hass): +async def test_set_config_entry_unique_id(hass, entry, mock_plex_server): """Test updating missing unique_id from config entry.""" - - mock_plex_server = MockPlexServer() - - entry = MockConfigEntry( - domain=const.DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=None, - ) - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() - ), patch("homeassistant.components.plex.PlexWebsocket.listen") as mock_listen: - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert mock_listen.called - assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 assert entry.state == ENTRY_STATE_LOADED @@ -54,16 +35,8 @@ async def test_set_config_entry_unique_id(hass): ) -async def test_setup_config_entry_with_error(hass): +async def test_setup_config_entry_with_error(hass, entry): """Test setup component from config entry with errors.""" - - entry = MockConfigEntry( - domain=const.DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) - with patch( "homeassistant.components.plex.PlexServer.connect", side_effect=requests.exceptions.ConnectionError, @@ -87,91 +60,38 @@ async def test_setup_config_entry_with_error(hass): assert entry.state == ENTRY_STATE_SETUP_ERROR -async def test_setup_with_insecure_config_entry(hass): +async def test_setup_with_insecure_config_entry(hass, entry, setup_plex_server): """Test setup component with config.""" - - mock_plex_server = MockPlexServer() - INSECURE_DATA = copy.deepcopy(DEFAULT_DATA) INSECURE_DATA[const.PLEX_SERVER_CONFIG][CONF_VERIFY_SSL] = False + entry.data = INSECURE_DATA - entry = MockConfigEntry( - domain=const.DOMAIN, - data=INSECURE_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() - ), patch("homeassistant.components.plex.PlexWebsocket.listen") as mock_listen: - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert mock_listen.called + await setup_plex_server(config_entry=entry) assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 assert entry.state == ENTRY_STATE_LOADED -async def test_unload_config_entry(hass): +async def test_unload_config_entry(hass, entry, mock_plex_server): """Test unloading a config entry.""" - mock_plex_server = MockPlexServer() - - entry = MockConfigEntry( - domain=const.DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) - entry.add_to_hass(hass) - config_entries = hass.config_entries.async_entries(const.DOMAIN) assert len(config_entries) == 1 assert entry is config_entries[0] - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() - ), patch("homeassistant.components.plex.PlexWebsocket.listen") as mock_listen: - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - assert mock_listen.called - assert entry.state == ENTRY_STATE_LOADED server_id = mock_plex_server.machineIdentifier loaded_server = hass.data[const.DOMAIN][const.SERVERS][server_id] - assert loaded_server.plex_server == mock_plex_server - with patch("homeassistant.components.plex.PlexWebsocket.close") as mock_close: - await hass.config_entries.async_unload(entry.entry_id) - assert mock_close.called - + websocket = hass.data[const.DOMAIN][const.WEBSOCKETS][server_id] + await hass.config_entries.async_unload(entry.entry_id) + assert websocket.close.called assert entry.state == ENTRY_STATE_NOT_LOADED -async def test_setup_with_photo_session(hass): +async def test_setup_with_photo_session(hass, entry, mock_websocket, setup_plex_server): """Test setup component with config.""" - - mock_plex_server = MockPlexServer(session_type="photo") - - entry = MockConfigEntry( - domain=const.DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() - ), 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() + mock_plex_server = await setup_plex_server(config_entry=entry, session_type="photo") assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 assert entry.state == ENTRY_STATE_LOADED @@ -186,7 +106,7 @@ async def test_setup_with_photo_session(hass): assert sensor.state == str(len(mock_plex_server.accounts)) -async def test_setup_when_certificate_changed(hass): +async def test_setup_when_certificate_changed(hass, entry): """Test setup component when the Plex certificate has changed.""" old_domain = "1-2-3-4.1234567890abcdef1234567890abcdef.plex.direct" @@ -210,8 +130,6 @@ async def test_setup_when_certificate_changed(hass): unique_id=DEFAULT_DATA["server_id"], ) - new_entry = MockConfigEntry(domain=const.DOMAIN, data=DEFAULT_DATA) - # Test with account failure with patch( "plexapi.server.PlexServer", side_effect=WrongCertHostnameException @@ -247,49 +165,23 @@ async def test_setup_when_certificate_changed(hass): assert ( old_entry.data[const.PLEX_SERVER_CONFIG][CONF_URL] - == new_entry.data[const.PLEX_SERVER_CONFIG][CONF_URL] + == entry.data[const.PLEX_SERVER_CONFIG][CONF_URL] ) -async def test_tokenless_server(hass): +async def test_tokenless_server(hass, entry, mock_websocket, setup_plex_server): """Test setup with a server with token auth disabled.""" - mock_plex_server = MockPlexServer() - TOKENLESS_DATA = copy.deepcopy(DEFAULT_DATA) TOKENLESS_DATA[const.PLEX_SERVER_CONFIG].pop(CONF_TOKEN, None) + entry.data = TOKENLESS_DATA - entry = MockConfigEntry( - domain=const.DOMAIN, - data=TOKENLESS_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "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() - + await setup_plex_server(config_entry=entry) assert entry.state == ENTRY_STATE_LOADED - trigger_plex_update(mock_websocket) - await hass.async_block_till_done() - -async def test_bad_token_with_tokenless_server(hass): +async def test_bad_token_with_tokenless_server(hass, entry): """Test setup with a bad token and a server with token auth disabled.""" - mock_plex_server = MockPlexServer() - - entry = MockConfigEntry( - domain=const.DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + with patch("plexapi.server.PlexServer", return_value=MockPlexServer()), patch( "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized ), patch( "homeassistant.components.plex.PlexWebsocket", autospec=True @@ -300,5 +192,6 @@ async def test_bad_token_with_tokenless_server(hass): assert entry.state == ENTRY_STATE_LOADED + # Ensure updates that rely on account return nothing trigger_plex_update(mock_websocket) await hass.async_block_till_done() diff --git a/tests/components/plex/test_media_players.py b/tests/components/plex/test_media_players.py index d3e2de91cf9..fdacd6051ae 100644 --- a/tests/components/plex/test_media_players.py +++ b/tests/components/plex/test_media_players.py @@ -3,32 +3,12 @@ from plexapi.exceptions import NotFound from homeassistant.components.plex.const import DOMAIN, SERVERS -from .const import DEFAULT_DATA, DEFAULT_OPTIONS -from .mock_classes import MockPlexAccount, MockPlexServer - from tests.async_mock import patch -from tests.common import MockConfigEntry -async def test_plex_tv_clients(hass): +async def test_plex_tv_clients(hass, entry, mock_plex_account, setup_plex_server): """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.listen"): - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - + mock_plex_server = await setup_plex_server() server_id = mock_plex_server.machineIdentifier plex_server = hass.data[DOMAIN][SERVERS][server_id] @@ -46,12 +26,7 @@ async def test_plex_tv_clients(hass): # Ensure one more client is discovered await hass.config_entries.async_unload(entry.entry_id) - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "plexapi.myplex.MyPlexAccount", return_value=mock_plex_account - ), patch("homeassistant.components.plex.PlexWebsocket.listen"): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - + mock_plex_server = await setup_plex_server(config_entry=entry) plex_server = hass.data[DOMAIN][SERVERS][server_id] await plex_server._async_update_platforms() @@ -63,15 +38,10 @@ async def test_plex_tv_clients(hass): # Ensure only plex.tv resource client is found await hass.config_entries.async_unload(entry.entry_id) + mock_plex_server = await setup_plex_server(config_entry=entry) mock_plex_server.clear_clients() mock_plex_server.clear_sessions() - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "plexapi.myplex.MyPlexAccount", return_value=mock_plex_account - ), patch("homeassistant.components.plex.PlexWebsocket.listen"): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - plex_server = hass.data[DOMAIN][SERVERS][server_id] await plex_server._async_update_platforms() diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py index b031aff25cd..bd694419421 100644 --- a/tests/components/plex/test_playback.py +++ b/tests/components/plex/test_playback.py @@ -10,32 +10,11 @@ from homeassistant.components.plex.const import DOMAIN, SERVERS, SERVICE_PLAY_ON from homeassistant.const import ATTR_ENTITY_ID from homeassistant.exceptions import HomeAssistantError -from .const import DEFAULT_DATA, DEFAULT_OPTIONS -from .mock_classes import MockPlexAccount, MockPlexServer - from tests.async_mock import patch -from tests.common import MockConfigEntry -async def test_sonos_playback(hass): +async def test_sonos_playback(hass, mock_plex_server): """Test playing media on a Sonos speaker.""" - - 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.listen"): - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - server_id = mock_plex_server.machineIdentifier loaded_server = hass.data[DOMAIN][SERVERS][server_id] diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index b3623681f8a..b650821b3f2 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -40,33 +40,16 @@ from .mock_classes import ( ) from tests.async_mock import patch -from tests.common import MockConfigEntry -async def test_new_users_available(hass): +async def test_new_users_available(hass, entry, mock_websocket, setup_plex_server): """Test setting up when new users available on Plex server.""" - MONITORED_USERS = {"Owner": {"enabled": True}} OPTIONS_WITH_USERS = copy.deepcopy(DEFAULT_OPTIONS) OPTIONS_WITH_USERS[MP_DOMAIN][CONF_MONITORED_USERS] = MONITORED_USERS + entry.options = OPTIONS_WITH_USERS - entry = MockConfigEntry( - domain=DOMAIN, - data=DEFAULT_DATA, - options=OPTIONS_WITH_USERS, - 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 - ) as mock_websocket: - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + mock_plex_server = await setup_plex_server(config_entry=entry) server_id = mock_plex_server.machineIdentifier @@ -83,31 +66,17 @@ async def test_new_users_available(hass): assert sensor.state == str(len(mock_plex_server.accounts)) -async def test_new_ignored_users_available(hass, caplog): +async def test_new_ignored_users_available( + hass, caplog, entry, mock_websocket, setup_plex_server +): """Test setting up when new users available on Plex server but are ignored.""" - MONITORED_USERS = {"Owner": {"enabled": True}} OPTIONS_WITH_USERS = copy.deepcopy(DEFAULT_OPTIONS) OPTIONS_WITH_USERS[MP_DOMAIN][CONF_MONITORED_USERS] = MONITORED_USERS OPTIONS_WITH_USERS[MP_DOMAIN][CONF_IGNORE_NEW_SHARED_USERS] = True + entry.options = OPTIONS_WITH_USERS - entry = MockConfigEntry( - domain=DOMAIN, - data=DEFAULT_DATA, - options=OPTIONS_WITH_USERS, - 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 - ) as mock_websocket: - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + mock_plex_server = await setup_plex_server(config_entry=entry) server_id = mock_plex_server.machineIdentifier @@ -134,26 +103,10 @@ async def test_new_ignored_users_available(hass, caplog): assert sensor.state == str(len(mock_plex_server.accounts)) -async def test_network_error_during_refresh(hass, caplog): +async def test_network_error_during_refresh( + hass, caplog, mock_plex_server, mock_websocket +): """Test network failures during refreshes.""" - entry = MockConfigEntry( - domain=DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) - - mock_plex_server = MockPlexServer() - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() - ), patch( - "homeassistant.components.plex.PlexWebsocket", autospec=True - ) as mock_websocket: - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - server_id = mock_plex_server.machineIdentifier loaded_server = hass.data[DOMAIN][SERVERS][server_id] @@ -172,26 +125,8 @@ async def test_network_error_during_refresh(hass, caplog): ) -async def test_mark_sessions_idle(hass): +async def test_mark_sessions_idle(hass, mock_plex_server, mock_websocket): """Test marking media_players as idle when sessions end.""" - entry = MockConfigEntry( - domain=DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) - - mock_plex_server = MockPlexServer() - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() - ), patch( - "homeassistant.components.plex.PlexWebsocket", autospec=True - ) as mock_websocket: - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - server_id = mock_plex_server.machineIdentifier loaded_server = hass.data[DOMAIN][SERVERS][server_id] @@ -211,26 +146,17 @@ async def test_mark_sessions_idle(hass): assert sensor.state == "0" -async def test_ignore_plex_web_client(hass): +async def test_ignore_plex_web_client(hass, entry, mock_websocket): """Test option to ignore Plex Web clients.""" - OPTIONS = copy.deepcopy(DEFAULT_OPTIONS) OPTIONS[MP_DOMAIN][CONF_IGNORE_PLEX_WEB_CLIENTS] = True - - entry = MockConfigEntry( - domain=DOMAIN, - data=DEFAULT_DATA, - options=OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) + entry.options = OPTIONS mock_plex_server = MockPlexServer(config_entry=entry) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(players=0) - ), 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() @@ -246,27 +172,8 @@ async def test_ignore_plex_web_client(hass): assert len(media_players) == int(sensor.state) - 1 -async def test_media_lookups(hass): +async def test_media_lookups(hass, mock_plex_server, mock_websocket): """Test media lookups to Plex server.""" - - 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 - ) as mock_websocket: - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - server_id = mock_plex_server.machineIdentifier loaded_server = hass.data[DOMAIN][SERVERS][server_id] diff --git a/tests/components/plex/test_services.py b/tests/components/plex/test_services.py index 078ba3b97e9..a3f4d4c833a 100644 --- a/tests/components/plex/test_services.py +++ b/tests/components/plex/test_services.py @@ -15,31 +15,15 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) -from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN -from .mock_classes import MockPlexAccount, MockPlexLibrarySection, MockPlexServer +from .const import MOCK_SERVERS, MOCK_TOKEN +from .mock_classes import MockPlexLibrarySection from tests.async_mock import patch from tests.common import MockConfigEntry -async def test_refresh_library(hass): +async def test_refresh_library(hass, mock_plex_server, setup_plex_server): """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( @@ -84,13 +68,7 @@ async def test_refresh_library(hass): }, ) - 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() + await setup_plex_server(config_entry=entry_2) # Test multiple servers available but none specified with patch.object(MockPlexLibrarySection, "update") as mock_update: @@ -103,24 +81,8 @@ async def test_refresh_library(hass): assert not mock_update.called -async def test_scan_clients(hass): +async def test_scan_clients(hass, mock_plex_server): """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, From 7876cdb37a7de94e7ff0ffe1d11362d745ceacb9 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 23 Sep 2020 09:21:57 +0200 Subject: [PATCH 308/514] Fix handling of quoted time_pattern values (#40470) Co-authored-by: J. Nick Koston Co-authored-by: Franck Nijhof --- .../homeassistant/triggers/time_pattern.py | 2 +- .../triggers/test_time_pattern.py | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant/triggers/time_pattern.py b/homeassistant/components/homeassistant/triggers/time_pattern.py index adacc939870..5f03fb593d6 100644 --- a/homeassistant/components/homeassistant/triggers/time_pattern.py +++ b/homeassistant/components/homeassistant/triggers/time_pattern.py @@ -36,7 +36,7 @@ class TimePattern: if isinstance(value, str) and value.startswith("/"): number = int(value[1:]) else: - number = int(value) + value = number = int(value) if not (0 <= number <= self.maximum): raise vol.Invalid(f"must be a value between 0 and {self.maximum}") diff --git a/tests/components/homeassistant/triggers/test_time_pattern.py b/tests/components/homeassistant/triggers/test_time_pattern.py index 0ef071aadb6..3d32748c176 100644 --- a/tests/components/homeassistant/triggers/test_time_pattern.py +++ b/tests/components/homeassistant/triggers/test_time_pattern.py @@ -1,4 +1,6 @@ """The tests for the time_pattern automation.""" +from datetime import timedelta + import pytest import voluptuous as vol @@ -123,6 +125,39 @@ async def test_if_fires_when_second_matches(hass, calls): assert len(calls) == 1 +async def test_if_fires_when_second_as_string_matches(hass, calls): + """Test for firing if seconds are matching.""" + now = dt_util.utcnow() + time_that_will_not_match_right_away = dt_util.utcnow().replace( + year=now.year + 1, second=15 + ) + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "time_pattern", + "hours": "*", + "minutes": "*", + "seconds": "30", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + async_fire_time_changed( + hass, time_that_will_not_match_right_away + timedelta(seconds=15) + ) + + await hass.async_block_till_done() + assert len(calls) == 1 + + async def test_if_fires_when_all_matches(hass, calls): """Test for firing if everything matches.""" now = dt_util.utcnow() From c2d20c548f7f901a38b6b763271a2e8ae557e72d Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 23 Sep 2020 11:29:56 +0200 Subject: [PATCH 309/514] Use content type multipart constant (#40314) --- homeassistant/components/camera/__init__.py | 3 ++- homeassistant/components/ffmpeg/__init__.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index f6b909231ca..49b60183920 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -35,6 +35,7 @@ from homeassistant.components.stream.const import ( from homeassistant.const import ( ATTR_ENTITY_ID, CONF_FILENAME, + CONTENT_TYPE_MULTIPART, EVENT_HOMEASSISTANT_START, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -184,7 +185,7 @@ async def async_get_still_stream(request, image_cb, content_type, interval): This method must be run in the event loop. """ response = web.StreamResponse() - response.content_type = "multipart/x-mixed-replace; boundary=--frameboundary" + response.content_type = CONTENT_TYPE_MULTIPART.format("--frameboundary") await response.prepare(request) async def write_to_mjpeg_stream(img_bytes): diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index f109103a99c..ca752208f12 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, + CONTENT_TYPE_MULTIPART, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) @@ -122,9 +123,9 @@ class FFmpegManager: def ffmpeg_stream_content_type(self): """Return HTTP content type for ffmpeg stream.""" if self._major_version is not None and self._major_version > 3: - return "multipart/x-mixed-replace;boundary=ffmpeg" + return CONTENT_TYPE_MULTIPART.format("ffmpeg") - return "multipart/x-mixed-replace;boundary=ffserver" + return CONTENT_TYPE_MULTIPART.format("ffserver") class FFmpegBase(Entity): From 7c61caf68e6898d0287f1d5af66cbe664f37d43f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Sep 2020 12:39:27 +0200 Subject: [PATCH 310/514] Revert "Support learning different command types with remote" (#40486) This reverts commit c6a48d3b611fb0797b25cbf2152a2c93243def58. --- homeassistant/components/remote/__init__.py | 2 -- homeassistant/components/remote/services.yaml | 3 --- tests/components/remote/common.py | 5 ----- tests/components/remote/test_init.py | 1 - 4 files changed, 11 deletions(-) diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 2b481925b0f..ca9de0bfb62 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -30,7 +30,6 @@ _LOGGER = logging.getLogger(__name__) ATTR_ACTIVITY = "activity" ATTR_COMMAND = "command" -ATTR_COMMAND_TYPE = "command_type" ATTR_DEVICE = "device" ATTR_NUM_REPEATS = "num_repeats" ATTR_DELAY_SECS = "delay_secs" @@ -104,7 +103,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: { vol.Optional(ATTR_DEVICE): cv.string, vol.Optional(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_COMMAND_TYPE): cv.string, vol.Optional(ATTR_ALTERNATIVE): cv.boolean, vol.Optional(ATTR_TIMEOUT): cv.positive_int, }, diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index 3244f018fbd..fc9d170726e 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -58,9 +58,6 @@ learn_command: command: description: A single command or a list of commands to learn. example: "Turn on" - command_type: - description: The type of command to be learned. - example: "rf" alternative: description: If code must be stored as alternative (useful for discrete remotes). example: "True" diff --git a/tests/components/remote/common.py b/tests/components/remote/common.py index 6560b7bf7f7..1f4a5268440 100644 --- a/tests/components/remote/common.py +++ b/tests/components/remote/common.py @@ -7,7 +7,6 @@ from homeassistant.components.remote import ( ATTR_ACTIVITY, ATTR_ALTERNATIVE, ATTR_COMMAND, - ATTR_COMMAND_TYPE, ATTR_DELAY_SECS, ATTR_DEVICE, ATTR_NUM_REPEATS, @@ -82,7 +81,6 @@ def learn_command( device=None, command=None, alternative=None, - command_type=None, timeout=None, ): """Learn a command from a device.""" @@ -96,9 +94,6 @@ def learn_command( if command: data[ATTR_COMMAND] = command - if command_type: - data[ATTR_COMMAND_TYPE] = command_type - if alternative: data[ATTR_ALTERNATIVE] = alternative diff --git a/tests/components/remote/test_init.py b/tests/components/remote/test_init.py index 467187985db..eb47d365f83 100644 --- a/tests/components/remote/test_init.py +++ b/tests/components/remote/test_init.py @@ -101,7 +101,6 @@ class TestRemote(unittest.TestCase): entity_id="entity_id_val", device="test_device", command=["test_command"], - command_type="rf", alternative=True, timeout=20, ) From c7f48e9ea3b1a6a69485465f113d9213db15b2cd Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 23 Sep 2020 15:50:01 +0200 Subject: [PATCH 311/514] Make modbus switch read_coil failure resistent (#40417) * Make modbus switch read_coil failure resistent. Make sure all return paths return true/false. * Add comment how binary_sensor get its value (is_on). --- homeassistant/components/modbus/switch.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index c238e105659..fa5b42807b0 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -158,20 +158,23 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity): """Update the state of the switch.""" self._is_on = self._read_coil(self._coil) - def _read_coil(self, coil) -> Optional[bool]: + def _read_coil(self, coil) -> bool: """Read coil using the Modbus hub slave.""" try: result = self._hub.read_coils(self._slave, coil, 1) except ConnectionException: self._available = False - return + return False if isinstance(result, (ModbusException, ExceptionResponse)): self._available = False - return + return False self._available = True - return bool(result.bits[coil]) + # bits[0] select the lowest bit in result, + # is_on for a binary_sensor is true if the bit are 1 + # The other bits are not considered. + return bool(result.bits[0]) def _write_coil(self, coil, value): """Write coil using the Modbus hub slave.""" From 784af5ad10bffb0263e8a7c0fb073ff8995a6b74 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Sep 2020 16:44:25 +0200 Subject: [PATCH 312/514] Upgrade sentry-sdk to 0.17.7 (#40492) --- 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 6f511837ca8..6adb2e38f1c 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.6"], + "requirements": ["sentry-sdk==0.17.7"], "codeowners": ["@dcramer", "@frenck"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4b6f5451abd..444e6e1e75b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1970,7 +1970,7 @@ sense-hat==2.2.0 sense_energy==0.8.0 # homeassistant.components.sentry -sentry-sdk==0.17.6 +sentry-sdk==0.17.7 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4dc6bad66cf..5c54a64509c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -918,7 +918,7 @@ samsungtvws==1.4.0 sense_energy==0.8.0 # homeassistant.components.sentry -sentry-sdk==0.17.6 +sentry-sdk==0.17.7 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From 40d003eccfbe991f4fdbaf3bbdf1b22d72343d3d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Sep 2020 16:44:51 +0200 Subject: [PATCH 313/514] Upgrade isort to 5.5.3 (#40493) --- .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 e24b6095b4a..114e6b6c87b 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.2 + rev: 5.5.3 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 06b8430a740..d464c648e75 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.2 +isort==5.5.3 pydocstyle==5.1.1 pyupgrade==2.7.2 yamllint==1.24.2 From ccff7f97cbe832bc5e548596420f040d9ee234ea Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 23 Sep 2020 17:05:28 +0200 Subject: [PATCH 314/514] Add supervisor helper to start add-on (#40495) --- homeassistant/components/hassio/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 36bfcf04bca..8a831fb790e 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -158,6 +158,17 @@ async def async_uninstall_addon(hass: HomeAssistantType, slug: str) -> None: await hassio.send_command(command) +@bind_hass +async def async_start_addon(hass: HomeAssistantType, slug: str) -> None: + """Start add-on. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/start" + await hassio.send_command(command) + + @callback @bind_hass def get_info(hass): From 27c5594cda02869d41c8b15d6707a51f805d4578 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 23 Sep 2020 17:54:07 +0200 Subject: [PATCH 315/514] Add supervisor add-on stop helper (#40501) --- homeassistant/components/hassio/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 8a831fb790e..9604507fcf0 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -169,6 +169,17 @@ async def async_start_addon(hass: HomeAssistantType, slug: str) -> None: await hassio.send_command(command) +@bind_hass +async def async_stop_addon(hass: HomeAssistantType, slug: str) -> None: + """Stop add-on. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/stop" + await hassio.send_command(command) + + @callback @bind_hass def get_info(hass): From 9f5cd5547bb7d0aa4ef6979d99f19ad9422ec918 Mon Sep 17 00:00:00 2001 From: nagyrobi Date: Wed, 23 Sep 2020 17:56:23 +0200 Subject: [PATCH 316/514] Support all available languages in voicerss integration (#40502) --- homeassistant/components/voicerss/tts.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/homeassistant/components/voicerss/tts.py b/homeassistant/components/voicerss/tts.py index 23d4c9ff864..69029ea7031 100644 --- a/homeassistant/components/voicerss/tts.py +++ b/homeassistant/components/voicerss/tts.py @@ -28,32 +28,55 @@ ERROR_MSG = [ ] SUPPORT_LANGUAGES = [ + "ar-eg", + "ar-sa", + "bg-bg", "ca-es", "zh-cn", "zh-hk", "zh-tw", + "hr-hr", + "cs-cz", "da-dk", + "nl-be", "nl-nl", "en-au", "en-ca", "en-gb", "en-in", + "en-ie", "en-us", "fi-fi", "fr-ca", "fr-fr", + "fr-ch", + "de-at", "de-de", + "de-ch", + "el-gr", + "he-il", + "hi-in", + "hu-hu", + "id-id", "it-it", "ja-jp", "ko-kr", + "ms-my", "nb-no", "pl-pl", "pt-br", "pt-pt", + "ro-ro", "ru-ru", + "sk-sk", + "sl-si", "es-mx", "es-es", "sv-se", + "ta-in", + "th-th", + "tr-tr", + "vi-vn", ] SUPPORT_CODECS = ["mp3", "wav", "aac", "ogg", "caf"] From 223000a9fbd2a46539054ad93a9dd29333205415 Mon Sep 17 00:00:00 2001 From: Tomasz Date: Wed, 23 Sep 2020 17:57:06 +0200 Subject: [PATCH 317/514] Add all supported languages to OpenWeatherMap (#40448) --- .../components/openweathermap/const.py | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index d2ea7f34d50..03ed97d4075 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -73,7 +73,57 @@ FORECAST_MONITORED_CONDITIONS = [ ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, ] -LANGUAGES = ["en", "es", "ru", "it"] +LANGUAGES = [ + "af", + "al", + "ar", + "az", + "bg", + "ca", + "cz", + "da", + "de", + "el", + "en", + "es", + "eu", + "fa", + "fi", + "fr", + "gl", + "he", + "hi", + "hr", + "hu", + "id", + "it", + "ja", + "kr", + "la", + "lt", + "mk", + "nl", + "no", + "pl", + "pt", + "pt_br", + "ro", + "ru", + "se", + "sk", + "sl", + "sp", + "sr", + "sv", + "th", + "tr", + "ua", + "uk", + "vi", + "zh_cn", + "zh_tw", + "zu", +] CONDITION_CLASSES = { "cloudy": [803, 804], "fog": [701, 741], From 9c1eb78a0f89c17a1a9fa8e1d3bdb89c0dbf5205 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Wed, 23 Sep 2020 18:57:35 +0200 Subject: [PATCH 318/514] Adjust safe_theme for better readability (#40223) --- homeassistant/components/frontend/__init__.py | 2 +- tests/components/frontend/test_init.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 73eb24d08cd..66c1b6d997f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -559,7 +559,7 @@ def websocket_get_themes(hass, connection, msg): "themes": { "safe_mode": { "primary-color": "#db4437", - "accent-color": "#eeee02", + "accent-color": "#ffca28", } }, "default_theme": "safe_mode", diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 7298ad753d2..7802ee60e8c 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -140,7 +140,7 @@ async def test_themes_api(hass, hass_ws_client): assert msg["result"]["default_theme"] == "safe_mode" assert msg["result"]["themes"] == { - "safe_mode": {"primary-color": "#db4437", "accent-color": "#eeee02"} + "safe_mode": {"primary-color": "#db4437", "accent-color": "#ffca28"} } From 6a7caad8dcd89a8488ec6243559aa98990e87398 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 23 Sep 2020 20:21:55 +0200 Subject: [PATCH 319/514] Use content type json constant (#40312) --- .../components/microsoft_face/__init__.py | 4 +- .../components/mobile_app/helpers.py | 6 +-- tests/components/adguard/test_config_flow.py | 9 +++-- tests/components/agent_dvr/__init__.py | 6 +-- .../components/agent_dvr/test_config_flow.py | 6 +-- tests/components/alexa/test_intent.py | 3 +- .../components/alexa/test_smart_home_http.py | 4 +- tests/components/atag/__init__.py | 8 ++-- tests/components/bsblan/__init__.py | 4 +- tests/components/bsblan/test_config_flow.py | 4 +- tests/components/cloud/test_client.py | 7 ++-- tests/components/deconz/test_config_flow.py | 38 +++++++++---------- tests/components/directv/__init__.py | 29 ++++++++------ tests/components/elgato/__init__.py | 10 ++--- tests/components/elgato/test_config_flow.py | 8 ++-- tests/components/emulated_hue/test_hue_api.py | 31 +++++++-------- tests/components/emulated_hue/test_upnp.py | 6 +-- tests/components/flo/conftest.py | 26 ++++++------- tests/components/flo/test_config_flow.py | 3 +- tests/components/rest/test_binary_sensor.py | 6 +-- tests/components/rest/test_sensor.py | 29 ++++++++------ tests/components/rest/test_switch.py | 4 +- tests/components/rest_command/test_init.py | 18 ++++----- tests/components/sonarr/__init__.py | 15 ++++---- .../twentemilieu/test_config_flow.py | 8 ++-- tests/components/unifi/test_config_flow.py | 13 ++++--- tests/components/wled/__init__.py | 10 ++--- tests/components/wled/test_config_flow.py | 8 ++-- 28 files changed, 170 insertions(+), 153 deletions(-) diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index 208cecf6d3b..69a738724c3 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -8,7 +8,7 @@ from aiohttp.hdrs import CONTENT_TYPE import async_timeout import voluptuous as vol -from homeassistant.const import ATTR_NAME, CONF_API_KEY, CONF_TIMEOUT +from homeassistant.const import ATTR_NAME, CONF_API_KEY, CONF_TIMEOUT, CONTENT_TYPE_JSON from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -290,7 +290,7 @@ class MicrosoftFace: headers[CONTENT_TYPE] = "application/octet-stream" payload = data else: - headers[CONTENT_TYPE] = "application/json" + headers[CONTENT_TYPE] = CONTENT_TYPE_JSON if data is not None: payload = json.dumps(data).encode() else: diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index b2cb3d22e4b..7c5cbd135ed 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -7,7 +7,7 @@ from aiohttp.web import Response, json_response from nacl.encoding import Base64Encoder from nacl.secret import SecretBox -from homeassistant.const import HTTP_BAD_REQUEST, HTTP_OK +from homeassistant.const import CONTENT_TYPE_JSON, HTTP_BAD_REQUEST, HTTP_OK from homeassistant.core import Context from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.typing import HomeAssistantType @@ -94,7 +94,7 @@ def registration_context(registration: Dict) -> Context: def empty_okay_response(headers: Dict = None, status: int = HTTP_OK) -> Response: """Return a Response with empty JSON object and a 200.""" return Response( - text="{}", status=status, content_type="application/json", headers=headers + text="{}", status=status, content_type=CONTENT_TYPE_JSON, headers=headers ) @@ -161,7 +161,7 @@ def webhook_response( data = json.dumps({"encrypted": True, "encrypted_data": enc_data}) return Response( - text=data, status=status, content_type="application/json", headers=headers + text=data, status=status, content_type=CONTENT_TYPE_JSON, headers=headers ) diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index dae9e0da79d..e773768ebe6 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, + CONTENT_TYPE_JSON, ) from tests.async_mock import patch @@ -62,7 +63,7 @@ async def test_full_flow_implementation(hass, aioclient_mock): f"://{FIXTURE_USER_INPUT[CONF_HOST]}" f":{FIXTURE_USER_INPUT[CONF_PORT]}/control/status", json={"version": "v0.99.0"}, - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) flow = config_flow.AdGuardHomeFlowHandler() @@ -134,12 +135,12 @@ async def test_hassio_update_instance_running(hass, aioclient_mock): aioclient_mock.get( "http://mock-adguard-updated:3000/control/status", json={"version": "v0.99.0"}, - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( "http://mock-adguard:3000/control/status", json={"version": "v0.99.0"}, - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) entry = MockConfigEntry( @@ -195,7 +196,7 @@ async def test_hassio_confirm(hass, aioclient_mock): aioclient_mock.get( "http://mock-adguard:3000/control/status", json={"version": "v0.99.0"}, - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/agent_dvr/__init__.py b/tests/components/agent_dvr/__init__.py index 2cda0b8aa73..ec35b521a17 100644 --- a/tests/components/agent_dvr/__init__.py +++ b/tests/components/agent_dvr/__init__.py @@ -1,7 +1,7 @@ """Tests for the agent_dvr component.""" from homeassistant.components.agent_dvr.const import DOMAIN, SERVER_URL -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -18,12 +18,12 @@ async def init_integration( aioclient_mock.get( "http://example.local:8090/command.cgi?cmd=getStatus", text=load_fixture("agent_dvr/status.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( "http://example.local:8090/command.cgi?cmd=getObjects", text=load_fixture("agent_dvr/objects.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/agent_dvr/test_config_flow.py b/tests/components/agent_dvr/test_config_flow.py index 403dd9f4bee..064034a4a69 100644 --- a/tests/components/agent_dvr/test_config_flow.py +++ b/tests/components/agent_dvr/test_config_flow.py @@ -3,7 +3,7 @@ from homeassistant import data_entry_flow from homeassistant.components.agent_dvr import config_flow from homeassistant.components.agent_dvr.const import SERVER_URL from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant from . import init_integration @@ -61,13 +61,13 @@ async def test_full_user_flow_implementation( aioclient_mock.get( "http://example.local:8090/command.cgi?cmd=getStatus", text=load_fixture("agent_dvr/status.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( "http://example.local:8090/command.cgi?cmd=getObjects", text=load_fixture("agent_dvr/objects.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index 8937a7938ac..c838bf5b3a3 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -6,6 +6,7 @@ import pytest from homeassistant.components import alexa from homeassistant.components.alexa import intent +from homeassistant.const import CONTENT_TYPE_JSON from homeassistant.core import callback from homeassistant.setup import async_setup_component @@ -111,7 +112,7 @@ def _intent_req(client, data=None): return client.post( intent.INTENTS_API_ENDPOINT, data=json.dumps(data or {}), - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) diff --git a/tests/components/alexa/test_smart_home_http.py b/tests/components/alexa/test_smart_home_http.py index 46db47dc2c9..c46a61aef41 100644 --- a/tests/components/alexa/test_smart_home_http.py +++ b/tests/components/alexa/test_smart_home_http.py @@ -2,7 +2,7 @@ import json from homeassistant.components.alexa import DOMAIN, smart_home_http -from homeassistant.const import HTTP_NOT_FOUND +from homeassistant.const import CONTENT_TYPE_JSON, HTTP_NOT_FOUND from homeassistant.setup import async_setup_component from . import get_new_request @@ -17,7 +17,7 @@ async def do_http_discovery(config, hass, hass_client): response = await http_client.post( smart_home_http.SMART_HOME_HTTP_ENDPOINT, data=json.dumps(request), - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) return response diff --git a/tests/components/atag/__init__.py b/tests/components/atag/__init__.py index 3f2b6468491..11162f3d8a7 100644 --- a/tests/components/atag/__init__.py +++ b/tests/components/atag/__init__.py @@ -1,7 +1,7 @@ """Tests for the Atag integration.""" from homeassistant.components.atag import DOMAIN -from homeassistant.const import CONF_EMAIL, CONF_HOST, CONF_PORT +from homeassistant.const import CONF_EMAIL, CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -62,17 +62,17 @@ async def init_integration( aioclient_mock.get( "http://127.0.0.1:10000/retrieve", json=RECEIVE_REPLY, - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.post( "http://127.0.0.1:10000/update", json=UPDATE_REPLY, - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.post( "http://127.0.0.1:10000/pair", json=PAIR_REPLY, - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) entry = MockConfigEntry(domain=DOMAIN, data=USER_INPUT) diff --git a/tests/components/bsblan/__init__.py b/tests/components/bsblan/__init__.py index 45b0f16c0a1..511b566ce41 100644 --- a/tests/components/bsblan/__init__.py +++ b/tests/components/bsblan/__init__.py @@ -5,7 +5,7 @@ from homeassistant.components.bsblan.const import ( CONF_PASSKEY, DOMAIN, ) -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -23,7 +23,7 @@ async def init_integration( "http://example.local:80/1234/JQ?Parameter=6224,6225,6226", params={"Parameter": "6224,6225,6226"}, text=load_fixture("bsblan/info.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) entry = MockConfigEntry( diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py index c24fbc34f6a..4c24e10a237 100644 --- a/tests/components/bsblan/test_config_flow.py +++ b/tests/components/bsblan/test_config_flow.py @@ -5,7 +5,7 @@ from homeassistant import data_entry_flow from homeassistant.components.bsblan import config_flow from homeassistant.components.bsblan.const import CONF_DEVICE_IDENT, CONF_PASSKEY from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant from . import init_integration @@ -67,7 +67,7 @@ async def test_full_user_flow_implementation( aioclient_mock.post( "http://example.local:80/1234/JQ?Parameter=6224,6225,6226", text=load_fixture("bsblan/info.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index d0d9c4b25b7..aaabca71885 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -5,6 +5,7 @@ import pytest from homeassistant.components.cloud import DOMAIN from homeassistant.components.cloud.client import CloudClient from homeassistant.components.cloud.const import PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE +from homeassistant.const import CONTENT_TYPE_JSON from homeassistant.core import State from homeassistant.setup import async_setup_component @@ -175,7 +176,7 @@ async def test_webhook_msg(hass, caplog): { "cloudhook_id": "mock-cloud-id", "body": '{"hello": "world"}', - "headers": {"content-type": "application/json"}, + "headers": {"content-type": CONTENT_TYPE_JSON}, "method": "POST", "query": None, } @@ -184,7 +185,7 @@ async def test_webhook_msg(hass, caplog): assert response == { "status": 200, "body": '{"from": "handler"}', - "headers": {"Content-Type": "application/json"}, + "headers": {"Content-Type": CONTENT_TYPE_JSON}, } assert len(received) == 1 @@ -197,7 +198,7 @@ async def test_webhook_msg(hass, caplog): { "cloudhook_id": "mock-nonexisting-id", "body": '{"nonexisting": "payload"}', - "headers": {"content-type": "application/json"}, + "headers": {"content-type": CONTENT_TYPE_JSON}, "method": "POST", "query": None, } diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 43536a44bbe..092d59bec76 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -17,7 +17,7 @@ from homeassistant.components.deconz.const import ( CONF_MASTER_GATEWAY, DOMAIN, ) -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON from .test_gateway import API_KEY, BRIDGEID, setup_deconz_integration @@ -32,7 +32,7 @@ async def test_flow_discovered_bridges(hass, aioclient_mock): {"id": BRIDGEID, "internalipaddress": "1.2.3.4", "internalport": 80}, {"id": "1234E567890A", "internalipaddress": "5.6.7.8", "internalport": 80}, ], - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_init( @@ -52,7 +52,7 @@ async def test_flow_discovered_bridges(hass, aioclient_mock): aioclient_mock.post( "http://1.2.3.4:80/api", json=[{"success": {"username": API_KEY}}], - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_configure( @@ -73,7 +73,7 @@ async def test_flow_manual_configuration_decision(hass, aioclient_mock): aioclient_mock.get( pydeconz.utils.URL_DISCOVER, json=[{"id": BRIDGEID, "internalipaddress": "1.2.3.4", "internalport": 80}], - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_init( @@ -98,13 +98,13 @@ async def test_flow_manual_configuration_decision(hass, aioclient_mock): aioclient_mock.post( "http://1.2.3.4:80/api", json=[{"success": {"username": API_KEY}}], - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( f"http://1.2.3.4:80/api/{API_KEY}/config", json={"bridgeid": BRIDGEID}, - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_configure( @@ -125,7 +125,7 @@ async def test_flow_manual_configuration(hass, aioclient_mock): aioclient_mock.get( pydeconz.utils.URL_DISCOVER, json=[], - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_init( @@ -146,13 +146,13 @@ async def test_flow_manual_configuration(hass, aioclient_mock): aioclient_mock.post( "http://1.2.3.4:80/api", json=[{"success": {"username": API_KEY}}], - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( f"http://1.2.3.4:80/api/{API_KEY}/config", json={"bridgeid": BRIDGEID}, - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_configure( @@ -201,7 +201,7 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock): aioclient_mock.get( pydeconz.utils.URL_DISCOVER, json=[], - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_init( @@ -222,13 +222,13 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock): aioclient_mock.post( "http://2.3.4.5:80/api", json=[{"success": {"username": API_KEY}}], - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( f"http://2.3.4.5:80/api/{API_KEY}/config", json={"bridgeid": BRIDGEID}, - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_configure( @@ -247,7 +247,7 @@ async def test_manual_configuration_dont_update_configuration(hass, aioclient_mo aioclient_mock.get( pydeconz.utils.URL_DISCOVER, json=[], - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_init( @@ -268,13 +268,13 @@ async def test_manual_configuration_dont_update_configuration(hass, aioclient_mo aioclient_mock.post( "http://1.2.3.4:80/api", json=[{"success": {"username": API_KEY}}], - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( f"http://1.2.3.4:80/api/{API_KEY}/config", json={"bridgeid": BRIDGEID}, - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_configure( @@ -290,7 +290,7 @@ async def test_manual_configuration_timeout_get_bridge(hass, aioclient_mock): aioclient_mock.get( pydeconz.utils.URL_DISCOVER, json=[], - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_init( @@ -311,7 +311,7 @@ async def test_manual_configuration_timeout_get_bridge(hass, aioclient_mock): aioclient_mock.post( "http://1.2.3.4:80/api", json=[{"success": {"username": API_KEY}}], - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( @@ -331,7 +331,7 @@ async def test_link_get_api_key_ResponseError(hass, aioclient_mock): aioclient_mock.get( pydeconz.utils.URL_DISCOVER, json=[{"id": BRIDGEID, "internalipaddress": "1.2.3.4", "internalport": 80}], - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_init( @@ -374,7 +374,7 @@ async def test_flow_ssdp_discovery(hass, aioclient_mock): aioclient_mock.post( "http://1.2.3.4:80/api", json=[{"success": {"username": API_KEY}}], - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/directv/__init__.py b/tests/components/directv/__init__.py index 6b71b1e8cfe..9f09c377bd4 100644 --- a/tests/components/directv/__init__.py +++ b/tests/components/directv/__init__.py @@ -1,7 +1,12 @@ """Tests for the DirecTV component.""" from homeassistant.components.directv.const import CONF_RECEIVER_ID, DOMAIN from homeassistant.components.ssdp import ATTR_SSDP_LOCATION -from homeassistant.const import CONF_HOST, HTTP_FORBIDDEN, HTTP_INTERNAL_SERVER_ERROR +from homeassistant.const import ( + CONF_HOST, + CONTENT_TYPE_JSON, + HTTP_FORBIDDEN, + HTTP_INTERNAL_SERVER_ERROR, +) from homeassistant.helpers.typing import HomeAssistantType from tests.common import MockConfigEntry, load_fixture @@ -22,20 +27,20 @@ def mock_connection(aioclient_mock: AiohttpClientMocker) -> None: aioclient_mock.get( f"http://{HOST}:8080/info/getVersion", text=load_fixture("directv/info-get-version.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( f"http://{HOST}:8080/info/getLocations", text=load_fixture("directv/info-get-locations.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( f"http://{HOST}:8080/info/mode", params={"clientAddr": "B01234567890"}, text=load_fixture("directv/info-mode-standby.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( @@ -43,39 +48,39 @@ def mock_connection(aioclient_mock: AiohttpClientMocker) -> None: params={"clientAddr": "9XXXXXXXXXX9"}, status=HTTP_INTERNAL_SERVER_ERROR, text=load_fixture("directv/info-mode-error.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( f"http://{HOST}:8080/info/mode", text=load_fixture("directv/info-mode.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( f"http://{HOST}:8080/remote/processKey", text=load_fixture("directv/remote-process-key.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( f"http://{HOST}:8080/tv/tune", text=load_fixture("directv/tv-tune.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( f"http://{HOST}:8080/tv/getTuned", params={"clientAddr": "2CA17D1CD30X"}, text=load_fixture("directv/tv-get-tuned.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( f"http://{HOST}:8080/tv/getTuned", params={"clientAddr": "A01234567890"}, text=load_fixture("directv/tv-get-tuned-music.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( @@ -83,13 +88,13 @@ def mock_connection(aioclient_mock: AiohttpClientMocker) -> None: params={"clientAddr": "C01234567890"}, status=HTTP_FORBIDDEN, text=load_fixture("directv/tv-get-tuned-restricted.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( f"http://{HOST}:8080/tv/getTuned", text=load_fixture("directv/tv-get-tuned-movie.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) diff --git a/tests/components/elgato/__init__.py b/tests/components/elgato/__init__.py index 5f0f2f5fb14..3b1942aee14 100644 --- a/tests/components/elgato/__init__.py +++ b/tests/components/elgato/__init__.py @@ -1,7 +1,7 @@ """Tests for the Elgato Key Light integration.""" from homeassistant.components.elgato.const import CONF_SERIAL_NUMBER, DOMAIN -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -18,25 +18,25 @@ async def init_integration( aioclient_mock.get( "http://1.2.3.4:9123/elgato/accessory-info", text=load_fixture("elgato/info.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.put( "http://1.2.3.4:9123/elgato/lights", text=load_fixture("elgato/state.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( "http://1.2.3.4:9123/elgato/lights", text=load_fixture("elgato/state.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( "http://5.6.7.8:9123/elgato/accessory-info", text=load_fixture("elgato/info.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) entry = MockConfigEntry( diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py index bd65811133a..e0d34aecad2 100644 --- a/tests/components/elgato/test_config_flow.py +++ b/tests/components/elgato/test_config_flow.py @@ -5,7 +5,7 @@ from homeassistant import data_entry_flow from homeassistant.components.elgato import config_flow from homeassistant.components.elgato.const import CONF_SERIAL_NUMBER from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant from . import init_integration @@ -44,7 +44,7 @@ async def test_show_zerconf_form( aioclient_mock.get( "http://1.2.3.4:9123/elgato/accessory-info", text=load_fixture("elgato/info.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) flow = config_flow.ElgatoFlowHandler() @@ -176,7 +176,7 @@ async def test_full_user_flow_implementation( aioclient_mock.get( "http://1.2.3.4:9123/elgato/accessory-info", text=load_fixture("elgato/info.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_init( @@ -208,7 +208,7 @@ async def test_full_zeroconf_flow_implementation( aioclient_mock.get( "http://1.2.3.4:9123/elgato/accessory-info", text=load_fixture("elgato/info.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) flow = config_flow.ElgatoFlowHandler() diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 0f61178107d..576a464c86a 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -38,6 +38,7 @@ from homeassistant.components.emulated_hue.hue_api import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, + CONTENT_TYPE_JSON, HTTP_NOT_FOUND, HTTP_OK, HTTP_UNAUTHORIZED, @@ -245,7 +246,7 @@ async def test_discover_lights(hue_client): result = await hue_client.get("/api/username/lights") assert result.status == HTTP_OK - assert "application/json" in result.headers["content-type"] + assert CONTENT_TYPE_JSON in result.headers["content-type"] result_json = await result.json() @@ -342,7 +343,7 @@ async def test_light_without_brightness_can_be_turned_off(hass_hue, hue_client): no_brightness_result_json = await no_brightness_result.json() assert no_brightness_result.status == HTTP_OK - assert "application/json" in no_brightness_result.headers["content-type"] + assert CONTENT_TYPE_JSON in no_brightness_result.headers["content-type"] assert len(no_brightness_result_json) == 1 # Verify that SERVICE_TURN_OFF has been called @@ -384,7 +385,7 @@ async def test_light_without_brightness_can_be_turned_on(hass_hue, hue_client): no_brightness_result_json = await no_brightness_result.json() assert no_brightness_result.status == HTTP_OK - assert "application/json" in no_brightness_result.headers["content-type"] + assert CONTENT_TYPE_JSON in no_brightness_result.headers["content-type"] assert len(no_brightness_result_json) == 1 # Verify that SERVICE_TURN_ON has been called @@ -421,7 +422,7 @@ async def test_discover_full_state(hue_client): result = await hue_client.get(f"/api/{HUE_API_USERNAME}") assert result.status == HTTP_OK - assert "application/json" in result.headers["content-type"] + assert CONTENT_TYPE_JSON in result.headers["content-type"] result_json = await result.json() @@ -471,7 +472,7 @@ async def test_discover_config(hue_client): result = await hue_client.get(f"/api/{HUE_API_USERNAME}/config") assert result.status == 200 - assert "application/json" in result.headers["content-type"] + assert CONTENT_TYPE_JSON in result.headers["content-type"] config_json = await result.json() @@ -508,7 +509,7 @@ async def test_discover_config(hue_client): result = await hue_client.get("/api/config") assert result.status == 200 - assert "application/json" in result.headers["content-type"] + assert CONTENT_TYPE_JSON in result.headers["content-type"] config_json = await result.json() assert "error" not in config_json @@ -517,7 +518,7 @@ async def test_discover_config(hue_client): result = await hue_client.get("/api/wronguser/config") assert result.status == 200 - assert "application/json" in result.headers["content-type"] + assert CONTENT_TYPE_JSON in result.headers["content-type"] config_json = await result.json() assert "error" not in config_json @@ -550,7 +551,7 @@ async def test_get_light_state(hass_hue, hue_client): result = await hue_client.get("/api/username/lights") assert result.status == HTTP_OK - assert "application/json" in result.headers["content-type"] + assert CONTENT_TYPE_JSON in result.headers["content-type"] result_json = await result.json() @@ -667,7 +668,7 @@ async def test_put_light_state(hass, hass_hue, hue_client): ceiling_result_json = await ceiling_result.json() assert ceiling_result.status == HTTP_OK - assert "application/json" in ceiling_result.headers["content-type"] + assert CONTENT_TYPE_JSON in ceiling_result.headers["content-type"] assert len(ceiling_result_json) == 1 @@ -857,7 +858,7 @@ async def test_close_cover(hass_hue, hue_client): ) assert cover_result.status == HTTP_OK - assert "application/json" in cover_result.headers["content-type"] + assert CONTENT_TYPE_JSON in cover_result.headers["content-type"] for _ in range(7): future = dt_util.utcnow() + timedelta(seconds=1) @@ -905,7 +906,7 @@ async def test_set_position_cover(hass_hue, hue_client): ) assert cover_result.status == HTTP_OK - assert "application/json" in cover_result.headers["content-type"] + assert CONTENT_TYPE_JSON in cover_result.headers["content-type"] cover_result_json = await cover_result.json() @@ -1104,7 +1105,7 @@ async def test_get_empty_groups_state(hue_client): # pylint: disable=invalid-name async def perform_put_test_on_ceiling_lights( - hass_hue, hue_client, content_type="application/json" + hass_hue, hue_client, content_type=CONTENT_TYPE_JSON ): """Test the setting of a light.""" # Turn the office light off first @@ -1124,7 +1125,7 @@ async def perform_put_test_on_ceiling_lights( ) assert office_result.status == HTTP_OK - assert "application/json" in office_result.headers["content-type"] + assert CONTENT_TYPE_JSON in office_result.headers["content-type"] office_result_json = await office_result.json() @@ -1143,7 +1144,7 @@ async def perform_get_light_state_by_number(client, entity_number, expected_stat assert result.status == expected_status if expected_status == HTTP_OK: - assert "application/json" in result.headers["content-type"] + assert CONTENT_TYPE_JSON in result.headers["content-type"] return await result.json() @@ -1164,7 +1165,7 @@ async def perform_put_light_state( entity_id, is_on, brightness=None, - content_type="application/json", + content_type=CONTENT_TYPE_JSON, hue=None, saturation=None, color_temp=None, diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 8a5556b1222..e68688399e0 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -9,7 +9,7 @@ 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 homeassistant.const import CONTENT_TYPE_JSON, HTTP_OK from tests.common import get_test_home_assistant, get_test_instance_port @@ -167,7 +167,7 @@ MX:3 ) assert result.status_code == HTTP_OK - assert "application/json" in result.headers["content-type"] + assert CONTENT_TYPE_JSON in result.headers["content-type"] resp_json = result.json() success_json = resp_json[0] @@ -186,7 +186,7 @@ MX:3 ) assert result.status_code == HTTP_OK - assert "application/json" in result.headers["content-type"] + assert CONTENT_TYPE_JSON in result.headers["content-type"] resp_json = result.json() assert len(resp_json) == 1 diff --git a/tests/components/flo/conftest.py b/tests/components/flo/conftest.py index 3a835cb0547..907feb85569 100644 --- a/tests/components/flo/conftest.py +++ b/tests/components/flo/conftest.py @@ -5,7 +5,7 @@ import time import pytest from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONTENT_TYPE_JSON from .common import TEST_EMAIL_ADDRESS, TEST_PASSWORD, TEST_TOKEN, TEST_USER_ID @@ -40,7 +40,7 @@ def aioclient_mock_fixture(aioclient_mock): "timeNow": now, } ), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, status=200, ) # Mocks the device for flo. @@ -48,28 +48,28 @@ def aioclient_mock_fixture(aioclient_mock): "https://api-gw.meetflo.com/api/v2/devices/98765", text=load_fixture("flo/device_info_response.json"), status=200, - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_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"}, + headers={"Content-Type": CONTENT_TYPE_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"}, + headers={"Content-Type": CONTENT_TYPE_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"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, params={"expand": "locations"}, ) # Mocks the user info for flo. @@ -77,14 +77,14 @@ def aioclient_mock_fixture(aioclient_mock): "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"}, + headers={"Content-Type": CONTENT_TYPE_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"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, json={"valve": {"target": "open"}}, ) # Mocks the valve close call for flo. @@ -92,7 +92,7 @@ def aioclient_mock_fixture(aioclient_mock): "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"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, json={"valve": {"target": "closed"}}, ) # Mocks the health test call for flo. @@ -100,14 +100,14 @@ def aioclient_mock_fixture(aioclient_mock): "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"}, + headers={"Content-Type": CONTENT_TYPE_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"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, json={"systemMode": {"target": "home"}}, ) # Mocks the health test call for flo. @@ -115,7 +115,7 @@ def aioclient_mock_fixture(aioclient_mock): "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"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, json={"systemMode": {"target": "away"}}, ) # Mocks the health test call for flo. @@ -123,7 +123,7 @@ def aioclient_mock_fixture(aioclient_mock): "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"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, json={ "systemMode": { "target": "sleep", diff --git a/tests/components/flo/test_config_flow.py b/tests/components/flo/test_config_flow.py index 265f2ae2d38..edc9705b7cd 100644 --- a/tests/components/flo/test_config_flow.py +++ b/tests/components/flo/test_config_flow.py @@ -4,6 +4,7 @@ import time from homeassistant import config_entries, setup from homeassistant.components.flo.const import DOMAIN +from homeassistant.const import CONTENT_TYPE_JSON from .common import TEST_EMAIL_ADDRESS, TEST_PASSWORD, TEST_TOKEN, TEST_USER_ID @@ -53,7 +54,7 @@ async def test_form_cannot_connect(hass, aioclient_mock): "timeNow": now, } ), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, status=400, ) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index e56342d861f..b18d8f300cf 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -9,7 +9,7 @@ import requests_mock import homeassistant.components.binary_sensor as binary_sensor import homeassistant.components.rest.binary_sensor as rest -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import CONTENT_TYPE_JSON, STATE_OFF, STATE_ON from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import template from homeassistant.setup import setup_component @@ -143,7 +143,7 @@ class TestRestBinarySensorSetup(unittest.TestCase): "authentication": "basic", "username": "my username", "password": "my password", - "headers": {"Accept": "application/json"}, + "headers": {"Accept": CONTENT_TYPE_JSON}, } }, ) @@ -170,7 +170,7 @@ class TestRestBinarySensorSetup(unittest.TestCase): "authentication": "basic", "username": "my username", "password": "my password", - "headers": {"Accept": "application/json"}, + "headers": {"Accept": CONTENT_TYPE_JSON}, } }, ) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 7fbb211fb4f..5ffa12c6167 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -12,7 +12,12 @@ 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 CONTENT_TYPE_TEXT_PLAIN, DATA_MEGABYTES, SERVICE_RELOAD +from homeassistant.const import ( + CONTENT_TYPE_JSON, + CONTENT_TYPE_TEXT_PLAIN, + DATA_MEGABYTES, + SERVICE_RELOAD, +) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.config_validation import template from homeassistant.setup import async_setup_component, setup_component @@ -135,7 +140,7 @@ class TestRestSensorSetup(unittest.TestCase): "authentication": "basic", "username": "my username", "password": "my password", - "headers": {"Accept": "application/json"}, + "headers": {"Accept": CONTENT_TYPE_JSON}, } }, ) @@ -164,7 +169,7 @@ class TestRestSensorSetup(unittest.TestCase): "authentication": "basic", "username": "my username", "password": "my password", - "headers": {"Accept": "application/json"}, + "headers": {"Accept": CONTENT_TYPE_JSON}, } }, ) @@ -212,7 +217,7 @@ class TestRestSensor(unittest.TestCase): "rest.RestData.update", side_effect=self.update_side_effect( '{ "key": "' + self.initial_state + '" }', - CaseInsensitiveDict({"Content-Type": "application/json"}), + CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}), ), ) self.name = "foo" @@ -276,7 +281,7 @@ class TestRestSensor(unittest.TestCase): "rest.RestData.update", side_effect=self.update_side_effect( '{ "key": "updated_state" }', - CaseInsensitiveDict({"Content-Type": "application/json"}), + CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}), ), ) self.sensor.update() @@ -288,7 +293,7 @@ class TestRestSensor(unittest.TestCase): self.rest.update = Mock( "rest.RestData.update", side_effect=self.update_side_effect( - "plain_state", CaseInsensitiveDict({"Content-Type": "application/json"}) + "plain_state", CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}) ), ) self.sensor = rest.RestSensor( @@ -313,7 +318,7 @@ class TestRestSensor(unittest.TestCase): "rest.RestData.update", side_effect=self.update_side_effect( '{ "key": "some_json_value" }', - CaseInsensitiveDict({"Content-Type": "application/json"}), + CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}), ), ) self.sensor = rest.RestSensor( @@ -337,7 +342,7 @@ class TestRestSensor(unittest.TestCase): "rest.RestData.update", side_effect=self.update_side_effect( '[{ "key": "another_value" }]', - CaseInsensitiveDict({"Content-Type": "application/json"}), + CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}), ), ) self.sensor = rest.RestSensor( @@ -361,7 +366,7 @@ class TestRestSensor(unittest.TestCase): self.rest.update = Mock( "rest.RestData.update", side_effect=self.update_side_effect( - None, CaseInsensitiveDict({"Content-Type": "application/json"}) + None, CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}) ), ) self.sensor = rest.RestSensor( @@ -387,7 +392,7 @@ class TestRestSensor(unittest.TestCase): "rest.RestData.update", side_effect=self.update_side_effect( '["list", "of", "things"]', - CaseInsensitiveDict({"Content-Type": "application/json"}), + CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}), ), ) self.sensor = rest.RestSensor( @@ -439,7 +444,7 @@ class TestRestSensor(unittest.TestCase): "rest.RestData.update", side_effect=self.update_side_effect( '{ "key": "json_state_updated_value" }', - CaseInsensitiveDict({"Content-Type": "application/json"}), + CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}), ), ) self.sensor = rest.RestSensor( @@ -471,7 +476,7 @@ class TestRestSensor(unittest.TestCase): "rest.RestData.update", side_effect=self.update_side_effect( '{ "toplevel": {"master_value": "master", "second_level": {"some_json_key": "some_json_value", "some_json_key2": "some_json_value2" } } }', - CaseInsensitiveDict({"Content-Type": "application/json"}), + CaseInsensitiveDict({"Content-Type": CONTENT_TYPE_JSON}), ), ) self.sensor = rest.RestSensor( diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index 065645fffd1..47eec8e700f 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -123,7 +123,7 @@ class TestRestSwitchSetup: CONF_NAME: "foo", CONF_RESOURCE: "http://localhost", rest.CONF_STATE_RESOURCE: "http://localhost/state", - CONF_HEADERS: {"Content-type": "application/json"}, + CONF_HEADERS: {"Content-type": CONTENT_TYPE_JSON}, rest.CONF_BODY_ON: "custom on text", rest.CONF_BODY_OFF: "custom off text", } @@ -143,7 +143,7 @@ class TestRestSwitch: self.method = "post" self.resource = "http://localhost/" self.state_resource = self.resource - self.headers = {"Content-type": "application/json"} + self.headers = {"Content-type": CONTENT_TYPE_JSON} self.auth = None self.body_on = Template("on", self.hass) self.body_off = Template("off", self.hass) diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py index 780e513ea38..80ede61be84 100644 --- a/tests/components/rest_command/test_init.py +++ b/tests/components/rest_command/test_init.py @@ -4,7 +4,7 @@ import asyncio import aiohttp import homeassistant.components.rest_command as rc -from homeassistant.const import CONTENT_TYPE_TEXT_PLAIN +from homeassistant.const import CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN from homeassistant.setup import setup_component from tests.common import assert_setup_component, get_test_home_assistant @@ -222,24 +222,24 @@ class TestRestCommandComponent: "content_type_test": {"content_type": CONTENT_TYPE_TEXT_PLAIN}, "headers_test": { "headers": { - "Accept": "application/json", + "Accept": CONTENT_TYPE_JSON, "User-Agent": "Mozilla/5.0", } }, "headers_and_content_type_test": { - "headers": {"Accept": "application/json"}, + "headers": {"Accept": CONTENT_TYPE_JSON}, "content_type": CONTENT_TYPE_TEXT_PLAIN, }, "headers_and_content_type_override_test": { "headers": { - "Accept": "application/json", + "Accept": CONTENT_TYPE_JSON, aiohttp.hdrs.CONTENT_TYPE: "application/pdf", }, "content_type": CONTENT_TYPE_TEXT_PLAIN, }, "headers_template_test": { "headers": { - "Accept": "application/json", + "Accept": CONTENT_TYPE_JSON, "User-Agent": "Mozilla/{{ 3 + 2 }}.0", } }, @@ -291,7 +291,7 @@ class TestRestCommandComponent: # headers_test assert len(aioclient_mock.mock_calls[2][3]) == 2 - assert aioclient_mock.mock_calls[2][3].get("Accept") == "application/json" + assert aioclient_mock.mock_calls[2][3].get("Accept") == CONTENT_TYPE_JSON assert aioclient_mock.mock_calls[2][3].get("User-Agent") == "Mozilla/5.0" # headers_and_content_type_test @@ -300,7 +300,7 @@ class TestRestCommandComponent: aioclient_mock.mock_calls[3][3].get(aiohttp.hdrs.CONTENT_TYPE) == CONTENT_TYPE_TEXT_PLAIN ) - assert aioclient_mock.mock_calls[3][3].get("Accept") == "application/json" + assert aioclient_mock.mock_calls[3][3].get("Accept") == CONTENT_TYPE_JSON # headers_and_content_type_override_test assert len(aioclient_mock.mock_calls[4][3]) == 2 @@ -308,11 +308,11 @@ class TestRestCommandComponent: aioclient_mock.mock_calls[4][3].get(aiohttp.hdrs.CONTENT_TYPE) == CONTENT_TYPE_TEXT_PLAIN ) - assert aioclient_mock.mock_calls[4][3].get("Accept") == "application/json" + assert aioclient_mock.mock_calls[4][3].get("Accept") == CONTENT_TYPE_JSON # headers_template_test assert len(aioclient_mock.mock_calls[5][3]) == 2 - assert aioclient_mock.mock_calls[5][3].get("Accept") == "application/json" + assert aioclient_mock.mock_calls[5][3].get("Accept") == CONTENT_TYPE_JSON assert aioclient_mock.mock_calls[5][3].get("User-Agent") == "Mozilla/5.0" # headers_and_content_type_override_template_test diff --git a/tests/components/sonarr/__init__.py b/tests/components/sonarr/__init__.py index b076f39d0d2..0ce08e0c868 100644 --- a/tests/components/sonarr/__init__.py +++ b/tests/components/sonarr/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_PORT, CONF_SSL, CONF_VERIFY_SSL, + CONTENT_TYPE_JSON, ) from homeassistant.helpers.typing import HomeAssistantType @@ -85,43 +86,43 @@ def mock_connection( aioclient_mock.get( f"{sonarr_url}/system/status", text=load_fixture("sonarr/system-status.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( f"{sonarr_url}/diskspace", text=load_fixture("sonarr/diskspace.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( f"{sonarr_url}/calendar", text=load_fixture("sonarr/calendar.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( f"{sonarr_url}/command", text=load_fixture("sonarr/command.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( f"{sonarr_url}/queue", text=load_fixture("sonarr/queue.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( f"{sonarr_url}/series", text=load_fixture("sonarr/series.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( f"{sonarr_url}/wanted/missing", text=load_fixture("sonarr/wanted-missing.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) diff --git a/tests/components/twentemilieu/test_config_flow.py b/tests/components/twentemilieu/test_config_flow.py index 7dd19e755f3..d176c6b1ee4 100644 --- a/tests/components/twentemilieu/test_config_flow.py +++ b/tests/components/twentemilieu/test_config_flow.py @@ -9,7 +9,7 @@ from homeassistant.components.twentemilieu.const import ( CONF_POST_CODE, DOMAIN, ) -from homeassistant.const import CONF_ID +from homeassistant.const import CONF_ID, CONTENT_TYPE_JSON from tests.common import MockConfigEntry @@ -51,7 +51,7 @@ async def test_invalid_address(hass, aioclient_mock): aioclient_mock.post( "https://twentemilieuapi.ximmio.com/api/FetchAdress", json={"dataList": []}, - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) flow = config_flow.TwenteMilieuFlowHandler() @@ -72,7 +72,7 @@ async def test_address_already_set_up(hass, aioclient_mock): aioclient_mock.post( "https://twentemilieuapi.ximmio.com/api/FetchAdress", json={"dataList": [{"UniqueId": "12345"}]}, - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) flow = config_flow.TwenteMilieuFlowHandler() @@ -88,7 +88,7 @@ async def test_full_flow_implementation(hass, aioclient_mock): aioclient_mock.post( "https://twentemilieuapi.ximmio.com/api/FetchAdress", json={"dataList": [{"UniqueId": "12345"}]}, - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) flow = config_flow.TwenteMilieuFlowHandler() diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index aec6ebed664..8b935d7744b 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL, + CONTENT_TYPE_JSON, ) from .test_controller import setup_unifi_integration @@ -94,7 +95,7 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery): aioclient_mock.post( "https://1.2.3.4:1234/api/login", json={"data": "login successful", "meta": {"rc": "ok"}}, - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( @@ -103,7 +104,7 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery): "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}], "meta": {"rc": "ok"}, }, - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_configure( @@ -145,7 +146,7 @@ async def test_flow_works_multiple_sites(hass, aioclient_mock): aioclient_mock.post( "https://1.2.3.4:1234/api/login", json={"data": "login successful", "meta": {"rc": "ok"}}, - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( @@ -157,7 +158,7 @@ async def test_flow_works_multiple_sites(hass, aioclient_mock): ], "meta": {"rc": "ok"}, }, - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_configure( @@ -196,7 +197,7 @@ async def test_flow_fails_site_already_configured(hass, aioclient_mock): aioclient_mock.post( "https://1.2.3.4:1234/api/login", json={"data": "login successful", "meta": {"rc": "ok"}}, - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( @@ -205,7 +206,7 @@ async def test_flow_fails_site_already_configured(hass, aioclient_mock): "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}], "meta": {"rc": "ok"}, }, - headers={"content-type": "application/json"}, + headers={"content-type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/wled/__init__.py b/tests/components/wled/__init__.py index 487ccd9ab9e..a39d1ef6453 100644 --- a/tests/components/wled/__init__.py +++ b/tests/components/wled/__init__.py @@ -3,7 +3,7 @@ import json from homeassistant.components.wled.const import DOMAIN -from homeassistant.const import CONF_HOST, CONF_MAC +from homeassistant.const import CONF_HOST, CONF_MAC, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -24,25 +24,25 @@ async def init_integration( aioclient_mock.get( "http://192.168.1.123:80/json/", json=data, - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.post( "http://192.168.1.123:80/json/state", json=data["state"], - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( "http://192.168.1.123:80/json/info", json=data["info"], - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( "http://192.168.1.123:80/json/state", json=data["state"], - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) entry = MockConfigEntry( diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index a0c5c11e7ce..7793dc2a378 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -5,7 +5,7 @@ from wled import WLEDConnectionError from homeassistant import data_entry_flow from homeassistant.components.wled import config_flow from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant from . import init_integration @@ -45,7 +45,7 @@ async def test_show_zerconf_form( aioclient_mock.get( "http://192.168.1.123:80/json/", text=load_fixture("wled/rgb.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) flow = config_flow.WLEDFlowHandler() @@ -190,7 +190,7 @@ async def test_full_user_flow_implementation( aioclient_mock.get( "http://192.168.1.123:80/json/", text=load_fixture("wled/rgb.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) result = await hass.config_entries.flow.async_init( @@ -218,7 +218,7 @@ async def test_full_zeroconf_flow_implementation( aioclient_mock.get( "http://192.168.1.123:80/json/", text=load_fixture("wled/rgb.json"), - headers={"Content-Type": "application/json"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, ) flow = config_flow.WLEDFlowHandler() From 6c8e0e20fbd0da03cc5a5d2561a94a4721367bf5 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 23 Sep 2020 20:48:01 +0200 Subject: [PATCH 320/514] Add and use light lux constant in entire code base (#40171) --- .../components/ambient_station/__init__.py | 3 ++- homeassistant/components/awair/const.py | 3 ++- homeassistant/components/bh1750/sensor.py | 5 ++--- homeassistant/components/deconz/sensor.py | 3 ++- homeassistant/components/fibaro/sensor.py | 5 +++-- homeassistant/components/homekit/accessories.py | 3 ++- .../components/homekit_controller/sensor.py | 5 ++--- homeassistant/components/homematic/sensor.py | 13 +++++++------ .../components/homematicip_cloud/sensor.py | 3 ++- homeassistant/components/hue/sensor.py | 3 ++- homeassistant/components/isy994/const.py | 3 ++- homeassistant/components/miflora/sensor.py | 3 ++- homeassistant/components/mysensors/sensor.py | 3 ++- homeassistant/components/onewire/sensor.py | 3 ++- homeassistant/components/plant/__init__.py | 3 ++- homeassistant/components/shelly/sensor.py | 3 ++- homeassistant/components/smartthings/sensor.py | 3 ++- homeassistant/components/tahoma/sensor.py | 4 ++-- homeassistant/components/tellduslive/sensor.py | 3 ++- homeassistant/components/vera/sensor.py | 4 ++-- homeassistant/components/xiaomi_aqara/sensor.py | 3 ++- homeassistant/components/xiaomi_miio/sensor.py | 3 ++- homeassistant/components/zha/sensor.py | 3 ++- homeassistant/const.py | 3 +++ tests/components/awair/test_sensor.py | 5 +++-- tests/components/homekit/test_get_accessories.py | 3 ++- tests/components/homematicip_cloud/test_sensor.py | 5 +++-- tests/components/plant/test_init.py | 7 ++++--- tests/components/vera/test_sensor.py | 4 ++-- tests/components/zha/test_sensor.py | 3 ++- 30 files changed, 71 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 8658c3d04b9..68bfb85cf62 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_API_KEY, DEGREE, EVENT_HOMEASSISTANT_STOP, + LIGHT_LUX, PERCENTAGE, POWER_WATT, PRESSURE_INHG, @@ -214,7 +215,7 @@ SENSOR_TYPES = { TYPE_SENSOR, None, ), - TYPE_SOLARRADIATION_LX: ("Solar Rad (lx)", "lx", TYPE_SENSOR, "illuminance"), + TYPE_SOLARRADIATION_LX: ("Solar Rad (lx)", LIGHT_LUX, TYPE_SENSOR, "illuminance"), TYPE_TEMP10F: ("Temp 10", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), TYPE_TEMP1F: ("Temp 1", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), TYPE_TEMP2F: ("Temp 2", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py index e3c2a176119..b262fdec572 100644 --- a/homeassistant/components/awair/const.py +++ b/homeassistant/components/awair/const.py @@ -14,6 +14,7 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS, ) @@ -63,7 +64,7 @@ SENSOR_TYPES = { API_LUX: { ATTR_DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE, ATTR_ICON: None, - ATTR_UNIT: "lx", + ATTR_UNIT: LIGHT_LUX, ATTR_LABEL: "Illuminance", ATTR_UNIQUE_ID: "illuminance", }, diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py index df8e87f751d..4d4067eb77d 100644 --- a/homeassistant/components/bh1750/sensor.py +++ b/homeassistant/components/bh1750/sensor.py @@ -7,7 +7,7 @@ import smbus # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, DEVICE_CLASS_ILLUMINANCE +from homeassistant.const import CONF_NAME, DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -37,7 +37,6 @@ OPERATION_MODES = { ONE_TIME_HIGH_RES_MODE_2: (0x21, False), # 0.5lx resolution. } -SENSOR_UNIT = "lx" DEFAULT_NAME = "BH1750 Light Sensor" DEFAULT_I2C_ADDRESS = "0x23" DEFAULT_I2C_BUS = 1 @@ -85,7 +84,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _LOGGER.error("BH1750 sensor not detected at %s", i2c_address) return False - dev = [BH1750Sensor(sensor, name, SENSOR_UNIT, config[CONF_MULTIPLIER])] + dev = [BH1750Sensor(sensor, name, LIGHT_LUX, config[CONF_MULTIPLIER])] _LOGGER.info( "Setup of BH1750 light sensor at %s in mode %s is complete", i2c_address, diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 0c9ecb032df..72465282421 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -22,6 +22,7 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, + LIGHT_LUX, PERCENTAGE, POWER_WATT, PRESSURE_HPA, @@ -59,7 +60,7 @@ ICON = { UNIT_OF_MEASUREMENT = { Consumption: ENERGY_KILO_WATT_HOUR, Humidity: PERCENTAGE, - LightLevel: "lx", + LightLevel: LIGHT_LUX, Power: POWER_WATT, Pressure: PRESSURE_HPA, Temperature: TEMP_CELSIUS, diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py index e9e7265f917..3c812970022 100644 --- a/homeassistant/components/fibaro/sensor.py +++ b/homeassistant/components/fibaro/sensor.py @@ -7,6 +7,7 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT, @@ -35,7 +36,7 @@ SENSOR_TYPES = { None, DEVICE_CLASS_HUMIDITY, ], - "com.fibaro.lightSensor": ["Light", "lx", None, DEVICE_CLASS_ILLUMINANCE], + "com.fibaro.lightSensor": ["Light", LIGHT_LUX, None, DEVICE_CLASS_ILLUMINANCE], } _LOGGER = logging.getLogger(__name__) @@ -71,7 +72,7 @@ class FibaroSensor(FibaroDevice, Entity): try: if not self._unit: if self.fibaro_device.properties.unit == "lux": - self._unit = "lx" + self._unit = LIGHT_LUX elif self.fibaro_device.properties.unit == "C": self._unit = TEMP_CELSIUS elif self.fibaro_device.properties.unit == "F": diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 2fb61a61fed..3912d8e9056 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -24,6 +24,7 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + LIGHT_LUX, PERCENTAGE, STATE_ON, STATE_UNAVAILABLE, @@ -197,7 +198,7 @@ def get_accessory(hass, driver, state, aid, config): a_type = "CarbonMonoxideSensor" elif device_class == DEVICE_CLASS_CO2 or DEVICE_CLASS_CO2 in state.entity_id: a_type = "CarbonDioxideSensor" - elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ("lm", "lx"): + elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ("lm", LIGHT_LUX): a_type = "LightSensor" elif state.domain == "switch": diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 944729d2e5c..2075eb9dcc3 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -7,6 +7,7 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS, ) @@ -19,8 +20,6 @@ TEMP_C_ICON = "mdi:thermometer" BRIGHTNESS_ICON = "mdi:brightness-6" CO2_ICON = "mdi:molecule-co2" -UNIT_LUX = "lux" - class HomeKitHumiditySensor(HomeKitEntity): """Representation of a Homekit humidity sensor.""" @@ -113,7 +112,7 @@ class HomeKitLightSensor(HomeKitEntity): @property def unit_of_measurement(self): """Return units for the sensor.""" - return UNIT_LUX + return LIGHT_LUX @property def state(self): diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 09ceb7c1da2..e6439c451c1 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -10,6 +10,7 @@ from homeassistant.const import ( ENERGY_WATT_HOUR, FREQUENCY_HERTZ, LENGTH_MILLIMETERS, + LIGHT_LUX, PERCENTAGE, POWER_WATT, PRESSURE_HPA, @@ -50,12 +51,12 @@ HM_UNIT_HA_CAST = { "ENERGY_COUNTER": ENERGY_WATT_HOUR, "GAS_POWER": VOLUME_CUBIC_METERS, "GAS_ENERGY_COUNTER": VOLUME_CUBIC_METERS, - "LUX": "lx", - "ILLUMINATION": "lx", - "CURRENT_ILLUMINATION": "lx", - "AVERAGE_ILLUMINATION": "lx", - "LOWEST_ILLUMINATION": "lx", - "HIGHEST_ILLUMINATION": "lx", + "LUX": LIGHT_LUX, + "ILLUMINATION": LIGHT_LUX, + "CURRENT_ILLUMINATION": LIGHT_LUX, + "AVERAGE_ILLUMINATION": LIGHT_LUX, + "LOWEST_ILLUMINATION": LIGHT_LUX, + "HIGHEST_ILLUMINATION": LIGHT_LUX, "RAIN_COUNTER": LENGTH_MILLIMETERS, "WIND_SPEED": SPEED_KILOMETERS_PER_HOUR, "WIND_DIRECTION": DEGREE, diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index f307fb9274e..082d7e9e355 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -31,6 +31,7 @@ from homeassistant.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, LENGTH_MILLIMETERS, + LIGHT_LUX, PERCENTAGE, POWER_WATT, SPEED_KILOMETERS_PER_HOUR, @@ -282,7 +283,7 @@ class HomematicipIlluminanceSensor(HomematicipGenericEntity): @property def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" - return "lx" + return LIGHT_LUX @property def device_state_attributes(self) -> Dict[str, Any]: diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index e96f844a5e1..f5911bbb50c 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -10,6 +10,7 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS, ) @@ -41,7 +42,7 @@ class HueLightLevel(GenericHueGaugeSensorEntity): """The light level sensor entity for a Hue motion sensor device.""" device_class = DEVICE_CLASS_ILLUMINANCE - unit_of_measurement = "lx" + unit_of_measurement = LIGHT_LUX @property def state(self): diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 7bef122f7d3..e003a52c91f 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -58,6 +58,7 @@ from homeassistant.const import ( LENGTH_METERS, LENGTH_MILES, LENGTH_MILLIMETERS, + LIGHT_LUX, MASS_KILOGRAMS, MASS_POUNDS, PERCENTAGE, @@ -352,7 +353,7 @@ UOM_FRIENDLY_NAME = { "33": ENERGY_KILO_WATT_HOUR, "34": "liedu", "35": VOLUME_LITERS, - "36": "lx", + "36": LIGHT_LUX, "37": "mercalli", "38": LENGTH_METERS, "39": f"{VOLUME_CUBIC_METERS}/{TIME_HOURS}", diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index 6206c67dc03..94db0417ddb 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_NAME, CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START, + LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT, @@ -53,7 +54,7 @@ ATTR_LAST_SUCCESSFUL_UPDATE = "last_successful_update" # Sensor types are defined like: Name, units, icon SENSOR_TYPES = { "temperature": ["Temperature", TEMP_CELSIUS, "mdi:thermometer"], - "light": ["Light intensity", "lx", "mdi:white-balance-sunny"], + "light": ["Light intensity", LIGHT_LUX, "mdi:white-balance-sunny"], "moisture": ["Moisture", PERCENTAGE, "mdi:water-percent"], "conductivity": ["Conductivity", CONDUCTIVITY, "mdi:flash-circle"], "battery": ["Battery", PERCENTAGE, "mdi:battery-charging"], diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 24c1d9be09b..6a6e95ddd01 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -9,6 +9,7 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, FREQUENCY_HERTZ, LENGTH_METERS, + LIGHT_LUX, MASS_KILOGRAMS, PERCENTAGE, POWER_WATT, @@ -41,7 +42,7 @@ SENSORS = { "V_LEVEL": { "S_SOUND": ["dB", "mdi:volume-high"], "S_VIBRATION": [FREQUENCY_HERTZ, None], - "S_LIGHT_LEVEL": ["lx", "mdi:white-balance-sunny"], + "S_LIGHT_LEVEL": [LIGHT_LUX, "mdi:white-balance-sunny"], }, "V_VOLTAGE": [VOLT, "mdi:flash"], "V_CURRENT": [ELECTRICAL_CURRENT_AMPERE, "mdi:flash-auto"], diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 148c596e130..4620d7593c9 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PORT, ELECTRICAL_CURRENT_AMPERE, + LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS, VOLT, @@ -70,7 +71,7 @@ SENSOR_TYPES = { "humidity": ["humidity", PERCENTAGE], "humidity_raw": ["humidity", PERCENTAGE], "pressure": ["pressure", "mb"], - "illuminance": ["illuminance", "lux"], + "illuminance": ["illuminance", LIGHT_LUX], "wetness_0": ["wetness", PERCENTAGE], "wetness_1": ["wetness", PERCENTAGE], "wetness_2": ["wetness", PERCENTAGE], diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index d78b12c06e0..1cb2416d12a 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONDUCTIVITY, CONF_SENSORS, + LIGHT_LUX, PERCENTAGE, STATE_OK, STATE_PROBLEM, @@ -153,7 +154,7 @@ class Plant(Entity): "max": CONF_MAX_CONDUCTIVITY, }, READING_BRIGHTNESS: { - ATTR_UNIT_OF_MEASUREMENT: "lux", + ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX, "min": CONF_MIN_BRIGHTNESS, "max": CONF_MAX_BRIGHTNESS, }, diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 52c2a1b5228..14c1b645118 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -5,6 +5,7 @@ from homeassistant.const import ( DEGREE, ELECTRICAL_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, + LIGHT_LUX, PERCENTAGE, POWER_WATT, ) @@ -116,7 +117,7 @@ SENSORS = { ), ("sensor", "luminosity"): BlockAttributeDescription( name="Luminosity", - unit="lx", + unit=LIGHT_LUX, device_class=sensor.DEVICE_CLASS_ILLUMINANCE, ), ("sensor", "tilt"): BlockAttributeDescription(name="tilt", unit=DEGREE), diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index a7a15e3cefc..f0240886913 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -13,6 +13,7 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, ENERGY_KILO_WATT_HOUR, + LIGHT_LUX, MASS_KILOGRAMS, PERCENTAGE, POWER_WATT, @@ -116,7 +117,7 @@ CAPABILITY_TO_SENSORS = { ) ], Capability.illuminance_measurement: [ - Map(Attribute.illuminance, "Illuminance", "lux", DEVICE_CLASS_ILLUMINANCE) + Map(Attribute.illuminance, "Illuminance", LIGHT_LUX, DEVICE_CLASS_ILLUMINANCE) ], Capability.infrared_level: [ Map(Attribute.infrared_level, "Infrared Level", PERCENTAGE, None) diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py index 7b28989ad8e..fb1129cfa0e 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, PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import ATTR_BATTERY_LEVEL, LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS from homeassistant.helpers.entity import Entity from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice @@ -49,7 +49,7 @@ class TahomaSensor(TahomaDevice, Entity): if self.tahoma_device.type == "io:SomfyBasicContactIOSystemSensor": return None if self.tahoma_device.type == "io:LightIOSystemSensor": - return "lx" + return LIGHT_LUX if self.tahoma_device.type == "Humidity Sensor": return PERCENTAGE if self.tahoma_device.type == "rtds:RTDSContactSensor": diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py index 7b785a808e8..e322481813a 100644 --- a/homeassistant/components/tellduslive/sensor.py +++ b/homeassistant/components/tellduslive/sensor.py @@ -7,6 +7,7 @@ from homeassistant.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, LENGTH_MILLIMETERS, + LIGHT_LUX, PERCENTAGE, POWER_WATT, SPEED_METERS_PER_SECOND, @@ -53,7 +54,7 @@ SENSOR_TYPES = { SENSOR_TYPE_WINDGUST: ["Wind gust", SPEED_METERS_PER_SECOND, "", None], SENSOR_TYPE_UV: ["UV", UV_INDEX, "", None], SENSOR_TYPE_WATT: ["Power", POWER_WATT, "", None], - SENSOR_TYPE_LUMINANCE: ["Luminance", "lx", None, DEVICE_CLASS_ILLUMINANCE], + SENSOR_TYPE_LUMINANCE: ["Luminance", LIGHT_LUX, None, DEVICE_CLASS_ILLUMINANCE], SENSOR_TYPE_DEW_POINT: ["Dew Point", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], SENSOR_TYPE_BAROMETRIC_PRESSURE: ["Barometric Pressure", "kPa", "", None], } diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index 0c51094fbc0..9c3dd097a78 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 PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.util import convert @@ -60,7 +60,7 @@ class VeraSensor(VeraDevice[veraApi.VeraSensor], Entity): if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR: return self._temperature_units if self.vera_device.category == veraApi.CATEGORY_LIGHT_SENSOR: - return "lx" + return LIGHT_LUX if self.vera_device.category == veraApi.CATEGORY_UV_SENSOR: return "level" if self.vera_device.category == veraApi.CATEGORY_HUMIDITY_SENSOR: diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py index ed1792a5fdb..5b1d3467d25 100644 --- a/homeassistant/components/xiaomi_aqara/sensor.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -9,6 +9,7 @@ from homeassistant.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + LIGHT_LUX, PERCENTAGE, POWER_WATT, PRESSURE_HPA, @@ -24,7 +25,7 @@ SENSOR_TYPES = { "temperature": [TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], "humidity": [PERCENTAGE, None, DEVICE_CLASS_HUMIDITY], "illumination": ["lm", None, DEVICE_CLASS_ILLUMINANCE], - "lux": ["lx", None, DEVICE_CLASS_ILLUMINANCE], + "lux": [LIGHT_LUX, None, DEVICE_CLASS_ILLUMINANCE], "pressure": [PRESSURE_HPA, None, DEVICE_CLASS_PRESSURE], "bed_activity": ["μm", None, None], "load_power": [POWER_WATT, None, DEVICE_CLASS_POWER], diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 15dc1bea8bd..6e25750ea50 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -21,6 +21,7 @@ from homeassistant.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + LIGHT_LUX, PERCENTAGE, PRESSURE_HPA, TEMP_CELSIUS, @@ -307,7 +308,7 @@ class XiaomiGatewayIlluminanceSensor(Entity): @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" - return "lux" + return LIGHT_LUX @property def device_class(self): diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 215299ca34f..a18d6bfa9dd 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + LIGHT_LUX, PERCENTAGE, POWER_WATT, PRESSURE_HPA, @@ -235,7 +236,7 @@ class Illuminance(Sensor): SENSOR_ATTR = "measured_value" _device_class = DEVICE_CLASS_ILLUMINANCE - _unit = "lx" + _unit = LIGHT_LUX @staticmethod def formatter(value): diff --git a/homeassistant/const.py b/homeassistant/const.py index e1738cf6425..4d5f4db665c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -449,6 +449,9 @@ MASS_POUNDS: str = "lb" # Conductivity units CONDUCTIVITY: str = f"µS/{LENGTH_CENTIMETERS}" +# Light units +LIGHT_LUX: str = "lx" + # UV Index units UV_INDEX: str = "UV index" diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index 4d48959632d..3b013fad29c 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, + LIGHT_LUX, PERCENTAGE, STATE_UNAVAILABLE, TEMP_CELSIUS, @@ -232,7 +233,7 @@ async def test_awair_mint_sensors(hass): "sensor.living_room_illuminance", f"{AWAIR_UUID}_{SENSOR_TYPES[API_LUX][ATTR_UNIQUE_ID]}", "441.7", - {ATTR_UNIT_OF_MEASUREMENT: "lx"}, + {ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX}, ) # The Mint does not have a CO2 sensor. @@ -290,7 +291,7 @@ async def test_awair_omni_sensors(hass): "sensor.living_room_illuminance", f"{AWAIR_UUID}_{SENSOR_TYPES[API_LUX][ATTR_UNIQUE_ID]}", "804.9", - {ATTR_UNIT_OF_MEASUREMENT: "lx"}, + {ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX}, ) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index bcbbbf3bcbf..a7468955d36 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -24,6 +24,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_TYPE, + LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT, @@ -190,7 +191,7 @@ def test_type_media_player(type_name, entity_id, state, attrs, config): ), ("LightSensor", "sensor.light", "900", {ATTR_DEVICE_CLASS: "illuminance"}), ("LightSensor", "sensor.light", "900", {ATTR_UNIT_OF_MEASUREMENT: "lm"}), - ("LightSensor", "sensor.light", "900", {ATTR_UNIT_OF_MEASUREMENT: "lx"}), + ("LightSensor", "sensor.light", "900", {ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX}), ( "TemperatureSensor", "sensor.temperature", diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index de9fc276795..20c5c41a5b5 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -25,6 +25,7 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, LENGTH_MILLIMETERS, + LIGHT_LUX, PERCENTAGE, POWER_WATT, SPEED_KILOMETERS_PER_HOUR, @@ -248,7 +249,7 @@ async def test_hmip_illuminance_sensor1(hass, default_mock_hap_factory): ) assert ha_state.state == "4890.0" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "lx" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == LIGHT_LUX await async_manipulate_test_data(hass, hmip_device, "illumination", 231) ha_state = hass.states.get(entity_id) assert ha_state.state == "231" @@ -268,7 +269,7 @@ async def test_hmip_illuminance_sensor2(hass, default_mock_hap_factory): ) assert ha_state.state == "807.3" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "lx" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == LIGHT_LUX await async_manipulate_test_data(hass, hmip_device, "averageIllumination", 231) ha_state = hass.states.get(entity_id) assert ha_state.state == "231" diff --git a/tests/components/plant/test_init.py b/tests/components/plant/test_init.py index 866702488dc..f30350141c4 100644 --- a/tests/components/plant/test_init.py +++ b/tests/components/plant/test_init.py @@ -8,6 +8,7 @@ import homeassistant.components.plant as plant from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONDUCTIVITY, + LIGHT_LUX, STATE_OK, STATE_PROBLEM, STATE_UNAVAILABLE, @@ -187,17 +188,17 @@ async def test_brightness_history(hass): assert await async_setup_component( hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}} ) - hass.states.async_set(BRIGHTNESS_ENTITY, 100, {ATTR_UNIT_OF_MEASUREMENT: "lux"}) + hass.states.async_set(BRIGHTNESS_ENTITY, 100, {ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX}) await hass.async_block_till_done() state = hass.states.get(f"plant.{plant_name}") assert STATE_PROBLEM == state.state - hass.states.async_set(BRIGHTNESS_ENTITY, 600, {ATTR_UNIT_OF_MEASUREMENT: "lux"}) + hass.states.async_set(BRIGHTNESS_ENTITY, 600, {ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX}) await hass.async_block_till_done() state = hass.states.get(f"plant.{plant_name}") assert STATE_OK == state.state - hass.states.async_set(BRIGHTNESS_ENTITY, 100, {ATTR_UNIT_OF_MEASUREMENT: "lux"}) + hass.states.async_set(BRIGHTNESS_ENTITY, 100, {ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX}) await hass.async_block_till_done() state = hass.states.get(f"plant.{plant_name}") assert STATE_OK == state.state diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py index 58cedeee450..3d6b11b0685 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 ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, LIGHT_LUX, PERCENTAGE from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config @@ -90,7 +90,7 @@ async def test_light_sensor( category=pv.CATEGORY_LIGHT_SENSOR, class_property="light", assert_states=(("12", "12"), ("13", "13")), - assert_unit_of_measurement="lx", + assert_unit_of_measurement=LIGHT_LUX, ) diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 1c7d85c4528..50ea430d1cc 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -14,6 +14,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, + LIGHT_LUX, PERCENTAGE, POWER_WATT, PRESSURE_HPA, @@ -58,7 +59,7 @@ async def async_test_pressure(hass, cluster, entity_id): async def async_test_illuminance(hass, cluster, entity_id): """Test illuminance sensor.""" await send_attributes_report(hass, cluster, {1: 1, 0: 10, 2: 20}) - assert_state(hass, entity_id, "1.0", "lx") + assert_state(hass, entity_id, "1.0", LIGHT_LUX) async def async_test_metering(hass, cluster, entity_id): From 3880ac0b0daacf8b47c2bf94fb0231d9e2de1bda Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Sep 2020 13:55:32 -0500 Subject: [PATCH 321/514] Ensure group state is recalculated when re-adding on reload (#40497) --- homeassistant/components/group/cover.py | 6 +++++- homeassistant/components/group/light.py | 7 ++++++- tests/components/group/test_light.py | 7 +++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index b0d0b8b7bd2..ab2ad65713d 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -39,7 +39,7 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, ) -from homeassistant.core import State +from homeassistant.core import CoreState, State import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change_event @@ -162,6 +162,10 @@ class CoverGroup(GroupEntity, CoverEntity): self.hass, self._entities, self._update_supported_features_event ) ) + + if self.hass.state == CoreState.running: + await self.async_update() + return await super().async_added_to_hass() @property diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 289bb8df3f0..007e05edfbb 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -36,7 +36,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import State +from homeassistant.core import CoreState, 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 @@ -111,6 +111,11 @@ class LightGroup(GroupEntity, light.LightEntity): self.hass, self._entity_ids, async_state_changed_listener ) ) + + if self.hass.state == CoreState.running: + await self.async_update() + return + await super().async_added_to_hass() @property diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index a22c56b4bfc..ba8fecbed32 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -737,6 +737,11 @@ async def test_reload_with_base_integration_platform_not_setup(hass): }, ) await hass.async_block_till_done() + hass.states.async_set("light.master_hall_lights", STATE_ON) + hass.states.async_set("light.master_hall_lights_2", STATE_OFF) + + hass.states.async_set("light.outside_patio_lights", STATE_OFF) + hass.states.async_set("light.outside_patio_lights_2", STATE_OFF) yaml_path = path.join( _get_fixtures_base_path(), @@ -755,6 +760,8 @@ async def test_reload_with_base_integration_platform_not_setup(hass): 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 + assert hass.states.get("light.master_hall_lights_g").state == STATE_ON + assert hass.states.get("light.outside_patio_lights_g").state == STATE_OFF def _get_fixtures_base_path(): From 62054b84336e4d4a8a6a1ea649724e9eb86876b4 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 23 Sep 2020 21:55:33 +0200 Subject: [PATCH 322/514] Correct label in mqtt config flow (#40507) --- homeassistant/components/mqtt/strings.json | 4 ++-- homeassistant/components/mqtt/translations/en.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 75c3fdec260..d2d18af6e60 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -68,7 +68,7 @@ "birth_payload": "Birth message payload", "birth_qos": "Birth message QoS", "birth_retain": "Birth message retain", - "will_enable": "Enable birth message", + "will_enable": "Enable will message", "will_topic": "Will message topic", "will_payload": "Will message payload", "will_qos": "Will message QoS", @@ -82,4 +82,4 @@ "bad_will": "Invalid will topic." } } -} \ No newline at end of file +} diff --git a/homeassistant/components/mqtt/translations/en.json b/homeassistant/components/mqtt/translations/en.json index 8ece91cb85d..7ccdf153c13 100644 --- a/homeassistant/components/mqtt/translations/en.json +++ b/homeassistant/components/mqtt/translations/en.json @@ -72,7 +72,7 @@ "birth_retain": "Birth message retain", "birth_topic": "Birth message topic", "discovery": "Enable discovery", - "will_enable": "Enable birth message", + "will_enable": "Enable will message", "will_payload": "Will message payload", "will_qos": "Will message QoS", "will_retain": "Will message retain", From c77c0e4780bfa542ae6e9bf4a843176f397cd49d Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 24 Sep 2020 00:05:22 +0000 Subject: [PATCH 323/514] [ci skip] Translation update --- .../components/canary/translations/et.json | 11 +++++++++++ .../components/canary/translations/fr.json | 11 +++++++++++ .../components/flunearyou/translations/fr.json | 1 + .../components/plugwise/translations/fr.json | 2 +- .../synology_dsm/translations/cs.json | 9 +++++++++ .../components/unifi/translations/cs.json | 3 ++- .../components/unifi/translations/fr.json | 3 ++- .../zodiac/translations/sensor.es.json | 18 ++++++++++++++++++ .../zodiac/translations/sensor.et.json | 18 ++++++++++++++++++ .../zodiac/translations/sensor.fr.json | 18 ++++++++++++++++++ 10 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/canary/translations/et.json create mode 100644 homeassistant/components/zodiac/translations/sensor.es.json create mode 100644 homeassistant/components/zodiac/translations/sensor.et.json create mode 100644 homeassistant/components/zodiac/translations/sensor.fr.json diff --git a/homeassistant/components/canary/translations/et.json b/homeassistant/components/canary/translations/et.json new file mode 100644 index 00000000000..8e17e25ee4c --- /dev/null +++ b/homeassistant/components/canary/translations/et.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timeout": "P\u00e4ringu ajal\u00f5pp (sekundites)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/fr.json b/homeassistant/components/canary/translations/fr.json index 3d3fc087fd9..9bb1761f9fb 100644 --- a/homeassistant/components/canary/translations/fr.json +++ b/homeassistant/components/canary/translations/fr.json @@ -1,8 +1,19 @@ { "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", + "unknown": "Erreur inattendue" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, "flow_title": "Canary : {name}", "step": { "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, "title": "Se connecter \u00e0 Canary" } } diff --git a/homeassistant/components/flunearyou/translations/fr.json b/homeassistant/components/flunearyou/translations/fr.json index 27789e1b4cf..50300be55e1 100644 --- a/homeassistant/components/flunearyou/translations/fr.json +++ b/homeassistant/components/flunearyou/translations/fr.json @@ -12,6 +12,7 @@ "latitude": "Latitude", "longitude": "Longitude" }, + "description": "Surveillez les rapports des utilisateurs et du CDC pour des coordonn\u00e9es.", "title": "Configurer Flu Near You" } } diff --git a/homeassistant/components/plugwise/translations/fr.json b/homeassistant/components/plugwise/translations/fr.json index dddc95cfe0a..fe4b88ab0f1 100644 --- a/homeassistant/components/plugwise/translations/fr.json +++ b/homeassistant/components/plugwise/translations/fr.json @@ -16,7 +16,7 @@ "password": "ID Smile", "port": "Num\u00e9ro de port Smile" }, - "description": "D\u00e9tails", + "description": "Veuillez saisir :", "title": "Se connecter \u00e0 Smile" } } diff --git a/homeassistant/components/synology_dsm/translations/cs.json b/homeassistant/components/synology_dsm/translations/cs.json index 57dc028c0f4..d1f593c7938 100644 --- a/homeassistant/components/synology_dsm/translations/cs.json +++ b/homeassistant/components/synology_dsm/translations/cs.json @@ -17,5 +17,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "\u010casov\u00fd limit (v sekund\u00e1ch)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/cs.json b/homeassistant/components/unifi/translations/cs.json index a2adfcce308..c7e1ddc7136 100644 --- a/homeassistant/components/unifi/translations/cs.json +++ b/homeassistant/components/unifi/translations/cs.json @@ -49,7 +49,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Vytvo\u0159it senzory vyu\u017eit\u00ed \u0161\u00ed\u0159ky p\u00e1sma pro p\u0159ipojen\u00e9 klienty" + "allow_bandwidth_sensors": "Vytvo\u0159it senzory vyu\u017eit\u00ed \u0161\u00ed\u0159ky p\u00e1sma pro p\u0159ipojen\u00e9 klienty", + "allow_uptime_sensors": "Vytvo\u0159it senzory doby provozuschopnosti pro s\u00ed\u0165ov\u00e9 klienty" }, "description": "Konfigurovat statistick\u00e9 senzory", "title": "Mo\u017enosti UniFi 3/3" diff --git a/homeassistant/components/unifi/translations/fr.json b/homeassistant/components/unifi/translations/fr.json index 931b8c6c38c..03a872a8f96 100644 --- a/homeassistant/components/unifi/translations/fr.json +++ b/homeassistant/components/unifi/translations/fr.json @@ -60,7 +60,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Cr\u00e9er des capteurs d'utilisation de la bande passante pour les clients r\u00e9seau" + "allow_bandwidth_sensors": "Cr\u00e9er des capteurs d'utilisation de la bande passante pour les clients r\u00e9seau", + "allow_uptime_sensors": "Capteurs de disponibilit\u00e9 pour les clients r\u00e9seau" }, "description": "Configurer des capteurs de statistiques", "title": "Options UniFi 3/3" diff --git a/homeassistant/components/zodiac/translations/sensor.es.json b/homeassistant/components/zodiac/translations/sensor.es.json new file mode 100644 index 00000000000..fbd9d1bd653 --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.es.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Acuario", + "aries": "Aries", + "cancer": "C\u00e1ncer", + "capricorn": "Capricornio", + "gemini": "G\u00e9minis", + "leo": "Leo", + "libra": "Libra", + "pisces": "Piscis", + "sagittarius": "Sagitario", + "scorpio": "Escorpio", + "taurus": "Tauro", + "virgo": "Virgo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/sensor.et.json b/homeassistant/components/zodiac/translations/sensor.et.json new file mode 100644 index 00000000000..caf26a0130e --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.et.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Veevalaja", + "aries": "J\u00e4\u00e4r", + "cancer": "V\u00e4hk", + "capricorn": "Kaljukits", + "gemini": "Kaksikud", + "leo": "L\u00f5vi", + "libra": "Kaalud", + "pisces": "Kalad", + "sagittarius": "Ambur", + "scorpio": "Skorpion", + "taurus": "S\u00f5nn", + "virgo": "Neitsi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/sensor.fr.json b/homeassistant/components/zodiac/translations/sensor.fr.json new file mode 100644 index 00000000000..8c492c29f0b --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.fr.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Verseau", + "aries": "B\u00e9lier", + "cancer": "Cancer", + "capricorn": "Capricorne", + "gemini": "G\u00e9meaux", + "leo": "Lion", + "libra": "Balance", + "pisces": "Poissons", + "sagittarius": "Sagittaire", + "scorpio": "Scorpion", + "taurus": "Taureau", + "virgo": "Vierge" + } + } +} \ No newline at end of file From d694c1f5486b3d2e51f2335b24f2f926571da149 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 23 Sep 2020 19:26:05 -0500 Subject: [PATCH 324/514] Bump plexapi to 4.1.1 (#40512) --- 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 bbf7be9914e..29b0fe8038e 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.1.0", + "plexapi==4.1.1", "plexauth==0.0.5", "plexwebsocket==0.0.11" ], diff --git a/requirements_all.txt b/requirements_all.txt index 444e6e1e75b..f0d48213cd5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1101,7 +1101,7 @@ pillow==7.2.0 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.1.0 +plexapi==4.1.1 # homeassistant.components.plex plexauth==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c54a64509c..3021b881cf2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -518,7 +518,7 @@ pilight==0.1.1 pillow==7.2.0 # homeassistant.components.plex -plexapi==4.1.0 +plexapi==4.1.1 # homeassistant.components.plex plexauth==0.0.5 From 4b58b8057da67be7c91fe9d035b9ff433b54570d Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 23 Sep 2020 19:29:37 -0500 Subject: [PATCH 325/514] Update sonarr to 0.3.0 (#40515) --- homeassistant/components/sonarr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonarr/manifest.json b/homeassistant/components/sonarr/manifest.json index 3cd6e88913b..65146b90759 100644 --- a/homeassistant/components/sonarr/manifest.json +++ b/homeassistant/components/sonarr/manifest.json @@ -3,7 +3,7 @@ "name": "Sonarr", "documentation": "https://www.home-assistant.io/integrations/sonarr", "codeowners": ["@ctalkington"], - "requirements": ["sonarr==0.2.3"], + "requirements": ["sonarr==0.3.0"], "config_flow": true, "quality_scale": "silver" } diff --git a/requirements_all.txt b/requirements_all.txt index f0d48213cd5..cd63daf08e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2044,7 +2044,7 @@ somecomfort==0.5.2 somfy-mylink-synergy==1.0.6 # homeassistant.components.sonarr -sonarr==0.2.3 +sonarr==0.3.0 # homeassistant.components.marytts speak2mary==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3021b881cf2..4d684453389 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -948,7 +948,7 @@ solaredge==0.0.2 somecomfort==0.5.2 # homeassistant.components.sonarr -sonarr==0.2.3 +sonarr==0.3.0 # homeassistant.components.marytts speak2mary==1.4.0 From 7a337ac6fb15df08bcb20b9359d4aea0ea21b7e3 Mon Sep 17 00:00:00 2001 From: Eugene Prystupa Date: Wed, 23 Sep 2020 22:40:38 -0400 Subject: [PATCH 326/514] Fix Bond error logging format (#40519) --- homeassistant/components/bond/fan.py | 2 +- homeassistant/components/bond/light.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index 19e345b7e23..e59d0234beb 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -120,7 +120,7 @@ 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) + _LOGGER.debug("Fan async_turn_on called with speed %s", speed) if speed is not None: if speed == SPEED_OFF: diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index af308891a06..5e66019579d 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -122,7 +122,7 @@ class BondFireplace(BondEntity, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the fireplace on.""" - _LOGGER.debug("fireplace async_turn_on called with: %s", kwargs) + _LOGGER.debug("Fireplace async_turn_on called with: %s", kwargs) brightness = kwargs.get(ATTR_BRIGHTNESS) if brightness: @@ -133,7 +133,7 @@ class BondFireplace(BondEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the fireplace off.""" - _LOGGER.debug("fireplace async_turn_off called with: %s", kwargs) + _LOGGER.debug("Fireplace async_turn_off called with: %s", kwargs) await self._hub.bond.action(self._device.device_id, Action.turn_off()) From ebe3b5bfff30c219de06a49243a7f8ded2c0dce3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Sep 2020 14:10:14 +0200 Subject: [PATCH 327/514] Upgrade sentry-sdk to 0.17.8 (#40531) --- 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 6adb2e38f1c..83b9debd79c 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.7"], + "requirements": ["sentry-sdk==0.17.8"], "codeowners": ["@dcramer", "@frenck"] } diff --git a/requirements_all.txt b/requirements_all.txt index cd63daf08e5..5f5e82f43a8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1970,7 +1970,7 @@ sense-hat==2.2.0 sense_energy==0.8.0 # homeassistant.components.sentry -sentry-sdk==0.17.7 +sentry-sdk==0.17.8 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4d684453389..dd28f3496fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -918,7 +918,7 @@ samsungtvws==1.4.0 sense_energy==0.8.0 # homeassistant.components.sentry -sentry-sdk==0.17.7 +sentry-sdk==0.17.8 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From 8e0bb92c79cd2807c279bba81bd6363f863afcf4 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Thu, 24 Sep 2020 20:35:52 +0800 Subject: [PATCH 328/514] Disable audio in stream when audio stream profile is None (#40521) --- homeassistant/components/stream/worker.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 40231d87a53..9e036a764f8 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -77,6 +77,9 @@ def _stream_worker_internal(hass, stream, quit_event): # compatible with empty_moov and manual bitstream filters not in PyAV if container.format.name in {"hls", "mpegts"}: audio_stream = None + # Some audio streams do not have a profile and throw errors when remuxing + if audio_stream and audio_stream.profile is None: + 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 From e3f9818af5c53e243ba7d13672ccbf74297ee5f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 24 Sep 2020 17:32:44 +0300 Subject: [PATCH 329/514] Add more Huawei LTE sensor metadata (#39988) --- homeassistant/components/huawei_lte/sensor.py | 96 ++++++++++++++++++- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index f547dbd2eb6..cf05ef04c1a 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -10,7 +10,13 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_SIGNAL_STRENGTH, DOMAIN as SENSOR_DOMAIN, ) -from homeassistant.const import CONF_URL, DATA_BYTES, STATE_UNKNOWN, TIME_SECONDS +from homeassistant.const import ( + CONF_URL, + DATA_BYTES, + DATA_RATE_BYTES_PER_SECOND, + STATE_UNKNOWN, + TIME_SECONDS, +) from . import HuaweiLteBaseEntity from .const import ( @@ -41,7 +47,35 @@ SENSOR_META = { ), (KEY_DEVICE_SIGNAL, "band"): dict(name="Band"), (KEY_DEVICE_SIGNAL, "cell_id"): dict(name="Cell ID"), + (KEY_DEVICE_SIGNAL, "dl_mcs"): dict(name="Downlink MCS"), + (KEY_DEVICE_SIGNAL, "dlbandwidth"): dict( + name="Downlink bandwidth", + icon=lambda x: (x is None or x < 8) + and "mdi:speedometer-slow" + or x < 15 + and "mdi:speedometer-medium" + or "mdi:speedometer", + ), + (KEY_DEVICE_SIGNAL, "earfcn"): dict(name="EARFCN"), (KEY_DEVICE_SIGNAL, "lac"): dict(name="LAC", icon="mdi:map-marker"), + (KEY_DEVICE_SIGNAL, "plmn"): dict(name="PLMN"), + (KEY_DEVICE_SIGNAL, "rac"): dict(name="RAC", icon="mdi:map-marker"), + (KEY_DEVICE_SIGNAL, "rrc_status"): dict(name="RRC status"), + (KEY_DEVICE_SIGNAL, "tac"): dict(name="TAC", icon="mdi:map-marker"), + (KEY_DEVICE_SIGNAL, "tdd"): dict(name="TDD"), + (KEY_DEVICE_SIGNAL, "txpower"): dict( + name="Transmit power", + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + ), + (KEY_DEVICE_SIGNAL, "ul_mcs"): dict(name="Uplink MCS"), + (KEY_DEVICE_SIGNAL, "ulbandwidth"): dict( + name="Uplink bandwidth", + icon=lambda x: (x is None or x < 8) + and "mdi:speedometer-slow" + or x < 15 + and "mdi:speedometer-medium" + or "mdi:speedometer", + ), (KEY_DEVICE_SIGNAL, "mode"): dict( name="Mode", formatter=lambda x: ({"0": "2G", "2": "3G", "7": "4G"}.get(x, "Unknown"), None), @@ -161,9 +195,19 @@ SENSOR_META = { (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentDownload"): dict( name="Current connection download", unit=DATA_BYTES, icon="mdi:download" ), + (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentDownloadRate"): dict( + name="Current download rate", + unit=DATA_RATE_BYTES_PER_SECOND, + icon="mdi:download", + ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentUpload"): dict( name="Current connection upload", unit=DATA_BYTES, icon="mdi:upload" ), + (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentUploadRate"): dict( + name="Current upload rate", + unit=DATA_RATE_BYTES_PER_SECOND, + icon="mdi:upload", + ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalConnectTime"): dict( name="Total connected duration", unit=TIME_SECONDS, icon="mdi:timer-outline" ), @@ -173,7 +217,9 @@ SENSOR_META = { (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalUpload"): dict( name="Total upload", unit=DATA_BYTES, icon="mdi:upload" ), - KEY_NET_CURRENT_PLMN: dict(exclude=re.compile(r"^(Rat|ShortName)$", re.IGNORECASE)), + KEY_NET_CURRENT_PLMN: dict( + exclude=re.compile(r"^(Rat|ShortName|Spn)$", re.IGNORECASE) + ), (KEY_NET_CURRENT_PLMN, "State"): dict( name="Operator search mode", formatter=lambda x: ({"0": "Auto", "1": "Manual"}.get(x, "Unknown"), None), @@ -200,8 +246,52 @@ SENSOR_META = { None, ), ), + (KEY_SMS_SMS_COUNT, "LocalDeleted"): dict( + name="SMS deleted (device)", + icon="mdi:email-minus", + ), + (KEY_SMS_SMS_COUNT, "LocalDraft"): dict( + name="SMS drafts (device)", + icon="mdi:email-send-outline", + ), + (KEY_SMS_SMS_COUNT, "LocalInbox"): dict( + name="SMS inbox (device)", + icon="mdi:email", + ), + (KEY_SMS_SMS_COUNT, "LocalMax"): dict( + name="SMS capacity (device)", + icon="mdi:email", + ), + (KEY_SMS_SMS_COUNT, "LocalOutbox"): dict( + name="SMS outbox (device)", + icon="mdi:email-send", + ), (KEY_SMS_SMS_COUNT, "LocalUnread"): dict( - name="SMS unread", + name="SMS unread (device)", + icon="mdi:email-receive", + ), + (KEY_SMS_SMS_COUNT, "SimDraft"): dict( + name="SMS drafts (SIM)", + icon="mdi:email-send-outline", + ), + (KEY_SMS_SMS_COUNT, "SimInbox"): dict( + name="SMS inbox (SIM)", + icon="mdi:email", + ), + (KEY_SMS_SMS_COUNT, "SimMax"): dict( + name="SMS capacity (SIM)", + icon="mdi:email", + ), + (KEY_SMS_SMS_COUNT, "SimOutbox"): dict( + name="SMS outbox (SIM)", + icon="mdi:email-send", + ), + (KEY_SMS_SMS_COUNT, "SimUnread"): dict( + name="SMS unread (SIM)", + icon="mdi:email-receive", + ), + (KEY_SMS_SMS_COUNT, "SimUsed"): dict( + name="SMS messages (SIM)", icon="mdi:email-receive", ), } From 49be0730026ae99a89d611c0ce1d0034257dc15c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 24 Sep 2020 16:37:15 +0200 Subject: [PATCH 330/514] Update constant name for onewire (#40530) --- homeassistant/components/onewire/sensor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 4620d7593c9..7409be73712 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -25,7 +25,8 @@ _LOGGER = logging.getLogger(__name__) CONF_MOUNT_DIR = "mount_dir" CONF_NAMES = "names" -DEFAULT_MOUNT_DIR = "/sys/bus/w1/devices/" +DEFAULT_OWSERVER_PORT = 4304 +DEFAULT_SYSBUS_MOUNT_DIR = "/sys/bus/w1/devices/" DEVICE_SENSORS = { # Family : { SensorType: owfs path } "10": {"temperature": "temperature"}, @@ -92,9 +93,9 @@ SENSOR_TYPES = { PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAMES): {cv.string: cv.string}, - vol.Optional(CONF_MOUNT_DIR, default=DEFAULT_MOUNT_DIR): cv.string, + vol.Optional(CONF_MOUNT_DIR, default=DEFAULT_SYSBUS_MOUNT_DIR): cv.string, vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=4304): cv.port, + vol.Optional(CONF_PORT, default=DEFAULT_OWSERVER_PORT): cv.port, } ) @@ -168,7 +169,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) # We have a raw GPIO ow sensor on a Pi - elif base_dir == DEFAULT_MOUNT_DIR: + elif base_dir == DEFAULT_SYSBUS_MOUNT_DIR: for device_family in DEVICE_SENSORS: for device_folder in glob(os.path.join(base_dir, f"{device_family}[.-]*")): sensor_id = os.path.split(device_folder)[1] From 73f29a6cd435ec0f19dd20fc0f007c9f5502a288 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 24 Sep 2020 16:39:24 +0200 Subject: [PATCH 331/514] Ensure consitstency of file docstring for 1-wire (#40528) --- homeassistant/components/onewire/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index ac5d1393378..21dc0c2ead3 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -1 +1 @@ -"""The onewire component.""" +"""The 1-Wire component.""" From dc30f0e00cc585680aea346cb925309df37fd446 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 24 Sep 2020 16:39:42 +0200 Subject: [PATCH 332/514] Ensure the title is consistent (#40528) From 0b11559031e507a98893cbe8078f4ca76b375ac5 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 24 Sep 2020 17:57:52 +0200 Subject: [PATCH 333/514] Improve devolo Home Control code quality (#40480) Co-authored-by: Markus Bong <2Fake1987@gmail.com> --- .../devolo_home_control/binary_sensor.py | 47 ++----- .../components/devolo_home_control/climate.py | 32 +---- .../components/devolo_home_control/cover.py | 34 +---- .../devolo_home_control/devolo_device.py | 21 ++- .../devolo_multi_level_switch.py | 12 -- .../components/devolo_home_control/sensor.py | 126 ++++++++---------- .../components/devolo_home_control/switch.py | 85 ++---------- 7 files changed, 100 insertions(+), 257 deletions(-) diff --git a/homeassistant/components/devolo_home_control/binary_sensor.py b/homeassistant/components/devolo_home_control/binary_sensor.py index e05350dbbac..ab07df9f770 100644 --- a/homeassistant/components/devolo_home_control/binary_sensor.py +++ b/homeassistant/components/devolo_home_control/binary_sensor.py @@ -67,50 +67,35 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity): element_uid ) - self._device_class = DEVICE_CLASS_MAPPING.get( - self._binary_sensor_property.sub_type - or self._binary_sensor_property.sensor_type - ) - name = device_instance.item_name - - if self._device_class is None: - if device_instance.binary_sensor_property.get(element_uid).sub_type != "": - name += f" {device_instance.binary_sensor_property.get(element_uid).sub_type}" - else: - name += f" {device_instance.binary_sensor_property.get(element_uid).sensor_type}" - super().__init__( homecontrol=homecontrol, device_instance=device_instance, element_uid=element_uid, - name=name, - sync=self._sync, ) - self._state = self._binary_sensor_property.state + self._device_class = DEVICE_CLASS_MAPPING.get( + self._binary_sensor_property.sub_type + or self._binary_sensor_property.sensor_type + ) - self._subscriber = None + if self._device_class is None: + if device_instance.binary_sensor_property.get(element_uid).sub_type != "": + self._name += f" {device_instance.binary_sensor_property.get(element_uid).sub_type}" + else: + self._name += f" {device_instance.binary_sensor_property.get(element_uid).sensor_type}" + + self._value = self._binary_sensor_property.state @property def is_on(self): """Return the state.""" - return self._state + return self._value @property def device_class(self): """Return device class.""" return self._device_class - def _sync(self, message=None): - """Update the binary sensor state.""" - if message[0].startswith("devolo.BinarySensor"): - self._state = self._device_instance.binary_sensor_property[message[0]].state - 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() - class DevoloRemoteControl(DevoloDeviceEntity, BinarySensorEntity): """Representation of a remote control within devolo Home Control.""" @@ -120,26 +105,22 @@ class DevoloRemoteControl(DevoloDeviceEntity, BinarySensorEntity): 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): + def _sync(self, message): """Update the binary sensor state.""" if ( message[0] == self._remote_control_property.element_uid diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index 05f4363c384..297e431e63a 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -14,7 +14,7 @@ from homeassistant.const import PRECISION_HALVES from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN -from .devolo_device import DevoloDeviceEntity +from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity _LOGGER = logging.getLogger(__name__) @@ -42,29 +42,13 @@ async def async_setup_entry( async_add_entities(entities, False) -class DevoloClimateDeviceEntity(DevoloDeviceEntity, ClimateEntity): +class DevoloClimateDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, 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 + return self._value @property def hvac_mode(self) -> str: @@ -104,13 +88,3 @@ class DevoloClimateDeviceEntity(DevoloDeviceEntity, ClimateEntity): 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/cover.py b/homeassistant/components/devolo_home_control/cover.py index b93713cc700..21e555e3122 100644 --- a/homeassistant/components/devolo_home_control/cover.py +++ b/homeassistant/components/devolo_home_control/cover.py @@ -12,7 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN -from .devolo_device import DevoloDeviceEntity +from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity _LOGGER = logging.getLogger(__name__) @@ -37,29 +37,13 @@ async def async_setup_entry( async_add_entities(entities, False) -class DevoloCoverDeviceEntity(DevoloDeviceEntity, CoverEntity): +class DevoloCoverDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, 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.item_name, - 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 + return self._value @property def device_class(self): @@ -69,7 +53,7 @@ class DevoloCoverDeviceEntity(DevoloDeviceEntity, CoverEntity): @property def is_closed(self): """Return if the blind is closed or not.""" - return not bool(self._position) + return not bool(self._value) @property def supported_features(self): @@ -87,13 +71,3 @@ class DevoloCoverDeviceEntity(DevoloDeviceEntity, CoverEntity): 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/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py index 06ddf2175f7..4e47b19806a 100644 --- a/homeassistant/components/devolo_home_control/devolo_device.py +++ b/homeassistant/components/devolo_home_control/devolo_device.py @@ -10,14 +10,17 @@ _LOGGER = logging.getLogger(__name__) class DevoloDeviceEntity(Entity): - """Representation of a sensor within devolo Home Control.""" + """Abstract representation of a device within devolo Home Control.""" - def __init__(self, homecontrol, device_instance, element_uid, name, sync): + def __init__(self, homecontrol, device_instance, element_uid): """Initialize a devolo device entity.""" self._device_instance = device_instance - self._name = name self._unique_id = element_uid self._homecontrol = homecontrol + self._name = device_instance.item_name + self._device_class = None + self._value = None + self._unit = None # This is not doing I/O. It fetches an internal state of the API self._available = device_instance.is_online() @@ -27,7 +30,7 @@ class DevoloDeviceEntity(Entity): self._model = device_instance.name self.subscriber = None - self.sync_callback = sync + self.sync_callback = self._sync async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" @@ -73,3 +76,13 @@ class DevoloDeviceEntity(Entity): def available(self) -> bool: """Return the online state.""" return self._available + + def _sync(self, message): + """Update the binary sensor state.""" + if message[0] == self._unique_id: + self._value = message[1] + 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() 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 70629854dea..8056192340c 100644 --- a/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py +++ b/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py @@ -15,21 +15,9 @@ class DevoloMultiLevelSwitchDeviceEntity(DevoloDeviceEntity): 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[ element_uid ] self._value = self._multi_level_switch_property.value - - def _sync(self, message): - """Update the multi level switch state.""" - if message[0] == self._multi_level_switch_property.element_uid: - self._value = message[1] - 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() diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index 4bb2536dcc2..15e3ab47ef5 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -33,7 +33,7 @@ async def async_setup_entry( for device in hass.data[DOMAIN]["homecontrol"].multi_level_sensor_devices: for multi_level_sensor in device.multi_level_sensor_property: entities.append( - DevoloMultiLevelDeviceEntity( + DevoloGenericMultiLevelDeviceEntity( homecontrol=hass.data[DOMAIN]["homecontrol"], device_instance=device, element_uid=multi_level_sensor, @@ -55,44 +55,7 @@ async def async_setup_entry( class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity): - """Representation of a multi level sensor within devolo Home Control.""" - - def __init__( - self, - homecontrol, - device_instance, - element_uid, - multi_level_sensor_property=None, - sync=None, - ): - """Initialize a devolo multi level sensor.""" - 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 - - self._device_class = DEVICE_CLASS_MAPPING.get( - self._multi_level_sensor_property.sensor_type - ) - - name = device_instance.item_name - - if self._device_class is None: - name += f" {self._multi_level_sensor_property.sensor_type}" - - self._unit = self._multi_level_sensor_property.unit - - super().__init__( - homecontrol=homecontrol, - device_instance=device_instance, - element_uid=element_uid, - name=name, - sync=self._sync if sync is None else sync, - ) + """Abstract representation of a multi level sensor within devolo Home Control.""" @property def device_class(self) -> str: @@ -102,24 +65,43 @@ class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity): @property def state(self): """Return the state of the sensor.""" - return self._state + return self._value @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" return self._unit - def _sync(self, message=None): - """Update the multi level sensor state.""" - if message[0] == self._multi_level_sensor_property.element_uid: - self._state = self._device_instance.multi_level_sensor_property[ - message[0] - ].value - 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() + +class DevoloGenericMultiLevelDeviceEntity(DevoloMultiLevelDeviceEntity): + """Representation of a generic multi level sensor within devolo Home Control.""" + + def __init__( + self, + homecontrol, + device_instance, + element_uid, + ): + """Initialize a devolo multi level sensor.""" + self._multi_level_sensor_property = device_instance.multi_level_sensor_property[ + element_uid + ] + + super().__init__( + homecontrol=homecontrol, + device_instance=device_instance, + element_uid=element_uid, + ) + + self._device_class = DEVICE_CLASS_MAPPING.get( + self._multi_level_sensor_property.sensor_type + ) + + self._value = self._multi_level_sensor_property.value + self._unit = self._multi_level_sensor_property.unit + + if self._device_class is None: + self._name += f" {self._multi_level_sensor_property.sensor_type}" class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity): @@ -127,36 +109,36 @@ class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity): 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, + homecontrol=homecontrol, + device_instance=device_instance, + element_uid=element_uid, ) + self._sensor_type = consumption + self._device_class = DEVICE_CLASS_MAPPING.get(consumption) + + self._value = getattr( + device_instance.consumption_property[element_uid], consumption + ) + self._unit = getattr( + device_instance.consumption_property[element_uid], f"{consumption}_unit" + ) + + self._name += f" {consumption}" + @property def unique_id(self): """Return the unique ID of the entity.""" - return f"{self._unique_id}_{self.sensor_type}" + return f"{self._unique_id}_{self._sensor_type}" - def _sync(self, message=None): + def _sync(self, message): """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, + if message[0] == self._unique_id: + self._value = getattr( + self._device_instance.consumption_property[self._unique_id], + self._sensor_type, ) elif message[0].startswith("hdm"): self._available = self._device_instance.is_online() diff --git a/homeassistant/components/devolo_home_control/switch.py b/homeassistant/components/devolo_home_control/switch.py index 9a7812af7bd..d668cf3071a 100644 --- a/homeassistant/components/devolo_home_control/switch.py +++ b/homeassistant/components/devolo_home_control/switch.py @@ -6,6 +6,7 @@ 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__) @@ -32,26 +33,16 @@ async def async_setup_entry( async_add_entities(entities) -class DevoloSwitch(SwitchEntity): +class DevoloSwitch(DevoloDeviceEntity, SwitchEntity): """Representation of a switch.""" def __init__(self, homecontrol, device_instance, element_uid): """Initialize an devolo Switch.""" - self._device_instance = device_instance - - # Create the unique ID - self._unique_id = element_uid - - self._homecontrol = homecontrol - 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 - self._brand = self._device_instance.brand - self._model = self._device_instance.name - + super().__init__( + homecontrol=homecontrol, + device_instance=device_instance, + element_uid=element_uid, + ) self._binary_switch_property = self._device_instance.binary_switch_property.get( self._unique_id ) @@ -64,47 +55,6 @@ class DevoloSwitch(SwitchEntity): else: self._consumption = None - self.subscriber = None - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.subscriber = Subscriber( - self._device_instance.item_name, callback=self.sync - ) - self._homecontrol.publisher.register( - self._device_instance.uid, self.subscriber, self.sync - ) - - @property - def unique_id(self): - """Return the unique ID of the switch.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info.""" - return { - "identifiers": {(DOMAIN, self._device_instance.uid)}, - "name": self.name, - "manufacturer": self._brand, - "model": self._model, - } - - @property - def device_id(self): - """Return the ID of this switch.""" - return self._unique_id - - @property - def name(self): - """Return the display name of this switch.""" - return self._name - - @property - def should_poll(self): - """Return the polling state.""" - return False - @property def is_on(self): """Return the state.""" @@ -115,11 +65,6 @@ class DevoloSwitch(SwitchEntity): """Return the current consumption.""" return self._consumption - @property - def available(self): - """Return the online state.""" - return self._available - def turn_on(self, **kwargs): """Switch on the device.""" self._is_on = True @@ -130,7 +75,7 @@ class DevoloSwitch(SwitchEntity): self._is_on = False self._binary_switch_property.set(state=False) - def sync(self, message=None): + def _sync(self, message): """Update the binary switch state and consumption.""" if message[0].startswith("devolo.BinarySwitch"): self._is_on = self._device_instance.binary_switch_property[message[0]].state @@ -143,17 +88,3 @@ class DevoloSwitch(SwitchEntity): else: _LOGGER.debug("No valid message received: %s", message) self.schedule_update_ha_state() - - -class Subscriber: - """Subscriber class for the publisher in mprm websocket class.""" - - def __init__(self, name, callback): - """Initiate the device.""" - self.name = name - self.callback = callback - - def update(self, message): - """Trigger hass to update the device.""" - _LOGGER.debug('%s got message "%s"', self.name, message) - self.callback(message) From c3b6675617aafdbc728b7e78b50f5be3e85c6aeb Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Thu, 24 Sep 2020 18:27:55 +0200 Subject: [PATCH 334/514] Increase upnp timeout from 5 seconds to 10 seconds (#40540) --- homeassistant/components/upnp/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index c4a81db1ff4..5f29043a1fe 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -67,7 +67,7 @@ class Device: """Create UPnP/IGD device.""" # build async_upnp_client requester session = async_get_clientsession(hass) - requester = AiohttpSessionRequester(session, True) + requester = AiohttpSessionRequester(session, True, 10) # create async_upnp_client device factory = UpnpFactory(requester, disable_state_variable_validation=True) From e06f2a89ea2b28758151cc5cc3b710dc3157f8db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 24 Sep 2020 20:18:55 +0300 Subject: [PATCH 335/514] Add Huawei LTE SMS storage full and unread sensors (#40021) --- .../components/huawei_lte/__init__.py | 5 +++ .../components/huawei_lte/binary_sensor.py | 39 ++++++++++++++++++- homeassistant/components/huawei_lte/const.py | 8 +++- homeassistant/components/huawei_lte/sensor.py | 10 +++++ 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 29db6026cc4..c0db281d768 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -63,6 +63,7 @@ from .const import ( KEY_DEVICE_INFORMATION, KEY_DEVICE_SIGNAL, KEY_DIALUP_MOBILE_DATASWITCH, + KEY_MONITORING_CHECK_NOTIFICATIONS, KEY_MONITORING_MONTH_STATISTICS, KEY_MONITORING_STATUS, KEY_MONITORING_TRAFFIC_STATISTICS, @@ -243,6 +244,10 @@ class Router: self._get_data( KEY_MONITORING_MONTH_STATISTICS, self.client.monitoring.month_statistics ) + self._get_data( + KEY_MONITORING_CHECK_NOTIFICATIONS, + self.client.monitoring.check_notifications, + ) self._get_data(KEY_MONITORING_STATUS, self.client.monitoring.status) self._get_data( KEY_MONITORING_TRAFFIC_STATISTICS, self.client.monitoring.traffic_statistics diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py index 575cc9789ca..2b55245719b 100644 --- a/homeassistant/components/huawei_lte/binary_sensor.py +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -13,7 +13,12 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import CONF_URL from . import HuaweiLteBaseEntity -from .const import DOMAIN, KEY_MONITORING_STATUS, KEY_WLAN_WIFI_FEATURE_SWITCH +from .const import ( + DOMAIN, + KEY_MONITORING_CHECK_NOTIFICATIONS, + KEY_MONITORING_STATUS, + KEY_WLAN_WIFI_FEATURE_SWITCH, +) _LOGGER = logging.getLogger(__name__) @@ -29,6 +34,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append(HuaweiLteWifi24ghzStatusBinarySensor(router)) entities.append(HuaweiLteWifi5ghzStatusBinarySensor(router)) + if router.data.get(KEY_MONITORING_CHECK_NOTIFICATIONS): + entities.append(HuaweiLteSmsStorageFullBinarySensor(router)) + async_add_entities(entities, True) @@ -194,3 +202,32 @@ class HuaweiLteWifi5ghzStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor): @property def _entity_name(self) -> str: return "5GHz WiFi status" + + +@attr.s +class HuaweiLteSmsStorageFullBinarySensor(HuaweiLteBaseBinarySensor): + """Huawei LTE SMS storage full binary sensor.""" + + def __attrs_post_init__(self): + """Initialize identifiers.""" + self.key = KEY_MONITORING_CHECK_NOTIFICATIONS + self.item = "SmsStorageFull" + + @property + def _entity_name(self) -> str: + return "SMS storage full" + + @property + def is_on(self) -> bool: + """Return whether the binary sensor is on.""" + return self._raw_state is not None and int(self._raw_state) != 0 + + @property + def assumed_state(self) -> bool: + """Return True if real state is assumed, not known.""" + return self._raw_state is None + + @property + def icon(self): + """Return WiFi status sensor icon.""" + return "mdi:email-alert" if self.is_on else "mdi:email-off" diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 2c5a3f8a9f6..039bab10fb9 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -27,6 +27,7 @@ KEY_DEVICE_BASIC_INFORMATION = "device_basic_information" KEY_DEVICE_INFORMATION = "device_information" KEY_DEVICE_SIGNAL = "device_signal" KEY_DIALUP_MOBILE_DATASWITCH = "dialup_mobile_dataswitch" +KEY_MONITORING_CHECK_NOTIFICATIONS = "monitoring_check_notifications" KEY_MONITORING_MONTH_STATISTICS = "monitoring_month_statistics" KEY_MONITORING_STATUS = "monitoring_status" KEY_MONITORING_TRAFFIC_STATISTICS = "monitoring_traffic_statistics" @@ -36,13 +37,18 @@ KEY_SMS_SMS_COUNT = "sms_sms_count" KEY_WLAN_HOST_LIST = "wlan_host_list" KEY_WLAN_WIFI_FEATURE_SWITCH = "wlan_wifi_feature_switch" -BINARY_SENSOR_KEYS = {KEY_MONITORING_STATUS, KEY_WLAN_WIFI_FEATURE_SWITCH} +BINARY_SENSOR_KEYS = { + KEY_MONITORING_CHECK_NOTIFICATIONS, + KEY_MONITORING_STATUS, + KEY_WLAN_WIFI_FEATURE_SWITCH, +} DEVICE_TRACKER_KEYS = {KEY_WLAN_HOST_LIST} SENSOR_KEYS = { KEY_DEVICE_INFORMATION, KEY_DEVICE_SIGNAL, + KEY_MONITORING_CHECK_NOTIFICATIONS, KEY_MONITORING_MONTH_STATISTICS, KEY_MONITORING_STATUS, KEY_MONITORING_TRAFFIC_STATISTICS, diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index cf05ef04c1a..ccdeb47ee88 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -23,6 +23,7 @@ from .const import ( DOMAIN, KEY_DEVICE_INFORMATION, KEY_DEVICE_SIGNAL, + KEY_MONITORING_CHECK_NOTIFICATIONS, KEY_MONITORING_MONTH_STATISTICS, KEY_MONITORING_STATUS, KEY_MONITORING_TRAFFIC_STATISTICS, @@ -157,6 +158,15 @@ SENSOR_META = { and "mdi:signal-cellular-2" or "mdi:signal-cellular-3", ), + KEY_MONITORING_CHECK_NOTIFICATIONS: dict( + exclude=re.compile( + r"^(onlineupdatestatus|smsstoragefull)$", + re.IGNORECASE, + ) + ), + (KEY_MONITORING_CHECK_NOTIFICATIONS, "UnreadMessage"): dict( + name="SMS unread", icon="mdi:email-receive" + ), KEY_MONITORING_MONTH_STATISTICS: dict( exclude=re.compile(r"^month(duration|lastcleartime)$", re.IGNORECASE) ), From 0a656f13eb665fddd11c7049a43ab4f0cedb911d Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Thu, 24 Sep 2020 12:37:34 -0700 Subject: [PATCH 336/514] Fix/Refactor Hyperion Integration (#39738) --- CODEOWNERS | 1 + homeassistant/components/hyperion/light.py | 479 ++++++++++-------- .../components/hyperion/manifest.json | 3 +- requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/hyperion/__init__.py | 1 + tests/components/hyperion/test_light.py | 430 ++++++++++++++++ 7 files changed, 711 insertions(+), 209 deletions(-) create mode 100644 tests/components/hyperion/__init__.py create mode 100644 tests/components/hyperion/test_light.py diff --git a/CODEOWNERS b/CODEOWNERS index 3fa8a7a366d..40584db7479 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -193,6 +193,7 @@ homeassistant/components/humidifier/* @home-assistant/core @Shulyaka homeassistant/components/hunterdouglas_powerview/* @bdraco homeassistant/components/hvv_departures/* @vigonotion homeassistant/components/hydrawise/* @ptcryan +homeassistant/components/hyperion/* @dermotduffy homeassistant/components/iammeter/* @lewei50 homeassistant/components/iaqualink/* @flz homeassistant/components/icloud/* @Quentame diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index d1baec315bf..db34a21dada 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -1,8 +1,7 @@ -"""Support for Hyperion remotes.""" -import json +"""Support for Hyperion-NG remotes.""" import logging -import socket +from hyperion import client, const import voluptuous as vol from homeassistant.components.light import ( @@ -16,6 +15,7 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -26,103 +26,91 @@ CONF_PRIORITY = "priority" CONF_HDMI_PRIORITY = "hdmi_priority" CONF_EFFECT_LIST = "effect_list" +# As we want to preserve brightness control for effects (e.g. to reduce the +# brightness for V4L), we need to persist the effect that is in flight, so +# subsequent calls to turn_on will know the keep the effect enabled. +# Unfortunately the Home Assistant UI does not easily expose a way to remove a +# selected effect (there is no 'No Effect' option by default). Instead, we +# create a new fake effect ("Solid") that is always selected by default for +# showing a solid color. This is the same method used by WLED. +KEY_EFFECT_SOLID = "Solid" + DEFAULT_COLOR = [255, 255, 255] +DEFAULT_BRIGHTNESS = 255 +DEFAULT_EFFECT = KEY_EFFECT_SOLID DEFAULT_NAME = "Hyperion" +DEFAULT_ORIGIN = "Home Assistant" DEFAULT_PORT = 19444 DEFAULT_PRIORITY = 128 DEFAULT_HDMI_PRIORITY = 880 -DEFAULT_EFFECT_LIST = [ - "HDMI", - "Cinema brighten lights", - "Cinema dim lights", - "Knight rider", - "Blue mood blobs", - "Cold mood blobs", - "Full color mood blobs", - "Green mood blobs", - "Red mood blobs", - "Warm mood blobs", - "Police Lights Single", - "Police Lights Solid", - "Rainbow mood", - "Rainbow swirl fast", - "Rainbow swirl", - "Random", - "Running dots", - "System Shutdown", - "Snake", - "Sparks Color", - "Sparks", - "Strobe blue", - "Strobe Raspbmc", - "Strobe white", - "Color traces", - "UDP multicast listener", - "UDP listener", - "X-Mas", -] +DEFAULT_EFFECT_LIST = [] SUPPORT_HYPERION = SUPPORT_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_DEFAULT_COLOR, default=DEFAULT_COLOR): vol.All( - list, - vol.Length(min=3, max=3), - [vol.All(vol.Coerce(int), vol.Range(min=0, max=255))], - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PRIORITY, default=DEFAULT_PRIORITY): cv.positive_int, - vol.Optional( - CONF_HDMI_PRIORITY, default=DEFAULT_HDMI_PRIORITY - ): cv.positive_int, - vol.Optional(CONF_EFFECT_LIST, default=DEFAULT_EFFECT_LIST): vol.All( - cv.ensure_list, [cv.string] - ), - } +PLATFORM_SCHEMA = vol.All( + cv.deprecated(CONF_HDMI_PRIORITY, invalidation_version="0.118"), + cv.deprecated(CONF_DEFAULT_COLOR, invalidation_version="0.118"), + cv.deprecated(CONF_EFFECT_LIST, invalidation_version="0.118"), + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_DEFAULT_COLOR, default=DEFAULT_COLOR): vol.All( + list, + vol.Length(min=3, max=3), + [vol.All(vol.Coerce(int), vol.Range(min=0, max=255))], + ), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PRIORITY, default=DEFAULT_PRIORITY): cv.positive_int, + vol.Optional( + CONF_HDMI_PRIORITY, default=DEFAULT_HDMI_PRIORITY + ): cv.positive_int, + vol.Optional(CONF_EFFECT_LIST, default=DEFAULT_EFFECT_LIST): vol.All( + cv.ensure_list, [cv.string] + ), + } + ), ) +ICON_LIGHTBULB = "mdi:lightbulb" +ICON_EFFECT = "mdi:lava-lamp" +ICON_EXTERNAL_SOURCE = "mdi:video-input-hdmi" -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 a Hyperion server remote.""" name = config[CONF_NAME] host = config[CONF_HOST] port = config[CONF_PORT] priority = config[CONF_PRIORITY] - hdmi_priority = config[CONF_HDMI_PRIORITY] - default_color = config[CONF_DEFAULT_COLOR] - effect_list = config[CONF_EFFECT_LIST] - device = Hyperion( - name, host, port, priority, default_color, hdmi_priority, effect_list - ) + hyperion_client = client.HyperionClient(host, port) - if device.setup(): - add_entities([device]) + if not await hyperion_client.async_client_connect(): + raise PlatformNotReady + + async_add_entities([Hyperion(name, priority, hyperion_client)]) class Hyperion(LightEntity): """Representation of a Hyperion remote.""" - def __init__( - self, name, host, port, priority, default_color, hdmi_priority, effect_list - ): + def __init__(self, name, priority, hyperion_client): """Initialize the light.""" - self._host = host - self._port = port self._name = name self._priority = priority - self._hdmi_priority = hdmi_priority - self._default_color = default_color - self._rgb_color = [0, 0, 0] - self._rgb_mem = [0, 0, 0] - self._brightness = 255 - self._icon = "mdi:lightbulb" - self._effect_list = effect_list - self._effect = None - self._skip_update = False + self._client = hyperion_client + + # Active state representing the Hyperion instance. + self._set_internal_state( + brightness=255, rgb_color=DEFAULT_COLOR, effect=KEY_EFFECT_SOLID + ) + self._effect_list = [] + + @property + def should_poll(self): + """Return whether or not this entity should be polled.""" + return False @property def name(self): @@ -142,7 +130,7 @@ class Hyperion(LightEntity): @property def is_on(self): """Return true if not black.""" - return self._rgb_color != [0, 0, 0] + return self._client.is_on() @property def icon(self): @@ -157,158 +145,233 @@ class Hyperion(LightEntity): @property def effect_list(self): """Return the list of supported effects.""" - return self._effect_list + return ( + self._effect_list + + const.KEY_COMPONENTID_EXTERNAL_SOURCES + + [KEY_EFFECT_SOLID] + ) @property def supported_features(self): """Flag supported features.""" return SUPPORT_HYPERION - def turn_on(self, **kwargs): + @property + def available(self): + """Return server availability.""" + return self._client.has_loaded_state + + @property + def unique_id(self): + """Return a unique id for this instance.""" + return self._client.id + + async def async_turn_on(self, **kwargs): """Turn the lights on.""" + # == Turn device on == + # Turn on both ALL (Hyperion itself) and LEDDEVICE. It would be + # preferable to enable LEDDEVICE after the settings (e.g. brightness, + # color, effect), but this is not possible due to: + # https://github.com/hyperion-project/hyperion.ng/issues/967 + if not self.is_on: + if not await self._client.async_send_set_component( + **{ + const.KEY_COMPONENTSTATE: { + const.KEY_COMPONENT: const.KEY_COMPONENTID_ALL, + const.KEY_STATE: True, + } + } + ): + return + + if not await self._client.async_send_set_component( + **{ + const.KEY_COMPONENTSTATE: { + const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE, + const.KEY_STATE: True, + } + } + ): + return + + # == Get key parameters == + brightness = kwargs.get(ATTR_BRIGHTNESS, self._brightness) + effect = kwargs.get(ATTR_EFFECT, self._effect) if ATTR_HS_COLOR in kwargs: rgb_color = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) - elif self._rgb_mem == [0, 0, 0]: - rgb_color = self._default_color else: - rgb_color = self._rgb_mem + rgb_color = self._rgb_color - brightness = kwargs.get(ATTR_BRIGHTNESS, self._brightness) - - if ATTR_EFFECT in kwargs: - self._skip_update = True - self._effect = kwargs[ATTR_EFFECT] - if self._effect == "HDMI": - self.json_request({"command": "clearall"}) - self._icon = "mdi:video-input-hdmi" - self._brightness = 255 - self._rgb_color = [125, 125, 125] - else: - self.json_request( - { - "command": "effect", - "priority": self._priority, - "effect": {"name": self._effect}, + # == Set brightness == + if self._brightness != brightness: + if not await self._client.async_send_set_adjustment( + **{ + const.KEY_ADJUSTMENT: { + const.KEY_BRIGHTNESS: int( + round((float(brightness) * 100) / 255) + ) } - ) - self._icon = "mdi:lava-lamp" - self._rgb_color = [175, 0, 255] - return - - cal_color = [int(round(x * float(brightness) / 255)) for x in rgb_color] - self.json_request( - {"command": "color", "priority": self._priority, "color": cal_color} - ) - - def turn_off(self, **kwargs): - """Disconnect all remotes.""" - self.json_request({"command": "clearall"}) - self.json_request( - {"command": "color", "priority": self._priority, "color": [0, 0, 0]} - ) - - def update(self): - """Get the lights status.""" - # postpone the immediate state check for changes that take time - if self._skip_update: - self._skip_update = False - return - response = self.json_request({"command": "serverinfo"}) - if response: - # workaround for outdated Hyperion - if "activeLedColor" not in response["info"]: - self._rgb_color = self._default_color - self._rgb_mem = self._default_color - self._brightness = 255 - self._icon = "mdi:lightbulb" - self._effect = None + } + ): return - # Check if Hyperion is in ambilight mode trough an HDMI grabber - try: - active_priority = response["info"]["priorities"][0]["priority"] - if active_priority == self._hdmi_priority: - self._brightness = 255 - self._rgb_color = [125, 125, 125] - self._icon = "mdi:video-input-hdmi" - self._effect = "HDMI" + + # == Set an external source + if effect and effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES: + + # Clear any color/effect. + if not await self._client.async_send_clear( + **{const.KEY_PRIORITY: self._priority} + ): + return + + # Turn off all external sources, except the intended. + for key in const.KEY_COMPONENTID_EXTERNAL_SOURCES: + if not await self._client.async_send_set_component( + **{ + const.KEY_COMPONENTSTATE: { + const.KEY_COMPONENT: key, + const.KEY_STATE: effect == key, + } + } + ): return - except (KeyError, IndexError): - pass - led_color = response["info"]["activeLedColor"] - if not led_color or led_color[0]["RGB Value"] == [0, 0, 0]: - # Get the active effect - if response["info"].get("activeEffects"): - self._rgb_color = [175, 0, 255] - self._icon = "mdi:lava-lamp" - try: - s_name = response["info"]["activeEffects"][0]["script"] - s_name = s_name.split("/")[-1][:-3].split("-")[0] - self._effect = [ - x for x in self._effect_list if s_name.lower() in x.lower() - ][0] - except (KeyError, IndexError): - self._effect = None - # Bulb off state - else: - self._rgb_color = [0, 0, 0] - self._icon = "mdi:lightbulb" - self._effect = None + # == Set an effect + elif effect and effect != KEY_EFFECT_SOLID: + # This call should not be necessary, but without it there is no priorities-update issued: + # https://github.com/hyperion-project/hyperion.ng/issues/992 + if not await self._client.async_send_clear( + **{const.KEY_PRIORITY: self._priority} + ): + return + + if not await self._client.async_send_set_effect( + **{ + const.KEY_PRIORITY: self._priority, + const.KEY_EFFECT: {const.KEY_NAME: effect}, + const.KEY_ORIGIN: DEFAULT_ORIGIN, + } + ): + return + # == Set a color + else: + if not await self._client.async_send_set_color( + **{ + const.KEY_PRIORITY: self._priority, + const.KEY_COLOR: rgb_color, + const.KEY_ORIGIN: DEFAULT_ORIGIN, + } + ): + return + + async def async_turn_off(self, **kwargs): + """Disable the LED output component.""" + if not await self._client.async_send_set_component( + **{ + const.KEY_COMPONENTSTATE: { + const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE, + const.KEY_STATE: False, + } + } + ): + return + + def _set_internal_state(self, brightness=None, rgb_color=None, effect=None): + """Set the internal state.""" + if brightness is not None: + self._brightness = brightness + if rgb_color is not None: + self._rgb_color = rgb_color + if effect is not None: + self._effect = effect + if effect == KEY_EFFECT_SOLID: + self._icon = ICON_LIGHTBULB + elif effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES: + self._icon = ICON_EXTERNAL_SOURCE else: - # Get the RGB color - self._rgb_color = led_color[0]["RGB Value"] - self._brightness = max(self._rgb_color) - self._rgb_mem = [ - int(round(float(x) * 255 / self._brightness)) - for x in self._rgb_color - ] - self._icon = "mdi:lightbulb" - self._effect = None + self._icon = ICON_EFFECT - def setup(self): - """Get the hostname of the remote.""" - response = self.json_request({"command": "serverinfo"}) - if response: - if self._name == self._host: - self._name = response["info"]["hostname"] - return True - return False + def _update_components(self, _=None): + """Update Hyperion components.""" + self.async_write_ha_state() - def json_request(self, request, wait_for_response=False): - """Communicate with the JSON server.""" - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(5) + def _update_adjustment(self, _=None): + """Update Hyperion adjustments.""" + if self._client.adjustment: + brightness_pct = self._client.adjustment[0].get( + const.KEY_BRIGHTNESS, DEFAULT_BRIGHTNESS + ) + if brightness_pct < 0 or brightness_pct > 100: + return + self._set_internal_state( + brightness=int(round((brightness_pct * 255) / float(100))) + ) + self.async_write_ha_state() - try: - sock.connect((self._host, self._port)) - except OSError: - sock.close() - return False + def _update_priorities(self, _=None): + """Update Hyperion priorities.""" + visible_priority = self._client.visible_priority + if visible_priority: + componentid = visible_priority.get(const.KEY_COMPONENTID) + if componentid in const.KEY_COMPONENTID_EXTERNAL_SOURCES: + self._set_internal_state(rgb_color=DEFAULT_COLOR, effect=componentid) + elif componentid == const.KEY_COMPONENTID_EFFECT: + # Owner is the effect name. + # See: https://docs.hyperion-project.org/en/json/ServerInfo.html#priorities + self._set_internal_state( + rgb_color=DEFAULT_COLOR, effect=visible_priority[const.KEY_OWNER] + ) + elif componentid == const.KEY_COMPONENTID_COLOR: + self._set_internal_state( + rgb_color=visible_priority[const.KEY_VALUE][const.KEY_RGB], + effect=KEY_EFFECT_SOLID, + ) + self.async_write_ha_state() - sock.send(bytearray(f"{json.dumps(request)}\n", "utf-8")) - try: - buf = sock.recv(4096) - except socket.timeout: - # Something is wrong, assume it's offline - sock.close() - return False + def _update_effect_list(self, _=None): + """Update Hyperion effects.""" + if not self._client.effects: + return + effect_list = [] + for effect in self._client.effects or []: + if const.KEY_NAME in effect: + effect_list.append(effect[const.KEY_NAME]) + if effect_list: + self._effect_list = effect_list + self.async_write_ha_state() - # Read until a newline or timeout - buffering = True - while buffering: - if "\n" in str(buf, "utf-8"): - response = str(buf, "utf-8").split("\n")[0] - buffering = False - else: - try: - more = sock.recv(4096) - except socket.timeout: - more = None - if not more: - buffering = False - response = str(buf, "utf-8") - else: - buf += more + def _update_full_state(self): + """Update full Hyperion state.""" + self._update_adjustment() + self._update_priorities() + self._update_effect_list() - sock.close() - return json.loads(response) + _LOGGER.debug( + "Hyperion full state update: On=%s,Brightness=%i,Effect=%s " + "(%i effects total),Color=%s", + self.is_on, + self._brightness, + self._effect, + len(self._effect_list), + self._rgb_color, + ) + + def _update_client(self, json): + """Update client connection state.""" + self.async_write_ha_state() + + async def async_added_to_hass(self): + """Register callbacks when entity added to hass.""" + self._client.set_callbacks( + { + f"{const.KEY_ADJUSTMENT}-{const.KEY_UPDATE}": self._update_adjustment, + f"{const.KEY_COMPONENTS}-{const.KEY_UPDATE}": self._update_components, + f"{const.KEY_EFFECTS}-{const.KEY_UPDATE}": self._update_effect_list, + f"{const.KEY_PRIORITIES}-{const.KEY_UPDATE}": self._update_priorities, + f"{const.KEY_CLIENT}-{const.KEY_UPDATE}": self._update_client, + } + ) + + # Load initial state. + self._update_full_state() + return True diff --git a/homeassistant/components/hyperion/manifest.json b/homeassistant/components/hyperion/manifest.json index 6d9d0ae4d9d..4a9bf2ada8c 100644 --- a/homeassistant/components/hyperion/manifest.json +++ b/homeassistant/components/hyperion/manifest.json @@ -2,5 +2,6 @@ "domain": "hyperion", "name": "Hyperion", "documentation": "https://www.home-assistant.io/integrations/hyperion", - "codeowners": [] + "requirements": ["hyperion-py==0.3.0"], + "codeowners": ["@dermotduffy"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5f5e82f43a8..6b7a8a7feee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -774,6 +774,9 @@ huawei-lte-api==1.4.12 # homeassistant.components.hydrawise hydrawiser==0.2 +# homeassistant.components.hyperion +hyperion-py==0.3.0 + # homeassistant.components.bh1750 # homeassistant.components.bme280 # homeassistant.components.htu21d diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd28f3496fd..2846c38d69c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -391,6 +391,9 @@ httplib2==0.10.3 # homeassistant.components.huawei_lte huawei-lte-api==1.4.12 +# homeassistant.components.hyperion +hyperion-py==0.3.0 + # homeassistant.components.iaqualink iaqualink==0.3.4 diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py new file mode 100644 index 00000000000..e4c1ee67efa --- /dev/null +++ b/tests/components/hyperion/__init__.py @@ -0,0 +1 @@ +"""Tests for the Hyperion component.""" diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py new file mode 100644 index 00000000000..c400e34db51 --- /dev/null +++ b/tests/components/hyperion/test_light.py @@ -0,0 +1,430 @@ +"""Tests for the Hyperion integration.""" +# from tests.async_mock import AsyncMock, MagicMock, patch +from asynctest import CoroutineMock, Mock, call, patch +from hyperion import const + +from homeassistant.components.hyperion import light as hyperion_light +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_EFFECT, + ATTR_HS_COLOR, + DOMAIN, +) +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.setup import async_setup_component + +TEST_HOST = "test-hyperion-host" +TEST_PORT = const.DEFAULT_PORT +TEST_NAME = "test_hyperion_name" +TEST_PRIORITY = 128 +TEST_ENTITY_ID = f"{DOMAIN}.{TEST_NAME}" + + +def create_mock_client(): + """Create a mock Hyperion client.""" + mock_client = Mock() + mock_client.async_client_connect = CoroutineMock(return_value=True) + mock_client.adjustment = None + mock_client.effects = None + mock_client.id = "%s:%i" % (TEST_HOST, TEST_PORT) + return mock_client + + +def call_registered_callback(client, key, *args, **kwargs): + """Call a Hyperion entity callback that was registered with the client.""" + return client.set_callbacks.call_args[0][0][key](*args, **kwargs) + + +async def setup_entity(hass, client=None): + """Add a test Hyperion entity to hass.""" + client = client or create_mock_client() + with patch("hyperion.client.HyperionClient", return_value=client): + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "platform": "hyperion", + "name": TEST_NAME, + "host": TEST_HOST, + "port": const.DEFAULT_PORT, + "priority": TEST_PRIORITY, + } + }, + ) + await hass.async_block_till_done() + + +async def test_setup_platform(hass): + """Test setting up the platform.""" + client = create_mock_client() + await setup_entity(hass, client=client) + assert hass.states.get(TEST_ENTITY_ID) is not None + + +async def test_setup_platform_not_ready(hass): + """Test the platform not being ready.""" + client = create_mock_client() + client.async_client_connect = CoroutineMock(return_value=False) + + await setup_entity(hass, client=client) + assert hass.states.get(TEST_ENTITY_ID) is None + + +async def test_light_basic_properies(hass): + """Test the basic properties.""" + client = create_mock_client() + await setup_entity(hass, client=client) + + entity_state = hass.states.get(TEST_ENTITY_ID) + assert entity_state.state == "on" + assert entity_state.attributes["brightness"] == 255 + assert entity_state.attributes["hs_color"] == (0.0, 0.0) + assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB + assert entity_state.attributes["effect"] == hyperion_light.KEY_EFFECT_SOLID + + # By default the effect list is the 3 external sources + 'Solid'. + assert len(entity_state.attributes["effect_list"]) == 4 + + assert ( + entity_state.attributes["supported_features"] == hyperion_light.SUPPORT_HYPERION + ) + + +async def test_light_async_turn_on(hass): + """Test turning the light on.""" + client = create_mock_client() + await setup_entity(hass, client=client) + + # On (=), 100% (=), solid (=), [255,255,255] (=) + client.async_send_set_color = CoroutineMock(return_value=True) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True + ) + + assert client.async_send_set_color.call_args == call( + **{ + const.KEY_PRIORITY: TEST_PRIORITY, + const.KEY_COLOR: [255, 255, 255], + const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN, + } + ) + + # On (=), 50% (!), solid (=), [255,255,255] (=) + # === + brightness = 128 + client.async_send_set_color = CoroutineMock(return_value=True) + client.async_send_set_adjustment = CoroutineMock(return_value=True) + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_BRIGHTNESS: brightness}, + blocking=True, + ) + + assert client.async_send_set_adjustment.call_args == call( + **{const.KEY_ADJUSTMENT: {const.KEY_BRIGHTNESS: 50}} + ) + assert client.async_send_set_color.call_args == call( + **{ + const.KEY_PRIORITY: TEST_PRIORITY, + const.KEY_COLOR: [255, 255, 255], + const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN, + } + ) + + # Simulate a state callback from Hyperion. + client.adjustment = [{const.KEY_BRIGHTNESS: 50}] + call_registered_callback(client, "adjustment-update") + entity_state = hass.states.get(TEST_ENTITY_ID) + assert entity_state.state == "on" + assert entity_state.attributes["brightness"] == brightness + + # On (=), 50% (=), solid (=), [0,255,255] (!) + hs_color = (180.0, 100.0) + client.async_send_set_color = CoroutineMock(return_value=True) + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_HS_COLOR: hs_color}, + blocking=True, + ) + + assert client.async_send_set_color.call_args == call( + **{ + const.KEY_PRIORITY: TEST_PRIORITY, + const.KEY_COLOR: (0, 255, 255), + const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN, + } + ) + + # Simulate a state callback from Hyperion. + client.visible_priority = { + const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR, + const.KEY_VALUE: {const.KEY_RGB: (0, 255, 255)}, + } + + call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_ENTITY_ID) + assert entity_state.attributes["hs_color"] == hs_color + assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB + + # On (=), 100% (!), solid, [0,255,255] (=) + brightness = 255 + client.async_send_set_color = CoroutineMock(return_value=True) + client.async_send_set_adjustment = CoroutineMock(return_value=True) + + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_BRIGHTNESS: brightness}, + blocking=True, + ) + + assert client.async_send_set_adjustment.call_args == call( + **{const.KEY_ADJUSTMENT: {const.KEY_BRIGHTNESS: 100}} + ) + assert client.async_send_set_color.call_args == call( + **{ + const.KEY_PRIORITY: TEST_PRIORITY, + const.KEY_COLOR: (0, 255, 255), + const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN, + } + ) + client.adjustment = [{const.KEY_BRIGHTNESS: 100}] + call_registered_callback(client, "adjustment-update") + entity_state = hass.states.get(TEST_ENTITY_ID) + assert entity_state.attributes["brightness"] == brightness + + # On (=), 100% (=), V4L (!), [0,255,255] (=) + effect = const.KEY_COMPONENTID_EXTERNAL_SOURCES[2] # V4L + client.async_send_clear = CoroutineMock(return_value=True) + client.async_send_set_component = CoroutineMock(return_value=True) + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_EFFECT: effect}, + blocking=True, + ) + + assert client.async_send_clear.call_args == call( + **{const.KEY_PRIORITY: TEST_PRIORITY} + ) + assert client.async_send_set_component.call_args_list == [ + call( + **{ + const.KEY_COMPONENTSTATE: { + const.KEY_COMPONENT: const.KEY_COMPONENTID_EXTERNAL_SOURCES[0], + const.KEY_STATE: False, + } + } + ), + call( + **{ + const.KEY_COMPONENTSTATE: { + const.KEY_COMPONENT: const.KEY_COMPONENTID_EXTERNAL_SOURCES[1], + const.KEY_STATE: False, + } + } + ), + call( + **{ + const.KEY_COMPONENTSTATE: { + const.KEY_COMPONENT: const.KEY_COMPONENTID_EXTERNAL_SOURCES[2], + const.KEY_STATE: True, + } + } + ), + ] + client.visible_priority = {const.KEY_COMPONENTID: effect} + call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_ENTITY_ID) + assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE + assert entity_state.attributes["effect"] == effect + + # On (=), 100% (=), "Warm Blobs" (!), [0,255,255] (=) + effect = "Warm Blobs" + client.async_send_clear = CoroutineMock(return_value=True) + client.async_send_set_effect = CoroutineMock(return_value=True) + + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_EFFECT: effect}, + blocking=True, + ) + + assert client.async_send_clear.call_args == call( + **{const.KEY_PRIORITY: TEST_PRIORITY} + ) + assert client.async_send_set_effect.call_args == call( + **{ + const.KEY_PRIORITY: TEST_PRIORITY, + const.KEY_EFFECT: {const.KEY_NAME: effect}, + const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN, + } + ) + client.visible_priority = { + const.KEY_COMPONENTID: const.KEY_COMPONENTID_EFFECT, + const.KEY_OWNER: effect, + } + call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_ENTITY_ID) + assert entity_state.attributes["icon"] == hyperion_light.ICON_EFFECT + assert entity_state.attributes["effect"] == effect + + # No calls if disconnected. + client.has_loaded_state = False + call_registered_callback(client, "client-update", {"loaded-state": False}) + client.async_send_clear = CoroutineMock(return_value=True) + client.async_send_set_effect = CoroutineMock(return_value=True) + + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True + ) + + assert not client.async_send_clear.called + assert not client.async_send_set_effect.called + + +async def test_light_async_turn_off(hass): + """Test turning the light off.""" + client = create_mock_client() + await setup_entity(hass, client=client) + + client.async_send_set_component = CoroutineMock(return_value=True) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True + ) + + assert client.async_send_set_component.call_args == call( + **{ + const.KEY_COMPONENTSTATE: { + const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE, + const.KEY_STATE: False, + } + } + ) + + # No calls if no state loaded. + client.has_loaded_state = False + client.async_send_set_component = CoroutineMock(return_value=True) + call_registered_callback(client, "client-update", {"loaded-state": False}) + + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True + ) + + assert not client.async_send_set_component.called + + +async def test_light_async_updates_from_hyperion_client(hass): + """Test receiving a variety of Hyperion client callbacks.""" + client = create_mock_client() + await setup_entity(hass, client=client) + + # Bright change gets accepted. + brightness = 10 + client.adjustment = [{const.KEY_BRIGHTNESS: brightness}] + call_registered_callback(client, "adjustment-update") + entity_state = hass.states.get(TEST_ENTITY_ID) + assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0)) + + # Broken brightness value is ignored. + bad_brightness = -200 + client.adjustment = [{const.KEY_BRIGHTNESS: bad_brightness}] + call_registered_callback(client, "adjustment-update") + entity_state = hass.states.get(TEST_ENTITY_ID) + assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0)) + + # Update components. + client.is_on.return_value = True + call_registered_callback(client, "components-update") + entity_state = hass.states.get(TEST_ENTITY_ID) + assert entity_state.state == "on" + + client.is_on.return_value = False + call_registered_callback(client, "components-update") + entity_state = hass.states.get(TEST_ENTITY_ID) + assert entity_state.state == "off" + + # Update priorities (V4L) + client.is_on.return_value = True + client.visible_priority = {const.KEY_COMPONENTID: const.KEY_COMPONENTID_V4L} + call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_ENTITY_ID) + assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE + assert entity_state.attributes["hs_color"] == (0.0, 0.0) + assert entity_state.attributes["effect"] == const.KEY_COMPONENTID_V4L + + # Update priorities (Effect) + effect = "foo" + client.visible_priority = { + const.KEY_COMPONENTID: const.KEY_COMPONENTID_EFFECT, + const.KEY_OWNER: effect, + } + + call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_ENTITY_ID) + assert entity_state.attributes["effect"] == effect + assert entity_state.attributes["icon"] == hyperion_light.ICON_EFFECT + assert entity_state.attributes["hs_color"] == (0.0, 0.0) + + # Update priorities (Color) + rgb = (0, 100, 100) + client.visible_priority = { + const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR, + const.KEY_VALUE: {const.KEY_RGB: rgb}, + } + + call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_ENTITY_ID) + assert entity_state.attributes["effect"] == hyperion_light.KEY_EFFECT_SOLID + assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB + assert entity_state.attributes["hs_color"] == (180.0, 100.0) + + # Update effect list + effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}] + client.effects = effects + call_registered_callback(client, "effects-update") + entity_state = hass.states.get(TEST_ENTITY_ID) + assert entity_state.attributes["effect_list"] == [ + effect[const.KEY_NAME] for effect in effects + ] + const.KEY_COMPONENTID_EXTERNAL_SOURCES + [hyperion_light.KEY_EFFECT_SOLID] + + # Update connection status (e.g. disconnection). + + # Turn on late, check state, disconnect, ensure it cannot be turned off. + client.has_loaded_state = False + call_registered_callback(client, "client-update", {"loaded-state": False}) + entity_state = hass.states.get(TEST_ENTITY_ID) + assert entity_state.state == "unavailable" + + # Update connection status (e.g. re-connection) + client.has_loaded_state = True + call_registered_callback(client, "client-update", {"loaded-state": True}) + entity_state = hass.states.get(TEST_ENTITY_ID) + assert entity_state.state == "on" + + +async def test_full_state_loaded_on_start(hass): + """Test receiving a variety of Hyperion client callbacks.""" + client = create_mock_client() + + # Update full state (should call all update methods). + brightness = 25 + client.adjustment = [{const.KEY_BRIGHTNESS: brightness}] + client.visible_priority = { + const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR, + const.KEY_VALUE: {const.KEY_RGB: (0, 100, 100)}, + } + client.effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}] + + await setup_entity(hass, client=client) + + entity_state = hass.states.get(TEST_ENTITY_ID) + + assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0)) + assert entity_state.attributes["effect"] == hyperion_light.KEY_EFFECT_SOLID + assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB + assert entity_state.attributes["hs_color"] == (180.0, 100.0) From 6afa197586ee8df598b630d59874b6f5c0baf2be Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 24 Sep 2020 22:43:34 +0200 Subject: [PATCH 337/514] Updated frontend to 20200918.2 (#40549) --- 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 4368bff8d0f..d0c86f2cdf5 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==20200918.0"], + "requirements": ["home-assistant-frontend==20200918.2"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0a73a23489f..2658e755890 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==20200918.0 +home-assistant-frontend==20200918.2 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 6b7a8a7feee..e6f59a668ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -750,7 +750,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200918.0 +home-assistant-frontend==20200918.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2846c38d69c..9b196dc3eff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -373,7 +373,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200918.0 +home-assistant-frontend==20200918.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 0856c7292ca4dd7e33f2d9dae06529f8c2afe0fc Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Thu, 24 Sep 2020 22:50:30 +0200 Subject: [PATCH 338/514] Fix connection validation during import for dsmr integration (#40548) * Close transport when equipment identifier is received * Minor fix --- homeassistant/components/dsmr/config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index d0d0304a02a..724f9393fbf 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -48,9 +48,9 @@ class DSMRConnection: """Test if we can validate connection with the device.""" def update_telegram(telegram): - self._telegram = telegram - - transport.close() + if obis_ref.EQUIPMENT_IDENTIFIER in telegram: + self._telegram = telegram + transport.close() if self._host is None: reader_factory = partial( From f82ca44aacf861e9f2ee968a544b6db00240ae95 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 24 Sep 2020 15:31:47 -0600 Subject: [PATCH 339/514] Bump pyairvisual to 5.0.2 (#40554) * Bump pyairvisual to 5.0.2 * Fix tests --- .../components/airvisual/__init__.py | 23 +++++------ .../components/airvisual/air_quality.py | 38 +++++++++---------- .../components/airvisual/config_flow.py | 18 ++++----- .../components/airvisual/manifest.json | 2 +- homeassistant/components/airvisual/sensor.py | 26 +++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/airvisual/test_config_flow.py | 20 ++++++---- 8 files changed, 59 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index f06e4fe70b7..d6d7a93a366 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -3,7 +3,7 @@ import asyncio from datetime import timedelta from math import ceil -from pyairvisual import Client +from pyairvisual import CloudAPI, NodeSamba from pyairvisual.errors import ( AirVisualError, InvalidKeyError, @@ -211,23 +211,22 @@ def _standardize_node_pro_config_entry(hass, config_entry): async def async_setup_entry(hass, config_entry): """Set up AirVisual as config entry.""" - websession = aiohttp_client.async_get_clientsession(hass) - if CONF_API_KEY in config_entry.data: _standardize_geography_config_entry(hass, config_entry) - client = Client(api_key=config_entry.data[CONF_API_KEY], session=websession) + websession = aiohttp_client.async_get_clientsession(hass) + cloud_api = CloudAPI(config_entry.data[CONF_API_KEY], session=websession) async def async_update_data(): """Get new data from the API.""" if CONF_CITY in config_entry.data: - api_coro = client.api.city( + api_coro = cloud_api.air_quality.city( config_entry.data[CONF_CITY], config_entry.data[CONF_STATE], config_entry.data[CONF_COUNTRY], ) else: - api_coro = client.api.nearest_city( + api_coro = cloud_api.air_quality.nearest_city( config_entry.data[CONF_LATITUDE], config_entry.data[CONF_LONGITUDE], ) @@ -267,17 +266,13 @@ async def async_setup_entry(hass, config_entry): else: _standardize_node_pro_config_entry(hass, config_entry) - client = Client(session=websession) - async def async_update_data(): """Get new data from the API.""" try: - return await client.node.from_samba( - config_entry.data[CONF_IP_ADDRESS], - config_entry.data[CONF_PASSWORD], - include_history=False, - include_trends=False, - ) + async with NodeSamba( + config_entry.data[CONF_IP_ADDRESS], config_entry.data[CONF_PASSWORD] + ) as node: + return await node.async_get_latest_measurements() except NodeProError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err diff --git a/homeassistant/components/airvisual/air_quality.py b/homeassistant/components/airvisual/air_quality.py index bb2d64a23db..047367fa67c 100644 --- a/homeassistant/components/airvisual/air_quality.py +++ b/homeassistant/components/airvisual/air_quality.py @@ -40,9 +40,9 @@ class AirVisualNodeProSensor(AirVisualEntity, AirQualityEntity): @property def air_quality_index(self): """Return the Air Quality Index (AQI).""" - if self.coordinator.data["current"]["settings"]["is_aqi_usa"]: - return self.coordinator.data["current"]["measurements"]["aqi_us"] - return self.coordinator.data["current"]["measurements"]["aqi_cn"] + if self.coordinator.data["settings"]["is_aqi_usa"]: + return self.coordinator.data["measurements"]["aqi_us"] + return self.coordinator.data["measurements"]["aqi_cn"] @property def available(self): @@ -52,61 +52,59 @@ class AirVisualNodeProSensor(AirVisualEntity, AirQualityEntity): @property def carbon_dioxide(self): """Return the CO2 (carbon dioxide) level.""" - return self.coordinator.data["current"]["measurements"].get("co2") + return self.coordinator.data["measurements"].get("co2") @property def device_info(self): """Return device registry information for this entity.""" return { - "identifiers": { - (DOMAIN, self.coordinator.data["current"]["serial_number"]) - }, - "name": self.coordinator.data["current"]["settings"]["node_name"], + "identifiers": {(DOMAIN, self.coordinator.data["serial_number"])}, + "name": self.coordinator.data["settings"]["node_name"], "manufacturer": "AirVisual", - "model": f'{self.coordinator.data["current"]["status"]["model"]}', + "model": f'{self.coordinator.data["status"]["model"]}', "sw_version": ( - f'Version {self.coordinator.data["current"]["status"]["system_version"]}' - f'{self.coordinator.data["current"]["status"]["app_version"]}' + f'Version {self.coordinator.data["status"]["system_version"]}' + f'{self.coordinator.data["status"]["app_version"]}' ), } @property def name(self): """Return the name.""" - node_name = self.coordinator.data["current"]["settings"]["node_name"] + node_name = self.coordinator.data["settings"]["node_name"] return f"{node_name} Node/Pro: Air Quality" @property def particulate_matter_2_5(self): """Return the particulate matter 2.5 level.""" - return self.coordinator.data["current"]["measurements"].get("pm2_5") + return self.coordinator.data["measurements"].get("pm2_5") @property def particulate_matter_10(self): """Return the particulate matter 10 level.""" - return self.coordinator.data["current"]["measurements"].get("pm1_0") + return self.coordinator.data["measurements"].get("pm1_0") @property def particulate_matter_0_1(self): """Return the particulate matter 0.1 level.""" - return self.coordinator.data["current"]["measurements"].get("pm0_1") + return self.coordinator.data["measurements"].get("pm0_1") @property def unique_id(self): """Return a unique, Home Assistant friendly identifier for this entity.""" - return self.coordinator.data["current"]["serial_number"] + return self.coordinator.data["serial_number"] @callback def update_from_latest_data(self): """Update the entity from the latest data.""" self._attrs.update( { - ATTR_VOC: self.coordinator.data["current"]["measurements"].get("voc"), + ATTR_VOC: self.coordinator.data["measurements"].get("voc"), **{ ATTR_SENSOR_LIFE.format(pollutant): lifespan - for pollutant, lifespan in self.coordinator.data["current"][ - "status" - ]["sensor_life"].items() + for pollutant, lifespan in self.coordinator.data["status"][ + "sensor_life" + ].items() }, } ) diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index bb1c262eba7..d8ab508b8bc 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -1,7 +1,7 @@ """Define a config flow manager for AirVisual.""" import asyncio -from pyairvisual import Client +from pyairvisual import CloudAPI, NodeSamba from pyairvisual.errors import InvalidKeyError, NodeProError import voluptuous as vol @@ -108,7 +108,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_configured") websession = aiohttp_client.async_get_clientsession(self.hass) - client = Client(session=websession, api_key=user_input[CONF_API_KEY]) + cloud_api = CloudAPI(user_input[CONF_API_KEY], session=websession) # If this is the first (and only the first) time we've seen this API key, check # that it's valid: @@ -120,7 +120,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async with check_keys_lock: if user_input[CONF_API_KEY] not in checked_keys: try: - await client.api.nearest_city() + await cloud_api.air_quality.nearest_city() except InvalidKeyError: return self.async_show_form( step_id="geography", @@ -157,16 +157,10 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self._async_set_unique_id(user_input[CONF_IP_ADDRESS]) - websession = aiohttp_client.async_get_clientsession(self.hass) - client = Client(session=websession) + node = NodeSamba(user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD]) try: - await client.node.from_samba( - user_input[CONF_IP_ADDRESS], - user_input[CONF_PASSWORD], - include_history=False, - include_trends=False, - ) + await node.async_connect() except NodeProError as err: LOGGER.error("Error connecting to Node/Pro unit: %s", err) return self.async_show_form( @@ -175,6 +169,8 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors={CONF_IP_ADDRESS: "unable_to_connect"}, ) + await node.async_disconnect() + return self.async_create_entry( title=f"Node/Pro ({user_input[CONF_IP_ADDRESS]})", data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO}, diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index 93b57a4804e..d7824551275 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -3,6 +3,6 @@ "name": "AirVisual", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airvisual", - "requirements": ["pyairvisual==4.4.0"], + "requirements": ["pyairvisual==5.0.2"], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index c67f31d0c3f..a81c118ecc9 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -225,22 +225,20 @@ class AirVisualNodeProSensor(AirVisualEntity): def device_info(self): """Return device registry information for this entity.""" return { - "identifiers": { - (DOMAIN, self.coordinator.data["current"]["serial_number"]) - }, - "name": self.coordinator.data["current"]["settings"]["node_name"], + "identifiers": {(DOMAIN, self.coordinator.data["serial_number"])}, + "name": self.coordinator.data["settings"]["node_name"], "manufacturer": "AirVisual", - "model": f'{self.coordinator.data["current"]["status"]["model"]}', + "model": f'{self.coordinator.data["status"]["model"]}', "sw_version": ( - f'Version {self.coordinator.data["current"]["status"]["system_version"]}' - f'{self.coordinator.data["current"]["status"]["app_version"]}' + f'Version {self.coordinator.data["status"]["system_version"]}' + f'{self.coordinator.data["status"]["app_version"]}' ), } @property def name(self): """Return the name.""" - node_name = self.coordinator.data["current"]["settings"]["node_name"] + node_name = self.coordinator.data["settings"]["node_name"] return f"{node_name} Node/Pro: {self._name}" @property @@ -251,18 +249,14 @@ class AirVisualNodeProSensor(AirVisualEntity): @property def unique_id(self): """Return a unique, Home Assistant friendly identifier for this entity.""" - return f"{self.coordinator.data['current']['serial_number']}_{self._kind}" + return f"{self.coordinator.data['serial_number']}_{self._kind}" @callback def update_from_latest_data(self): """Update the entity from the latest data.""" if self._kind == SENSOR_KIND_BATTERY_LEVEL: - self._state = self.coordinator.data["current"]["status"]["battery"] + self._state = self.coordinator.data["status"]["battery"] elif self._kind == SENSOR_KIND_HUMIDITY: - self._state = self.coordinator.data["current"]["measurements"].get( - "humidity" - ) + self._state = self.coordinator.data["measurements"].get("humidity") elif self._kind == SENSOR_KIND_TEMPERATURE: - self._state = self.coordinator.data["current"]["measurements"].get( - "temperature_C" - ) + self._state = self.coordinator.data["measurements"].get("temperature_C") diff --git a/requirements_all.txt b/requirements_all.txt index e6f59a668ec..feef14a2850 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1229,7 +1229,7 @@ pyaehw4a1==0.3.9 pyaftership==0.1.2 # homeassistant.components.airvisual -pyairvisual==4.4.0 +pyairvisual==5.0.2 # homeassistant.components.almond pyalmond==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b196dc3eff..60269de34f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -598,7 +598,7 @@ py_nextbusnext==0.1.4 pyaehw4a1==0.3.9 # homeassistant.components.airvisual -pyairvisual==4.4.0 +pyairvisual==5.0.2 # homeassistant.components.almond pyalmond==0.0.2 diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index d365720ad26..2d0acf8fe7b 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -69,7 +69,7 @@ async def test_invalid_identifier(hass): } with patch( - "pyairvisual.api.API.nearest_city", + "pyairvisual.air_quality.AirQuality", side_effect=InvalidKeyError, ): result = await hass.config_entries.flow.async_init( @@ -96,7 +96,7 @@ async def test_migration(hass): assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - with patch("pyairvisual.api.API.nearest_city"), patch.object( + with patch("pyairvisual.air_quality.AirQuality.nearest_city"), patch.object( hass.config_entries, "async_forward_entry_setup" ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: conf}) @@ -130,7 +130,7 @@ 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", + "pyairvisual.node.NodeSamba.async_connect", side_effect=NodeProError, ): result = await hass.config_entries.flow.async_init( @@ -185,7 +185,7 @@ async def test_step_geography(hass): with patch( "homeassistant.components.airvisual.async_setup_entry", return_value=True - ), patch("pyairvisual.api.API.nearest_city"): + ), patch("pyairvisual.air_quality.AirQuality.nearest_city"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=conf ) @@ -209,7 +209,7 @@ async def test_step_import(hass): with patch( "homeassistant.components.airvisual.async_setup_entry", return_value=True - ), patch("pyairvisual.api.API.nearest_city"): + ), patch("pyairvisual.air_quality.AirQuality.nearest_city"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=geography_conf ) @@ -230,7 +230,11 @@ async def test_step_node_pro(hass): with patch( "homeassistant.components.airvisual.async_setup_entry", return_value=True - ), patch("pyairvisual.node.Node.from_samba"): + ), patch("pyairvisual.node.NodeSamba.async_connect"), patch( + "pyairvisual.node.NodeSamba.async_get_latest_measurements" + ), patch( + "pyairvisual.node.NodeSamba.async_disconnect" + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={"type": "AirVisual Node/Pro"} ) @@ -268,8 +272,8 @@ async def test_step_reauth(hass): assert result["step_id"] == "reauth_confirm" with patch( - "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch("pyairvisual.api.API.nearest_city"): + "homeassistant.components.airvisual.async_setup_entry", return_value=True + ), patch("pyairvisual.air_quality.AirQuality"): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_KEY: "defgh67890"} ) From ebdb34a9114931267980acf6d7e499e96ed53553 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 24 Sep 2020 16:19:25 -0600 Subject: [PATCH 340/514] Bump simplisafe-python to 9.3.3 (#40560) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index dd9ab53cb98..f78762b17c8 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.3.0"], + "requirements": ["simplisafe-python==9.3.3"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index feef14a2850..1075fa4a22c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1991,7 +1991,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.3.0 +simplisafe-python==9.3.3 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 60269de34f2..60b54dfc850 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -930,7 +930,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.3.0 +simplisafe-python==9.3.3 # homeassistant.components.sleepiq sleepyq==0.7 From c30982c9816f65208b80dc430a9c6dc91e4a35a1 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 25 Sep 2020 00:04:29 +0000 Subject: [PATCH 341/514] [ci skip] Translation update --- .../alarmdecoder/translations/nl.json | 4 ++- .../components/august/translations/nl.json | 3 +- .../components/broadlink/translations/nl.json | 7 +++++ .../components/canary/translations/nl.json | 30 +++++++++++++++++++ .../components/demo/translations/no.json | 2 +- .../input_boolean/translations/no.json | 2 +- .../input_datetime/translations/no.json | 2 +- .../input_number/translations/no.json | 2 +- .../input_select/translations/no.json | 2 +- .../input_text/translations/no.json | 2 +- .../components/mqtt/translations/no.json | 2 +- .../components/mqtt/translations/ru.json | 2 +- .../components/mqtt/translations/zh-Hant.json | 2 +- .../openweathermap/translations/nl.json | 1 + .../components/spotify/translations/nl.json | 1 + .../components/unifi/translations/nl.json | 3 +- .../zodiac/translations/sensor.nl.json | 17 +++++++++++ .../zoneminder/translations/nl.json | 10 +++++++ 18 files changed, 82 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/broadlink/translations/nl.json create mode 100644 homeassistant/components/canary/translations/nl.json create mode 100644 homeassistant/components/zodiac/translations/sensor.nl.json diff --git a/homeassistant/components/alarmdecoder/translations/nl.json b/homeassistant/components/alarmdecoder/translations/nl.json index 970016fe8b0..6091d4c4bd7 100644 --- a/homeassistant/components/alarmdecoder/translations/nl.json +++ b/homeassistant/components/alarmdecoder/translations/nl.json @@ -13,6 +13,7 @@ "protocol": { "data": { "device_baudrate": "Baudrate van apparaat", + "device_path": "Apparaatpad", "host": "Host", "port": "Poort" }, @@ -21,7 +22,8 @@ "user": { "data": { "protocol": "Protocol" - } + }, + "title": "Kies AlarmDecoder Protocol" } } }, diff --git a/homeassistant/components/august/translations/nl.json b/homeassistant/components/august/translations/nl.json index 1697f634d9a..e48d27801cc 100644 --- a/homeassistant/components/august/translations/nl.json +++ b/homeassistant/components/august/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Account al geconfigureerd" + "already_configured": "Account al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "cannot_connect": "Verbinding mislukt, probeer het opnieuw", diff --git a/homeassistant/components/broadlink/translations/nl.json b/homeassistant/components/broadlink/translations/nl.json new file mode 100644 index 00000000000..d6185150e49 --- /dev/null +++ b/homeassistant/components/broadlink/translations/nl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "Apparaat wordt niet ondersteund" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/nl.json b/homeassistant/components/canary/translations/nl.json new file mode 100644 index 00000000000..9681bcd7c37 --- /dev/null +++ b/homeassistant/components/canary/translations/nl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n enkele configuratie mogelijk.", + "unknown": "Onverwachte fout" + }, + "error": { + "cannot_connect": "Kon niet verbinden" + }, + "flow_title": "Canary: {name}", + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "title": "Maak verbinding met Canary" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Time-out verzoek (seconden)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/no.json b/homeassistant/components/demo/translations/no.json index 5003b9da568..48da80fd629 100644 --- a/homeassistant/components/demo/translations/no.json +++ b/homeassistant/components/demo/translations/no.json @@ -5,7 +5,7 @@ "data": { "bool": "Valgfri boolean", "constant": "Konstant", - "int": "Numerisk inndata" + "int": "Numerisk innputt" } }, "options_2": { diff --git a/homeassistant/components/input_boolean/translations/no.json b/homeassistant/components/input_boolean/translations/no.json index b0a608a1754..f08c1e111de 100644 --- a/homeassistant/components/input_boolean/translations/no.json +++ b/homeassistant/components/input_boolean/translations/no.json @@ -5,5 +5,5 @@ "on": "P\u00e5" } }, - "title": "Inndata boolsk" + "title": "Innputt boolsk" } \ No newline at end of file diff --git a/homeassistant/components/input_datetime/translations/no.json b/homeassistant/components/input_datetime/translations/no.json index e9a36c0fc88..716ca6fbbc0 100644 --- a/homeassistant/components/input_datetime/translations/no.json +++ b/homeassistant/components/input_datetime/translations/no.json @@ -1,3 +1,3 @@ { - "title": "Inndata datotid" + "title": "Innputt datotid" } \ No newline at end of file diff --git a/homeassistant/components/input_number/translations/no.json b/homeassistant/components/input_number/translations/no.json index cc918fabb2f..3988fe3eace 100644 --- a/homeassistant/components/input_number/translations/no.json +++ b/homeassistant/components/input_number/translations/no.json @@ -1,3 +1,3 @@ { - "title": "Inndata nummer" + "title": "Innputt nummer" } \ No newline at end of file diff --git a/homeassistant/components/input_select/translations/no.json b/homeassistant/components/input_select/translations/no.json index c5802730c42..a87771349a8 100644 --- a/homeassistant/components/input_select/translations/no.json +++ b/homeassistant/components/input_select/translations/no.json @@ -1,3 +1,3 @@ { - "title": "Inndata valg" + "title": "Innputt valg" } \ No newline at end of file diff --git a/homeassistant/components/input_text/translations/no.json b/homeassistant/components/input_text/translations/no.json index bf41f9dc43c..9c1141de543 100644 --- a/homeassistant/components/input_text/translations/no.json +++ b/homeassistant/components/input_text/translations/no.json @@ -1,3 +1,3 @@ { - "title": "Inndata tekst" + "title": "Innputt tekst" } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index b1863b90d1c..5f91b68b9b8 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -72,7 +72,7 @@ "birth_retain": "F\u00f8dselsmelding behold", "birth_topic": "F\u00f8dselsmelding emne", "discovery": "Aktiver oppdagelse", - "will_enable": "Aktiver f\u00f8dselsmelding", + "will_enable": "Aktiver will melding", "will_payload": "Testament melding nyttelast", "will_qos": "Testament melding QoS", "will_retain": "Testament melding behold", diff --git a/homeassistant/components/mqtt/translations/ru.json b/homeassistant/components/mqtt/translations/ru.json index 4ff21126cfa..d6540e702e4 100644 --- a/homeassistant/components/mqtt/translations/ru.json +++ b/homeassistant/components/mqtt/translations/ru.json @@ -72,7 +72,7 @@ "birth_retain": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "birth_topic": "\u0422\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 (LWT)", "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435", - "will_enable": "\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", + "will_enable": "\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "will_payload": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0442\u043e\u043f\u0438\u043a\u0430 \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "will_qos": "QoS \u0442\u043e\u043f\u0438\u043a\u0430 \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "will_retain": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json index 02978a22327..0483ad35cd3 100644 --- a/homeassistant/components/mqtt/translations/zh-Hant.json +++ b/homeassistant/components/mqtt/translations/zh-Hant.json @@ -72,7 +72,7 @@ "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_enable": "\u958b\u555f Will \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/openweathermap/translations/nl.json b/homeassistant/components/openweathermap/translations/nl.json index 797bb2af0c6..fdff089bddb 100644 --- a/homeassistant/components/openweathermap/translations/nl.json +++ b/homeassistant/components/openweathermap/translations/nl.json @@ -17,6 +17,7 @@ "mode": "Mode", "name": "Naam van de integratie" }, + "description": "Stel OpenWeatherMap-integratie in. Ga naar https://openweathermap.org/appid om een API-sleutel te genereren", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/spotify/translations/nl.json b/homeassistant/components/spotify/translations/nl.json index 9c44f30cd7f..b300066ca95 100644 --- a/homeassistant/components/spotify/translations/nl.json +++ b/homeassistant/components/spotify/translations/nl.json @@ -4,6 +4,7 @@ "already_setup": "U kunt slechts \u00e9\u00e9n Spotify-account configureren.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "missing_configuration": "De Spotify integratie is niet geconfigureerd. Gelieve de documentatie te volgen.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout [check the help section] ({docs_url})", "reauth_account_mismatch": "Het Spotify account waarmee er is geverifieerd, komt niet overeen met het account dat opnieuw moet worden geverifieerd." }, "create_entry": { diff --git a/homeassistant/components/unifi/translations/nl.json b/homeassistant/components/unifi/translations/nl.json index 37a6148d377..f945e5c4d6d 100644 --- a/homeassistant/components/unifi/translations/nl.json +++ b/homeassistant/components/unifi/translations/nl.json @@ -58,7 +58,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Maak bandbreedtegebruiksensoren voor netwerkclients" + "allow_bandwidth_sensors": "Maak bandbreedtegebruiksensoren voor netwerkclients", + "allow_uptime_sensors": "Uptime-sensoren voor netwerkclients" }, "description": "Configureer statistische sensoren", "title": "UniFi-opties 3/3" diff --git a/homeassistant/components/zodiac/translations/sensor.nl.json b/homeassistant/components/zodiac/translations/sensor.nl.json new file mode 100644 index 00000000000..c07b20de21b --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.nl.json @@ -0,0 +1,17 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Waterman", + "aries": "Ram", + "capricorn": "Steenbok", + "gemini": "Tweelingen", + "leo": "Leo", + "libra": "Weegschaal", + "pisces": "Vissen", + "sagittarius": "Boogschutter", + "scorpio": "Schorpioen", + "taurus": "Stier", + "virgo": "Maagd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/nl.json b/homeassistant/components/zoneminder/translations/nl.json index e6e487320ac..03c7559dc44 100644 --- a/homeassistant/components/zoneminder/translations/nl.json +++ b/homeassistant/components/zoneminder/translations/nl.json @@ -1,8 +1,18 @@ { "config": { + "abort": { + "auth_fail": "Gebruikersnaam of wachtwoord is onjuist.", + "connection_error": "Kan geen verbinding maken met een ZoneMinder-server." + }, "step": { "user": { "data": { + "host": "Host en poort (ex 10.10.0.4:8010)", + "password": "Wachtwoord", + "path": "ZM-pad", + "path_zms": "ZMS-pad", + "ssl": "Gebruik SSL voor verbindingen met ZoneMinder", + "username": "Gebruikersnaam", "verify_ssl": "Verifieer SSLcertificaat" }, "title": "Voeg ZoneMinder server toe." From fd05a7232ae574809acbb112811d77807219a4b6 Mon Sep 17 00:00:00 2001 From: Ari Simonen Date: Fri, 25 Sep 2020 08:46:57 +0300 Subject: [PATCH 342/514] Improve handling of sources without a name in Denon (#40514) --- homeassistant/components/denon/media_player.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index 9f451ab3025..ed90c2ddcb0 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -111,10 +111,18 @@ class DenonDevice(MediaPlayerEntity): if nsfrn: self._name = nsfrn - # SSFUN - Configured sources with names + # SSFUN - Configured sources with (optional) names self._source_list = {} for line in self.telnet_request(telnet, "SSFUN ?", all_lines=True): - source, configured_name = line[len("SSFUN") :].split(" ", 1) + ssfun = line[len("SSFUN") :].split(" ", 1) + + source = ssfun[0] + if len(ssfun) == 2 and ssfun[1]: + configured_name = ssfun[1] + else: + # No name configured, reusing the source name + configured_name = source + self._source_list[configured_name] = source # SSSOD - Deleted sources From 700b119482322e47cc733781c257576f0b9cc656 Mon Sep 17 00:00:00 2001 From: Martin Eberhardt Date: Fri, 25 Sep 2020 07:48:08 +0200 Subject: [PATCH 343/514] Add @darkfox as Rejseplanen code owner (#40329) --- CODEOWNERS | 1 + homeassistant/components/rejseplanen/manifest.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 40584db7479..0f07edf16ef 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -350,6 +350,7 @@ homeassistant/components/raincloud/* @vanstinator homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert homeassistant/components/rainmachine/* @bachya homeassistant/components/random/* @fabaff +homeassistant/components/rejseplanen/* @DarkFox homeassistant/components/repetier/* @MTrab homeassistant/components/rfxtrx/* @danielhiversen @elupus homeassistant/components/ring/* @balloob diff --git a/homeassistant/components/rejseplanen/manifest.json b/homeassistant/components/rejseplanen/manifest.json index 82ba4812592..6f91e2a9abe 100644 --- a/homeassistant/components/rejseplanen/manifest.json +++ b/homeassistant/components/rejseplanen/manifest.json @@ -3,5 +3,5 @@ "name": "Rejseplanen", "documentation": "https://www.home-assistant.io/integrations/rejseplanen", "requirements": ["rjpl==0.3.6"], - "codeowners": [] + "codeowners": ["@DarkFox"] } From 4287694b42585e97fbd38ef0b60fb6d1dc8968e0 Mon Sep 17 00:00:00 2001 From: cagnulein Date: Fri, 25 Sep 2020 09:06:24 +0200 Subject: [PATCH 344/514] Fix luci device_tracker when release is none (#40524) Co-authored-by: Martin Hjelmare --- homeassistant/components/luci/device_tracker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index 40b111d0d83..d4fb1d5f7bc 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -96,6 +96,7 @@ class LuciDeviceScanner(DeviceScanner): for device in result: if ( not hasattr(self.router.router.owrt_version, "release") + or not self.router.router.owrt_version.release or self.router.router.owrt_version.release[0] < 19 or device.reachable ): From 371b589cb294b33a5371a4b033fafd75dcf42502 Mon Sep 17 00:00:00 2001 From: Kevin Cathcart Date: Fri, 25 Sep 2020 03:15:04 -0400 Subject: [PATCH 345/514] Fix bug in state trigger when using for: without to: (#40556) --- .../homeassistant/triggers/state.py | 2 +- .../homeassistant/triggers/test_state.py | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index f57db0ed56a..a7377ffe43e 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -145,7 +145,7 @@ async def async_attach_trigger( else: cur_value = new_st.attributes.get(attribute) - if CONF_TO not in config: + if CONF_FROM in config and CONF_TO not in config: return cur_value != old_value return cur_value == new_value diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py index ce9ecaba1b0..990fc9cc956 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -538,6 +538,39 @@ async def test_if_fires_on_entity_change_with_for_without_to(hass, calls): assert len(calls) == 1 +async def test_if_does_not_fires_on_entity_change_with_for_without_to_2(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() + + utcnow = dt_util.utcnow() + with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: + mock_utcnow.return_value = utcnow + + for i in range(10): + hass.states.async_set("test.entity", str(i)) + await hass.async_block_till_done() + + mock_utcnow.return_value += timedelta(seconds=1) + async_fire_time_changed(hass, mock_utcnow.return_value) + await hass.async_block_till_done() + + assert len(calls) == 0 + + 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 2d429ea678d4814d91df6b7fcf087d8de4f9cf88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 25 Sep 2020 10:02:26 +0200 Subject: [PATCH 346/514] Add modifications for snapshot uploads (#40503) Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> --- homeassistant/components/hassio/http.py | 11 +++++++++++ tests/components/hassio/test_http.py | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 6dccdeb2101..aba0dac6494 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -18,6 +18,7 @@ from .const import X_HASS_IS_ADMIN, X_HASS_USER_ID, X_HASSIO _LOGGER = logging.getLogger(__name__) +MAX_UPLOAD_SIZE = 1024 * 1024 * 1024 NO_TIMEOUT = re.compile( r"^(?:" @@ -71,6 +72,16 @@ class HassIOView(HomeAssistantView): read_timeout = _get_timeout(path) data = None headers = _init_header(request) + if path == "snapshots/new/upload": + # We need to reuse the full content type that includes the boundary + headers[ + "Content-Type" + ] = request._stored_content_type # pylint: disable=protected-access + + # Snapshots are big, so we need to adjust the allowed size + request._client_max_size = ( # pylint: disable=protected-access + MAX_UPLOAD_SIZE + ) try: with async_timeout.timeout(10): diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 7386cf57d0c..195a8652e2f 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -129,3 +129,21 @@ async def test_forwarding_user_info(hassio_client, hass_admin_user, aioclient_mo req_headers = aioclient_mock.mock_calls[0][-1] req_headers["X-Hass-User-ID"] == hass_admin_user.id req_headers["X-Hass-Is-Admin"] == "1" + + +async def test_snapshot_upload_headers(hassio_client, aioclient_mock): + """Test that we forward the full header for snapshot upload.""" + content_type = "multipart/form-data; boundary='--webkit'" + aioclient_mock.get("http://127.0.0.1/snapshots/new/upload") + + resp = await hassio_client.get( + "/api/hassio/snapshots/new/upload", headers={"Content-Type": content_type} + ) + + # Check we got right response + assert resp.status == 200 + + assert len(aioclient_mock.mock_calls) == 1 + + req_headers = aioclient_mock.mock_calls[0][-1] + req_headers["Content-Type"] == content_type From 82c61fb6a7a6d3c0fa8a43fab02b4cf1c846050b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Fri, 25 Sep 2020 10:05:01 +0200 Subject: [PATCH 347/514] Upgrade Tibber library to 0.15.3 (#40570) --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index b3decdd250a..74f61f17d29 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -2,7 +2,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.15.2"], + "requirements": ["pyTibber==0.15.3"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index 1075fa4a22c..d8c50212201 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1208,7 +1208,7 @@ pyRFXtrx==0.25 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.15.2 +pyTibber==0.15.3 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 60b54dfc850..b27d2a85dee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -589,7 +589,7 @@ pyMetno==0.8.1 pyRFXtrx==0.25 # homeassistant.components.tibber -pyTibber==0.15.2 +pyTibber==0.15.3 # homeassistant.components.nextbus py_nextbusnext==0.1.4 From b6aa29012e8380a7ad175f6499c12927e3f3a7f9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Fri, 25 Sep 2020 13:18:21 +0200 Subject: [PATCH 348/514] Bump Plugwise-Smile to v1.5.1 (#40572) --- 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 f4cb9164e5d..222db34b344 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.4.0"], + "requirements": ["Plugwise_Smile==1.5.1"], "codeowners": ["@CoMPaTech", "@bouwew"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index d8c50212201..7f3c6f2c012 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.4.0 +Plugwise_Smile==1.5.1 # homeassistant.components.essent PyEssent==0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b27d2a85dee..fb596cf04e6 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.4.0 +Plugwise_Smile==1.5.1 # homeassistant.components.flick_electric PyFlick==0.0.2 From 0fbeb3bf7b32555ab1f552fd0707e21b02e3ec3d Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 25 Sep 2020 14:27:38 +0200 Subject: [PATCH 349/514] Use HA constants for Rfxtrx units (#40562) * Add more units and device classes * Battery level is a value beteen 0 and 9 where 9 indicate full Actual 0 battery can't occur since since then we would get to signal * Adjust tests * Add wind direction and adjust rain rate * Adjust data types to be None rather than empty string * Set counter values to count unit * Forgotten unit --- homeassistant/components/rfxtrx/__init__.py | 50 ++++++++++++--------- homeassistant/components/rfxtrx/sensor.py | 22 +++++++-- tests/components/rfxtrx/test_sensor.py | 14 +++--- 3 files changed, 53 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 6082d54df12..eb13800f748 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -18,11 +18,19 @@ from homeassistant.const import ( CONF_DEVICES, CONF_HOST, CONF_PORT, + DEGREE, + ELECTRICAL_CURRENT_AMPERE, + ENERGY_KILO_WATT_HOUR, EVENT_HOMEASSISTANT_STOP, + LENGTH_MILLIMETERS, PERCENTAGE, POWER_WATT, + PRESSURE_HPA, + SPEED_METERS_PER_SECOND, TEMP_CELSIUS, + TIME_HOURS, UV_INDEX, + VOLT, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -52,30 +60,28 @@ DATA_TYPES = OrderedDict( ("Temperature", TEMP_CELSIUS), ("Temperature2", TEMP_CELSIUS), ("Humidity", PERCENTAGE), - ("Barometer", ""), - ("Wind direction", ""), - ("Rain rate", ""), + ("Barometer", PRESSURE_HPA), + ("Wind direction", DEGREE), + ("Rain rate", f"{LENGTH_MILLIMETERS}/{TIME_HOURS}"), ("Energy usage", POWER_WATT), - ("Total usage", POWER_WATT), - ("Sound", ""), - ("Sensor Status", ""), - ("Counter value", ""), + ("Total usage", ENERGY_KILO_WATT_HOUR), + ("Sound", None), + ("Sensor Status", None), + ("Counter value", "count"), ("UV", UV_INDEX), - ("Humidity status", ""), - ("Forecast", ""), - ("Forecast numeric", ""), - ("Rain total", ""), - ("Wind average speed", ""), - ("Wind gust", ""), - ("Chill", ""), - ("Total usage", ""), - ("Count", ""), - ("Current Ch. 1", ""), - ("Current Ch. 2", ""), - ("Current Ch. 3", ""), - ("Energy usage", ""), - ("Voltage", ""), - ("Current", ""), + ("Humidity status", None), + ("Forecast", None), + ("Forecast numeric", None), + ("Rain total", LENGTH_MILLIMETERS), + ("Wind average speed", SPEED_METERS_PER_SECOND), + ("Wind gust", SPEED_METERS_PER_SECOND), + ("Chill", TEMP_CELSIUS), + ("Count", "count"), + ("Current Ch. 1", ELECTRICAL_CURRENT_AMPERE), + ("Current Ch. 2", ELECTRICAL_CURRENT_AMPERE), + ("Current Ch. 3", ELECTRICAL_CURRENT_AMPERE), + ("Voltage", VOLT), + ("Current", ELECTRICAL_CURRENT_AMPERE), ("Battery numeric", PERCENTAGE), ("Rssi numeric", "dBm"), ] diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 4acde6b0450..81e0c60e055 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -9,7 +9,14 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, ) -from homeassistant.const import CONF_DEVICES +from homeassistant.const import ( + CONF_DEVICES, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_VOLTAGE, +) from homeassistant.core import callback from . import ( @@ -30,7 +37,7 @@ def _battery_convert(value): """Battery is given as a value between 0 and 9.""" if value is None: return None - return value * 10 + return (value + 1) * 10 def _rssi_convert(value): @@ -41,10 +48,17 @@ def _rssi_convert(value): DEVICE_CLASSES = { + "Barometer": DEVICE_CLASS_PRESSURE, "Battery numeric": DEVICE_CLASS_BATTERY, - "Rssi numeric": DEVICE_CLASS_SIGNAL_STRENGTH, + "Current Ch. 1": DEVICE_CLASS_CURRENT, + "Current Ch. 2": DEVICE_CLASS_CURRENT, + "Current Ch. 3": DEVICE_CLASS_CURRENT, + "Energy usage": DEVICE_CLASS_POWER, "Humidity": DEVICE_CLASS_HUMIDITY, + "Rssi numeric": DEVICE_CLASS_SIGNAL_STRENGTH, "Temperature": DEVICE_CLASS_TEMPERATURE, + "Total usage": DEVICE_CLASS_ENERGY, + "Voltage": DEVICE_CLASS_VOLTAGE, } @@ -124,7 +138,7 @@ class RfxtrxSensor(RfxtrxEntity): """Initialize the sensor.""" super().__init__(device, device_id, event=event) self.data_type = data_type - self._unit_of_measurement = DATA_TYPES.get(data_type, "") + self._unit_of_measurement = DATA_TYPES.get(data_type) self._name = f"{device.type_string} {device.id_string} {data_type}" self._unique_id = "_".join(x for x in (*self._device_id, data_type)) diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py index 0186d403245..d0100e4ea14 100644 --- a/tests/components/rfxtrx/test_sensor.py +++ b/tests/components/rfxtrx/test_sensor.py @@ -87,7 +87,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 status" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None state = hass.states.get(f"{base_id}_rssi_numeric") assert state @@ -164,7 +164,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): state = hass.states.get(f"{base_id}_humidity_status") assert state assert state.state == "normal" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None state = hass.states.get(f"{base_id}_rssi_numeric") assert state @@ -178,7 +178,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.state == "100" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE # 2 @@ -193,7 +193,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): state = hass.states.get(f"{base_id}_humidity_status") assert state assert state.state == "normal" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None state = hass.states.get(f"{base_id}_rssi_numeric") assert state @@ -207,7 +207,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.state == "100" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE # 1 Update @@ -222,7 +222,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): state = hass.states.get(f"{base_id}_humidity_status") assert state assert state.state == "normal" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None state = hass.states.get(f"{base_id}_rssi_numeric") assert state @@ -236,7 +236,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.state == "100" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert len(hass.states.async_all()) == 10 From 25bfaf6c0d9139a70ac82eba7c496de9f93f34ed Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 25 Sep 2020 14:29:27 +0200 Subject: [PATCH 350/514] Upgrade tqdm to 4.49.0 (#40573) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 2674c780b34..0fd681b62b2 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -25,4 +25,4 @@ pytest==6.0.2 requests_mock==1.8.0 responses==0.12.0 stdlib-list==0.7.0 -tqdm==4.48.2 +tqdm==4.49.0 From 318096be79520ba1b2b4f2abc631f1e72c278b90 Mon Sep 17 00:00:00 2001 From: Angelo Gagliano <25516409+TheGardenMonkey@users.noreply.github.com> Date: Fri, 25 Sep 2020 09:12:47 -0400 Subject: [PATCH 351/514] Remove auto from the fan speed modes for VeSync (#40559) --- homeassistant/components/vesync/fan.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 7d395d93a74..7cc3f00e1a0 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -21,8 +21,8 @@ DEV_TYPE_TO_HA = { "LV-PUR131S": "fan", } -SPEED_AUTO = "auto" -FAN_SPEEDS = [SPEED_AUTO, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] +FAN_SPEEDS = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] +FAN_MODE_AUTO = "auto" async def async_setup_entry(hass, config_entry, async_add_entities): @@ -36,7 +36,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass.data[DOMAIN][VS_DISPATCHERS].append(disp) _async_setup_entities(hass.data[DOMAIN][VS_FANS], async_add_entities) - return True @callback @@ -71,8 +70,8 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): @property def speed(self): """Return the current speed.""" - if self.smartfan.mode == SPEED_AUTO: - return SPEED_AUTO + if self.smartfan.mode == FAN_MODE_AUTO: + return None if self.smartfan.mode == "manual": current_level = self.smartfan.fan_level if current_level is not None: @@ -105,11 +104,8 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): 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)) + 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.""" From 0c12af347eaa744595200fb2d95f0fac4d0bc07a Mon Sep 17 00:00:00 2001 From: Oliver Acevedo Date: Fri, 25 Sep 2020 10:55:10 -0500 Subject: [PATCH 352/514] Add Omnilogic integration (#40474) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Scaffold * Added the en translation * Modified the name * Basic functionality for config flow. * Pulled in enough to validate config flow works. * Update manifest.json * initial data polling (water and air temp sensors) * Adding sensors, debugging update function * polling updates working * support for new data format from library * Updated entity_id, friendly name, conversion for ppm, attributes for hayward display units, MSPSystemID and component systemID * Fixed errors for PR * clean up * Add login exc, check if configured, test login. * Remove debug print. * Black formatting, ran isort, update requirements. * Updated w isort. fix flake8 failures. * Fix flake8 errors * Fixed self.attrs to remove invalid self._ values - small change * Missed on small change - fixing attributes * Updated naming, updated unit of measure, updated icon, bumped omnilog… * Updated to fix flake8 issues in __init__.py and config_flow.py * Updated test_config_flow.py to pass, updated config_flow.py to correct errors in test * Remove comments in preparation for PR * update .covezragerc * Formatting fix * Rewrote sensors to dynamically add all BOWs, pumps, clorinators. Still to do - add CSAD sensors. * Rewrote sensors to dynamically add all BOWs, pumps, clorinators. Still to do - add CSAD sensors. * Added CSAD sensors for pools that have them. * Added CSAD sensors for pools that have them. * Fixed CSAD to not create if blank or don't exist, removed broad except usage to pass linting. * Updated entity naming convention. Fixed linting issues. * Added device association to the back yard / omnilogic system * Removed .0 from ppm values when returning imperial values for salt sensor * Updated to return state = None for water temp when pump is off, handled Chlorinator operatingMode = 2, and added PlatformNotReady check * Corrected exception from Omnilogic library * Bumped omnilogic to 0.3.7. Added alarm sensor/data to sensors. Handle pump off condition for ph and orp sensors. * Bumped omnilogic to 0.3.7. Added alarm sensor/data to sensors. Handle pump off condition for ph and orp sensors. * Bumped omnilogic to 0.3.7. Added alarm sensor/data to sensors. Handle pump off condition for ph and orp sensors. * Removed nested_lookup dependency, bumped omnilogic.py to 0.3.8. * Fixed lint error * Added logging for sensor creation. * Fixed linting errors with logging. * Fixed explicit chaining of raised error. Fixed issue with alarm sensor. * Fixed manifest.json based on feedback. * Fixed self.attrs, should_poll, CoordinatorEntity, SCAN_INTERVAL from comments in PR. * Addressed unique_id, moved data update coordinator, addressed minor other issues from testing * Created main OmniLogic entity for common items, reworked DataUpdateCoordinator to it's own class. * Addressed config_schema not used in __init__.py * Fixed linting issues. * Addressed several comments, still todo - separate sensor classes. * Split the Omnilogic Sensors into separate logical classes for simpler logic. * Fixed snake case lint error for AddAlarms (to add_alarms) * Addressed config_flow issues from comments. * Changed addressed ConfigNotReady issue from comments. * Updated strings.json and generated corrected en.json with translations. * Updated en.json to standard generated file. * Added config_flow tests and updated issue with config_flow on cannot_connect * Added test case for incomplete information entered. * Compressed logic in the sensor classes to reduce duplication. * Updated strings.json for polling_interval, added generic exception handling on config flow. * Removed omnilogic from the .coveragerc omit file. * Updated test_config_flow to follow recommended pattern. * Excluded sensor.py from test coverage tests. * Corected minor issues in test_config_flow from comments * Fixed linting issues on last commits * Fixed linting issues. * Corrected issue when temp state is not available from Omnilogic * Added omnililogic_common.py from .coveragerc to bypass test coverage check. * Return false on Login Exception, handle OmniLogicException in config_flow and in tests. * Handle all exceptions and in config_flow and tests, clarified test naming. * Broke out test cases per comments. * Regenerated en.json file. * Addressed changes from comments in PR. * Added session and bumped API to 0.4.0, addressed other comments from PR. * Addressed entitydata (missed earlier). * Fixed pylint issue * Added test case for options flow in test_config_flow.py * Removed super() and used self when calling methods in current class. * Addressed comments in PR. * Addressed comments in PR. * Updated translations file. * Rewrote data coordinator to output dict for easy searching. * Updated chlorinator unit when chlorinator is on/off only * Scaffold * Added the en translation * Modified the name * Basic functionality for config flow. * Pulled in enough to validate config flow works. * Update manifest.json * initial data polling (water and air temp sensors) * Adding sensors, debugging update function * polling updates working * support for new data format from library * Updated entity_id, friendly name, conversion for ppm, attributes for hayward display units, MSPSystemID and component systemID * Fixed errors for PR * clean up * Add login exc, check if configured, test login. * Remove debug print. * Black formatting, ran isort, update requirements. * Updated w isort. fix flake8 failures. * Fix flake8 errors * Fixed self.attrs to remove invalid self._ values - small change * Missed on small change - fixing attributes * Updated naming, updated unit of measure, updated icon, bumped omnilog… * Updated to fix flake8 issues in __init__.py and config_flow.py * Updated test_config_flow.py to pass, updated config_flow.py to correct errors in test * Remove comments in preparation for PR * update .covezragerc * Formatting fix * Rewrote sensors to dynamically add all BOWs, pumps, clorinators. Still to do - add CSAD sensors. * Rewrote sensors to dynamically add all BOWs, pumps, clorinators. Still to do - add CSAD sensors. * Added CSAD sensors for pools that have them. * Added CSAD sensors for pools that have them. * Fixed CSAD to not create if blank or don't exist, removed broad except usage to pass linting. * Updated entity naming convention. Fixed linting issues. * Added device association to the back yard / omnilogic system * Removed .0 from ppm values when returning imperial values for salt sensor * Updated to return state = None for water temp when pump is off, handled Chlorinator operatingMode = 2, and added PlatformNotReady check * Corrected exception from Omnilogic library * Bumped omnilogic to 0.3.7. Added alarm sensor/data to sensors. Handle pump off condition for ph and orp sensors. * Bumped omnilogic to 0.3.7. Added alarm sensor/data to sensors. Handle pump off condition for ph and orp sensors. * Bumped omnilogic to 0.3.7. Added alarm sensor/data to sensors. Handle pump off condition for ph and orp sensors. * Removed nested_lookup dependency, bumped omnilogic.py to 0.3.8. * Fixed lint error * Added logging for sensor creation. * Fixed linting errors with logging. * Fixed explicit chaining of raised error. Fixed issue with alarm sensor. * Fixed manifest.json based on feedback. * Fixed self.attrs, should_poll, CoordinatorEntity, SCAN_INTERVAL from comments in PR. * Addressed unique_id, moved data update coordinator, addressed minor other issues from testing * Created main OmniLogic entity for common items, reworked DataUpdateCoordinator to it's own class. * Addressed config_schema not used in __init__.py * Fixed linting issues. * Addressed several comments, still todo - separate sensor classes. * Split the Omnilogic Sensors into separate logical classes for simpler logic. * Fixed snake case lint error for AddAlarms (to add_alarms) * Addressed config_flow issues from comments. * Changed addressed ConfigNotReady issue from comments. * Updated strings.json and generated corrected en.json with translations. * Updated en.json to standard generated file. * Added config_flow tests and updated issue with config_flow on cannot_connect * Added test case for incomplete information entered. * Compressed logic in the sensor classes to reduce duplication. * Updated strings.json for polling_interval, added generic exception handling on config flow. * Removed omnilogic from the .coveragerc omit file. * Updated test_config_flow to follow recommended pattern. * Excluded sensor.py from test coverage tests. * Corected minor issues in test_config_flow from comments * Fixed linting issues on last commits * Fixed linting issues. * Corrected issue when temp state is not available from Omnilogic * Added omnililogic_common.py from .coveragerc to bypass test coverage check. * Return false on Login Exception, handle OmniLogicException in config_flow and in tests. * Handle all exceptions and in config_flow and tests, clarified test naming. * Broke out test cases per comments. * Regenerated en.json file. * Addressed changes from comments in PR. * Added session and bumped API to 0.4.0, addressed other comments from PR. * Addressed entitydata (missed earlier). * Fixed pylint issue * Added test case for options flow in test_config_flow.py * Removed super() and used self when calling methods in current class. * Addressed comments in PR. * Addressed comments in PR. * Updated translations file. * Rewrote data coordinator to output dict for easy searching. * Updated chlorinator unit when chlorinator is on/off only * Fixed ORP method not being @property, fixed unique_id potential issue. Does not address comments from PR. * Rewrote coordinator for updated dict structure, rewrote sensors to parse new data structure. * Added alarms as attributes on all entities which support alarm reporting. * Updated SENSOR_TYPES to sensor_types to adhere to snake case in pylint. * Addressed PR comments. * Update homeassistant/components/omnilogic/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/omnilogic/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/omnilogic/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/omnilogic/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/omnilogic/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/omnilogic/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/omnilogic/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/omnilogic/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/omnilogic/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/omnilogic/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/omnilogic/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/omnilogic/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/omnilogic/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/omnilogic/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/omnilogic/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/omnilogic/sensor.py Co-authored-by: Martin Hjelmare * Removed binary sensor conditions (alarms, on/off sensor types) and added ability for multiple guard conditions * Update homeassistant/components/omnilogic/sensor.py Co-authored-by: Martin Hjelmare * Updated per comments in PR for Pump Type and removal of force_update(). * Update homeassistant/components/omnilogic/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/omnilogic/common.py Co-authored-by: Martin Hjelmare * Correctly asserting conditions for the login exception case. * Update .coveragerc Co-authored-by: Martin Hjelmare Co-authored-by: Mike Hershberger Co-authored-by: Chad <54695185+chadlyy@users.noreply.github.com> Co-authored-by: Tim Empringham Co-authored-by: djtimca <60706061+djtimca@users.noreply.github.com> Co-authored-by: Martin Hjelmare --- .coveragerc | 3 + CODEOWNERS | 1 + .../components/omnilogic/__init__.py | 90 +++++ homeassistant/components/omnilogic/common.py | 157 ++++++++ .../components/omnilogic/config_flow.py | 95 +++++ homeassistant/components/omnilogic/const.py | 29 ++ .../components/omnilogic/manifest.json | 8 + homeassistant/components/omnilogic/sensor.py | 356 ++++++++++++++++++ .../components/omnilogic/strings.json | 30 ++ .../components/omnilogic/translations/en.json | 30 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/omnilogic/__init__.py | 1 + .../components/omnilogic/test_config_flow.py | 147 ++++++++ 15 files changed, 954 insertions(+) create mode 100644 homeassistant/components/omnilogic/__init__.py create mode 100644 homeassistant/components/omnilogic/common.py create mode 100644 homeassistant/components/omnilogic/config_flow.py create mode 100644 homeassistant/components/omnilogic/const.py create mode 100644 homeassistant/components/omnilogic/manifest.json create mode 100644 homeassistant/components/omnilogic/sensor.py create mode 100644 homeassistant/components/omnilogic/strings.json create mode 100644 homeassistant/components/omnilogic/translations/en.json create mode 100644 tests/components/omnilogic/__init__.py create mode 100644 tests/components/omnilogic/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index a5ff56b8d04..6d4c8a66762 100644 --- a/.coveragerc +++ b/.coveragerc @@ -601,6 +601,9 @@ omit = homeassistant/components/oasa_telematics/sensor.py homeassistant/components/ohmconnect/sensor.py homeassistant/components/ombi/* + homeassistant/components/omnilogic/__init__.py + homeassistant/components/omnilogic/common.py + homeassistant/components/omnilogic/sensor.py homeassistant/components/onewire/sensor.py homeassistant/components/onkyo/media_player.py homeassistant/components/onvif/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 0f07edf16ef..05c3dcf5087 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -301,6 +301,7 @@ homeassistant/components/nzbget/* @chriscla homeassistant/components/obihai/* @dshokouhi homeassistant/components/ohmconnect/* @robbiet480 homeassistant/components/ombi/* @larssont +homeassistant/components/omnilogic/* @oliver84 @djtimca @gentoosu homeassistant/components/onboarding/* @home-assistant/core homeassistant/components/onewire/* @garbled1 homeassistant/components/onvif/* @hunterjm diff --git a/homeassistant/components/omnilogic/__init__.py b/homeassistant/components/omnilogic/__init__.py new file mode 100644 index 00000000000..ff4dd93a0e1 --- /dev/null +++ b/homeassistant/components/omnilogic/__init__.py @@ -0,0 +1,90 @@ +"""The Omnilogic integration.""" +import asyncio +import logging + +from omnilogic import LoginException, OmniLogic, OmniLogicException + +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 import aiohttp_client + +from .common import OmniLogicUpdateCoordinator +from .const import CONF_SCAN_INTERVAL, COORDINATOR, DOMAIN, OMNI_API + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["sensor"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Omnilogic component.""" + hass.data.setdefault(DOMAIN, {}) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Omnilogic from a config entry.""" + + conf = entry.data + username = conf[CONF_USERNAME] + password = conf[CONF_PASSWORD] + + polling_interval = 6 + if CONF_SCAN_INTERVAL in conf: + polling_interval = conf[CONF_SCAN_INTERVAL] + + session = aiohttp_client.async_get_clientsession(hass) + + api = OmniLogic(username, password, session) + + try: + await api.connect() + await api.get_telemetry_data() + except LoginException as error: + _LOGGER.error("Login Failed: %s", error) + return False + except OmniLogicException as error: + _LOGGER.debug("OmniLogic API error: %s", error) + raise ConfigEntryNotReady from error + + coordinator = OmniLogicUpdateCoordinator( + hass=hass, + api=api, + name="Omnilogic", + polling_interval=polling_interval, + ) + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + hass.data[DOMAIN][entry.entry_id] = { + COORDINATOR: coordinator, + OMNI_API: api, + } + + 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/omnilogic/common.py b/homeassistant/components/omnilogic/common.py new file mode 100644 index 00000000000..791d81b6757 --- /dev/null +++ b/homeassistant/components/omnilogic/common.py @@ -0,0 +1,157 @@ +"""Common classes and elements for Omnilogic Integration.""" + +from datetime import timedelta +import logging + +from omnilogic import OmniLogicException + +from homeassistant.const import ATTR_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ( + ALL_ITEM_KINDS, + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +class OmniLogicUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching update data from single endpoint.""" + + def __init__( + self, + hass: HomeAssistant, + api: str, + name: str, + polling_interval: int, + ): + """Initialize the global Omnilogic data updater.""" + self.api = api + + super().__init__( + hass=hass, + logger=_LOGGER, + name=name, + update_interval=timedelta(seconds=polling_interval), + ) + + async def _async_update_data(self): + """Fetch data from OmniLogic.""" + try: + data = await self.api.get_telemetry_data() + + except OmniLogicException as error: + raise UpdateFailed(f"Error updating from OmniLogic: {error}") from error + + parsed_data = {} + + def get_item_data(item, item_kind, current_id, data): + """Get data per kind of Omnilogic API item.""" + if isinstance(item, list): + for single_item in item: + data = get_item_data(single_item, item_kind, current_id, data) + + if "systemId" in item: + system_id = item["systemId"] + current_id = current_id + (item_kind, system_id) + data[current_id] = item + + for kind in ALL_ITEM_KINDS: + if kind in item: + data = get_item_data(item[kind], kind, current_id, data) + + return data + + parsed_data = get_item_data(data, "Backyard", (), parsed_data) + + return parsed_data + + +class OmniLogicEntity(CoordinatorEntity): + """Defines the base OmniLogic entity.""" + + def __init__( + self, + coordinator: OmniLogicUpdateCoordinator, + kind: str, + name: str, + item_id: tuple, + icon: str, + ): + """Initialize the OmniLogic Entity.""" + super().__init__(coordinator) + + bow_id = None + entity_data = coordinator.data[item_id] + + backyard_id = item_id[:2] + if len(item_id) == 6: + bow_id = item_id[:4] + + msp_system_id = coordinator.data[backyard_id]["systemId"] + entity_friendly_name = f"{coordinator.data[backyard_id]['BackyardName']} " + unique_id = f"{msp_system_id}" + + if bow_id is not None: + unique_id = f"{unique_id}_{coordinator.data[bow_id]['systemId']}" + entity_friendly_name = ( + f"{entity_friendly_name}{coordinator.data[bow_id]['Name']} " + ) + + unique_id = f"{unique_id}_{coordinator.data[item_id]['systemId']}_{kind}" + + if entity_data.get("Name") is not None: + entity_friendly_name = f"{entity_friendly_name} {entity_data['Name']}" + + entity_friendly_name = f"{entity_friendly_name} {name}" + + unique_id = unique_id.replace(" ", "_") + + self._kind = kind + self._name = entity_friendly_name + self._unique_id = unique_id + self._item_id = item_id + self._icon = icon + self._attrs = {} + self._msp_system_id = msp_system_id + self._backyard_name = coordinator.data[backyard_id]["BackyardName"] + + @property + def unique_id(self) -> str: + """Return a unique, Home Assistant friendly identifier for this entity.""" + return self._unique_id + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def icon(self): + """Return the icon for the entity.""" + return self._icon + + @property + def device_state_attributes(self): + """Return the attributes.""" + return self._attrs + + @property + def device_info(self): + """Define the device as back yard/MSP System.""" + + return { + ATTR_IDENTIFIERS: {(DOMAIN, self._msp_system_id)}, + ATTR_NAME: self._backyard_name, + ATTR_MANUFACTURER: "Hayward", + ATTR_MODEL: "OmniLogic", + } diff --git a/homeassistant/components/omnilogic/config_flow.py b/homeassistant/components/omnilogic/config_flow.py new file mode 100644 index 00000000000..641ec5a8d94 --- /dev/null +++ b/homeassistant/components/omnilogic/config_flow.py @@ -0,0 +1,95 @@ +"""Config flow for Omnilogic integration.""" +import logging + +from omnilogic import LoginException, OmniLogic, OmniLogicException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client + +from .const import CONF_SCAN_INTERVAL, DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Omnilogic.""" + + 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 OptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + + config_entry = self.hass.config_entries.async_entries(DOMAIN) + if config_entry: + return self.async_abort(reason="single_instance_allowed") + + errors = {} + + if user_input is not None: + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + + session = aiohttp_client.async_get_clientsession(self.hass) + omni = OmniLogic(username, password, session) + + try: + await omni.connect() + except LoginException: + errors["base"] = "invalid_auth" + except OmniLogicException: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(user_input["username"]) + self._abort_if_unique_id_configured() + return self.async_create_entry(title="Omnilogic", data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle Omnilogic client options.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage 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=vol.Schema( + { + vol.Optional( + CONF_SCAN_INTERVAL, + default=6, + ): int, + } + ), + ) diff --git a/homeassistant/components/omnilogic/const.py b/homeassistant/components/omnilogic/const.py new file mode 100644 index 00000000000..a57ef2b062a --- /dev/null +++ b/homeassistant/components/omnilogic/const.py @@ -0,0 +1,29 @@ +"""Constants for the Omnilogic integration.""" + +DOMAIN = "omnilogic" +CONF_SCAN_INTERVAL = "polling_interval" +COORDINATOR = "coordinator" +OMNI_API = "omni_api" +ATTR_IDENTIFIERS = "identifiers" +ATTR_MANUFACTURER = "manufacturer" +ATTR_MODEL = "model" + +PUMP_TYPES = { + "FMT_VARIABLE_SPEED_PUMP": "VARIABLE", + "FMT_SINGLE_SPEED": "SINGLE", + "FMT_DUAL_SPEED": "DUAL", + "PMP_VARIABLE_SPEED_PUMP": "VARIABLE", + "PMP_SINGLE_SPEED": "SINGLE", + "PMP_DUAL_SPEED": "DUAL", +} + +ALL_ITEM_KINDS = { + "BOWS", + "Filter", + "Heater", + "Chlorinator", + "CSAD", + "Lights", + "Relays", + "Pumps", +} diff --git a/homeassistant/components/omnilogic/manifest.json b/homeassistant/components/omnilogic/manifest.json new file mode 100644 index 00000000000..468b48d620a --- /dev/null +++ b/homeassistant/components/omnilogic/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "omnilogic", + "name": "Hayward Omnilogic", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/omnilogic", + "requirements": ["omnilogic==0.4.0"], + "codeowners": ["@oliver84","@djtimca","@gentoosu"] +} diff --git a/homeassistant/components/omnilogic/sensor.py b/homeassistant/components/omnilogic/sensor.py new file mode 100644 index 00000000000..f4bb0f45d5e --- /dev/null +++ b/homeassistant/components/omnilogic/sensor.py @@ -0,0 +1,356 @@ +"""Definition and setup of the Omnilogic Sensors for Home Assistant.""" + +import logging + +from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, + MASS_GRAMS, + PERCENTAGE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + VOLUME_LITERS, +) + +from .common import OmniLogicEntity, OmniLogicUpdateCoordinator +from .const import COORDINATOR, DOMAIN, PUMP_TYPES + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the sensor platform.""" + + coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] + entities = [] + + for item_id, item in coordinator.data.items(): + id_len = len(item_id) + item_kind = item_id[-2] + entity_settings = SENSOR_TYPES.get((id_len, item_kind)) + + if not entity_settings: + continue + + for entity_setting in entity_settings: + for state_key, entity_class in entity_setting["entity_classes"].items(): + if state_key not in item: + continue + + guard = False + for guard_condition in entity_setting["guard_condition"]: + if guard_condition and all( + item.get(guard_key) == guard_value + for guard_key, guard_value in guard_condition.items() + ): + guard = True + + if guard: + continue + + entity = entity_class( + coordinator=coordinator, + state_key=state_key, + name=entity_setting["name"], + kind=entity_setting["kind"], + item_id=item_id, + device_class=entity_setting["device_class"], + icon=entity_setting["icon"], + unit=entity_setting["unit"], + ) + + entities.append(entity) + + async_add_entities(entities) + + +class OmnilogicSensor(OmniLogicEntity): + """Defines an Omnilogic sensor entity.""" + + def __init__( + self, + coordinator: OmniLogicUpdateCoordinator, + kind: str, + name: str, + device_class: str, + icon: str, + unit: str, + item_id: tuple, + state_key: str, + ): + """Initialize Entities.""" + super().__init__( + coordinator=coordinator, + kind=kind, + name=name, + item_id=item_id, + icon=icon, + ) + + backyard_id = item_id[:2] + unit_type = coordinator.data[backyard_id].get("Unit-of-Measurement") + + self._unit_type = unit_type + self._device_class = device_class + self._unit = unit + self._state_key = state_key + + @property + def device_class(self): + """Return the device class of the entity.""" + return self._device_class + + @property + def unit_of_measurement(self): + """Return the right unit of measure.""" + return self._unit + + +class OmniLogicTemperatureSensor(OmnilogicSensor): + """Define an OmniLogic Temperature (Air/Water) Sensor.""" + + @property + def state(self): + """Return the state for the temperature sensor.""" + sensor_data = self.coordinator.data[self._item_id][self._state_key] + + hayward_state = sensor_data + hayward_unit_of_measure = TEMP_FAHRENHEIT + state = sensor_data + + if self._unit_type == "Metric": + hayward_state = round((hayward_state - 32) * 5 / 9, 1) + hayward_unit_of_measure = TEMP_CELSIUS + + if int(sensor_data) == -1: + hayward_state = None + state = None + + self._attrs["hayward_temperature"] = hayward_state + self._attrs["hayward_unit_of_measure"] = hayward_unit_of_measure + + self._unit = TEMP_FAHRENHEIT + + return state + + +class OmniLogicPumpSpeedSensor(OmnilogicSensor): + """Define an OmniLogic Pump Speed Sensor.""" + + @property + def state(self): + """Return the state for the pump speed sensor.""" + + pump_type = PUMP_TYPES[self.coordinator.data[self._item_id]["Filter-Type"]] + pump_speed = self.coordinator.data[self._item_id][self._state_key] + + if pump_type == "VARIABLE": + self._unit = PERCENTAGE + state = pump_speed + elif pump_type == "DUAL": + if pump_speed == 0: + state = "off" + elif pump_speed == self.coordinator.data[self._item_id].get( + "Min-Pump-Speed" + ): + state = "low" + elif pump_speed == self.coordinator.data[self._item_id].get( + "Max-Pump-Speed" + ): + state = "high" + + self._attrs["pump_type"] = pump_type + + return state + + +class OmniLogicSaltLevelSensor(OmnilogicSensor): + """Define an OmniLogic Salt Level Sensor.""" + + @property + def state(self): + """Return the state for the salt level sensor.""" + + salt_return = self.coordinator.data[self._item_id][self._state_key] + unit_of_measurement = self._unit + + if self._unit_type == "Metric": + salt_return = round(salt_return / 1000, 2) + unit_of_measurement = f"{MASS_GRAMS}/{VOLUME_LITERS}" + + self._unit = unit_of_measurement + + return salt_return + + +class OmniLogicChlorinatorSensor(OmnilogicSensor): + """Define an OmniLogic Chlorinator Sensor.""" + + @property + def state(self): + """Return the state for the chlorinator sensor.""" + state = self.coordinator.data[self._item_id][self._state_key] + + return state + + +class OmniLogicPHSensor(OmnilogicSensor): + """Define an OmniLogic pH Sensor.""" + + @property + def state(self): + """Return the state for the pH sensor.""" + + ph_state = self.coordinator.data[self._item_id][self._state_key] + + if ph_state == 0: + ph_state = None + + return ph_state + + +class OmniLogicORPSensor(OmnilogicSensor): + """Define an OmniLogic ORP Sensor.""" + + def __init__( + self, + coordinator: OmniLogicUpdateCoordinator, + state_key: str, + name: str, + kind: str, + item_id: tuple, + device_class: str, + icon: str, + unit: str, + ): + """Initialize the sensor.""" + super().__init__( + coordinator=coordinator, + kind=kind, + name=name, + device_class=device_class, + icon=icon, + unit=unit, + item_id=item_id, + state_key=state_key, + ) + + @property + def state(self): + """Return the state for the ORP sensor.""" + + orp_state = self.coordinator.data[self._item_id][self._state_key] + + if orp_state == -1: + orp_state = None + + return orp_state + + +SENSOR_TYPES = { + (2, "Backyard"): [ + { + "entity_classes": {"airTemp": OmniLogicTemperatureSensor}, + "name": "Air Temperature", + "kind": "air_temperature", + "device_class": DEVICE_CLASS_TEMPERATURE, + "icon": None, + "unit": TEMP_FAHRENHEIT, + "guard_condition": [{}], + }, + ], + (4, "BOWS"): [ + { + "entity_classes": {"waterTemp": OmniLogicTemperatureSensor}, + "name": "Water Temperature", + "kind": "water_temperature", + "device_class": DEVICE_CLASS_TEMPERATURE, + "icon": None, + "unit": TEMP_FAHRENHEIT, + "guard_condition": [{}], + }, + ], + (6, "Filter"): [ + { + "entity_classes": {"filterSpeed": OmniLogicPumpSpeedSensor}, + "name": "Speed", + "kind": "filter_pump_speed", + "device_class": None, + "icon": "mdi:speedometer", + "unit": PERCENTAGE, + "guard_condition": [ + {"Type": "FMT_SINGLE_SPEED"}, + ], + }, + ], + (6, "Pumps"): [ + { + "entity_classes": {"pumpSpeed": OmniLogicPumpSpeedSensor}, + "name": "Pump Speed", + "kind": "pump_speed", + "device_class": None, + "icon": "mdi:speedometer", + "unit": PERCENTAGE, + "guard_condition": [ + {"Type": "PMP_SINGLE_SPEED"}, + ], + }, + ], + (6, "Chlorinator"): [ + { + "entity_classes": {"Timed-Percent": OmniLogicChlorinatorSensor}, + "name": "Setting", + "kind": "chlorinator", + "device_class": None, + "icon": "mdi:gauge", + "unit": PERCENTAGE, + "guard_condition": [ + { + "Shared-Type": "BOW_SHARED_EQUIPMENT", + "status": "0", + }, + { + "operatingMode": "2", + }, + ], + }, + { + "entity_classes": {"avgSaltLevel": OmniLogicSaltLevelSensor}, + "name": "Salt Level", + "kind": "salt_level", + "device_class": None, + "icon": "mdi:gauge", + "unit": CONCENTRATION_PARTS_PER_MILLION, + "guard_condition": [ + { + "Shared-Type": "BOW_SHARED_EQUIPMENT", + "status": "0", + }, + ], + }, + ], + (6, "CSAD"): [ + { + "entity_classes": {"ph": OmniLogicPHSensor}, + "name": "pH", + "kind": "csad_ph", + "device_class": None, + "icon": "mdi:gauge", + "unit": "pH", + "guard_condition": [ + {"ph": ""}, + ], + }, + { + "entity_classes": {"orp": OmniLogicORPSensor}, + "name": "ORP", + "kind": "csad_orp", + "device_class": None, + "icon": "mdi:gauge", + "unit": "mV", + "guard_condition": [ + {"orp": ""}, + ], + }, + ], +} diff --git a/homeassistant/components/omnilogic/strings.json b/homeassistant/components/omnilogic/strings.json new file mode 100644 index 00000000000..285bc29b802 --- /dev/null +++ b/homeassistant/components/omnilogic/strings.json @@ -0,0 +1,30 @@ +{ + "title": "Omnilogic", + "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": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "polling_interval": "Polling interval (in seconds)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/en.json b/homeassistant/components/omnilogic/translations/en.json new file mode 100644 index 00000000000..8dde40be8fc --- /dev/null +++ b/homeassistant/components/omnilogic/translations/en.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + }, + "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": { + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "polling_interval": "Polling interval (in seconds)" + } + } + } + }, + "title": "Omnilogic" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index fae053ac1a1..55e6bf2eafe 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -127,6 +127,7 @@ FLOWS = [ "nut", "nws", "nzbget", + "omnilogic", "onvif", "opentherm_gw", "openuv", diff --git a/requirements_all.txt b/requirements_all.txt index 7f3c6f2c012..0032fa2fdc3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1012,6 +1012,9 @@ oauth2client==4.0.0 # homeassistant.components.oem oemthermostat==1.1 +# homeassistant.components.omnilogic +omnilogic==0.4.0 + # homeassistant.components.onkyo onkyo-eiscp==1.2.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fb596cf04e6..e0d14bd9ea2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -483,6 +483,9 @@ numpy==1.19.2 # homeassistant.components.google oauth2client==4.0.0 +# homeassistant.components.omnilogic +omnilogic==0.4.0 + # homeassistant.components.onvif onvif-zeep-async==0.5.0 diff --git a/tests/components/omnilogic/__init__.py b/tests/components/omnilogic/__init__.py new file mode 100644 index 00000000000..b7b8008abaa --- /dev/null +++ b/tests/components/omnilogic/__init__.py @@ -0,0 +1 @@ +"""Tests for the Omnilogic integration.""" diff --git a/tests/components/omnilogic/test_config_flow.py b/tests/components/omnilogic/test_config_flow.py new file mode 100644 index 00000000000..ef29ff9f674 --- /dev/null +++ b/tests/components/omnilogic/test_config_flow.py @@ -0,0 +1,147 @@ +"""Test the Omnilogic config flow.""" +from omnilogic import LoginException, OmniLogicException + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.omnilogic.const import DOMAIN + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +DATA = {"username": "test-username", "password": "test-password"} + + +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.omnilogic.config_flow.OmniLogic.connect", + return_value=True, + ), patch( + "homeassistant.components.omnilogic.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.omnilogic.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + DATA, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Omnilogic" + assert result2["data"] == DATA + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_already_configured(hass): + """Test config flow when Omnilogic component is already setup.""" + MockConfigEntry(domain="omnilogic", data=DATA).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"] == "abort" + assert result["reason"] == "single_instance_allowed" + + +async def test_with_invalid_credentials(hass): + """Test with invalid credentials.""" + + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.omnilogic.OmniLogic.connect", + side_effect=LoginException, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + DATA, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test if invalid response or no connection returned from Hayward.""" + + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.omnilogic.OmniLogic.connect", + side_effect=OmniLogicException, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + DATA, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_with_unknown_error(hass): + """Test with unknown error response from Hayward.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.omnilogic.OmniLogic.connect", + side_effect=Exception, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + DATA, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "unknown"} + + +async def test_option_flow(hass): + """Test option flow.""" + entry = MockConfigEntry(domain=DOMAIN, data=DATA) + entry.add_to_hass(hass) + + assert not entry.options + + with patch( + "homeassistant.components.omnilogic.async_setup_entry", return_value=True + ): + result = await hass.config_entries.options.async_init( + entry.entry_id, + data=None, + ) + + 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={"polling_interval": 9}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "" + assert result["data"]["polling_interval"] == 9 From e30acfbfee5d3b7e873b28b98f12559a7c38e8f5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 25 Sep 2020 19:14:25 +0200 Subject: [PATCH 353/514] Rewrite light component tests to async pytest tests (#40589) --- tests/components/light/test_init.py | 892 ++++++++++++++-------------- 1 file changed, 446 insertions(+), 446 deletions(-) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index a3f24bef3c9..f3276313775 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -2,9 +2,9 @@ # pylint: disable=protected-access from io import StringIO import os -import unittest import pytest +import voluptuous as vol from homeassistant import core from homeassistant.components import light @@ -18,479 +18,479 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.exceptions import Unauthorized -from homeassistant.setup import async_setup_component, setup_component +from homeassistant.setup import async_setup_component import tests.async_mock as mock -from tests.common import get_test_home_assistant, mock_service, mock_storage +from tests.common import async_mock_service from tests.components.light import common -class TestLight(unittest.TestCase): - """Test the light module.""" +@pytest.fixture +def mock_storage(hass, hass_storage): + """Clean up user light files at the end.""" + yield + user_light_file = hass.config.path(light.LIGHT_PROFILES_FILE) - # pylint: disable=invalid-name - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.addCleanup(self.tear_down_cleanup) + if os.path.isfile(user_light_file): + os.remove(user_light_file) - def tear_down_cleanup(self): - """Stop everything that was started.""" - self.hass.stop() - user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE) +async def test_methods(hass): + """Test if methods call the services as expected.""" + # Test is_on + hass.states.async_set("light.test", STATE_ON) + assert light.is_on(hass, "light.test") - if os.path.isfile(user_light_file): - os.remove(user_light_file) + hass.states.async_set("light.test", STATE_OFF) + assert not light.is_on(hass, "light.test") - def test_methods(self): - """Test if methods call the services as expected.""" - # Test is_on - self.hass.states.set("light.test", STATE_ON) - assert light.is_on(self.hass, "light.test") + # Test turn_on + turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON) - self.hass.states.set("light.test", STATE_OFF) - assert not light.is_on(self.hass, "light.test") + await common.async_turn_on( + hass, + entity_id="entity_id_val", + transition="transition_val", + brightness="brightness_val", + rgb_color="rgb_color_val", + xy_color="xy_color_val", + profile="profile_val", + color_name="color_name_val", + white_value="white_val", + ) - # Test turn_on - turn_on_calls = mock_service(self.hass, light.DOMAIN, SERVICE_TURN_ON) + await hass.async_block_till_done() - common.turn_on( - self.hass, - entity_id="entity_id_val", - transition="transition_val", - brightness="brightness_val", - rgb_color="rgb_color_val", - xy_color="xy_color_val", - profile="profile_val", - color_name="color_name_val", - white_value="white_val", + assert len(turn_on_calls) == 1 + call = turn_on_calls[-1] + + assert call.domain == light.DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data.get(ATTR_ENTITY_ID) == "entity_id_val" + assert call.data.get(light.ATTR_TRANSITION) == "transition_val" + assert call.data.get(light.ATTR_BRIGHTNESS) == "brightness_val" + assert call.data.get(light.ATTR_RGB_COLOR) == "rgb_color_val" + assert call.data.get(light.ATTR_XY_COLOR) == "xy_color_val" + assert call.data.get(light.ATTR_PROFILE) == "profile_val" + assert call.data.get(light.ATTR_COLOR_NAME) == "color_name_val" + assert call.data.get(light.ATTR_WHITE_VALUE) == "white_val" + + # Test turn_off + turn_off_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_OFF) + + await common.async_turn_off( + hass, entity_id="entity_id_val", transition="transition_val" + ) + + await hass.async_block_till_done() + + assert len(turn_off_calls) == 1 + call = turn_off_calls[-1] + + assert light.DOMAIN == call.domain + assert SERVICE_TURN_OFF == call.service + assert call.data[ATTR_ENTITY_ID] == "entity_id_val" + assert call.data[light.ATTR_TRANSITION] == "transition_val" + + # Test toggle + toggle_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TOGGLE) + + await common.async_toggle( + hass, entity_id="entity_id_val", transition="transition_val" + ) + + await hass.async_block_till_done() + + assert len(toggle_calls) == 1 + call = toggle_calls[-1] + + assert call.domain == light.DOMAIN + assert call.service == SERVICE_TOGGLE + assert call.data[ATTR_ENTITY_ID] == "entity_id_val" + assert call.data[light.ATTR_TRANSITION] == "transition_val" + + +async def test_services(hass): + """Test the provided services.""" + platform = getattr(hass.components, "test.light") + + platform.init() + assert await async_setup_component( + hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} + ) + await hass.async_block_till_done() + + ent1, ent2, ent3 = platform.ENTITIES + + # Test init + assert light.is_on(hass, ent1.entity_id) + assert not light.is_on(hass, ent2.entity_id) + assert not light.is_on(hass, ent3.entity_id) + + # Test basic turn_on, turn_off, toggle services + await common.async_turn_off(hass, entity_id=ent1.entity_id) + await common.async_turn_on(hass, entity_id=ent2.entity_id) + + await hass.async_block_till_done() + + assert not light.is_on(hass, ent1.entity_id) + assert light.is_on(hass, ent2.entity_id) + + # turn on all lights + await common.async_turn_on(hass) + + await hass.async_block_till_done() + + assert light.is_on(hass, ent1.entity_id) + assert light.is_on(hass, ent2.entity_id) + assert light.is_on(hass, ent3.entity_id) + + # turn off all lights + await common.async_turn_off(hass) + + await hass.async_block_till_done() + + assert not light.is_on(hass, ent1.entity_id) + assert not light.is_on(hass, ent2.entity_id) + assert not light.is_on(hass, ent3.entity_id) + + # turn off all lights by setting brightness to 0 + await common.async_turn_on(hass) + await hass.async_block_till_done() + + await common.async_turn_on(hass, brightness=0) + await hass.async_block_till_done() + + assert not light.is_on(hass, ent1.entity_id) + assert not light.is_on(hass, ent2.entity_id) + assert not light.is_on(hass, ent3.entity_id) + + # toggle all lights + await common.async_toggle(hass) + + await hass.async_block_till_done() + + assert light.is_on(hass, ent1.entity_id) + assert light.is_on(hass, ent2.entity_id) + assert light.is_on(hass, ent3.entity_id) + + # toggle all lights + await common.async_toggle(hass) + + await hass.async_block_till_done() + + assert not light.is_on(hass, ent1.entity_id) + assert not light.is_on(hass, ent2.entity_id) + assert not light.is_on(hass, ent3.entity_id) + + # Ensure all attributes process correctly + await common.async_turn_on( + hass, ent1.entity_id, transition=10, brightness=20, color_name="blue" + ) + await common.async_turn_on( + hass, ent2.entity_id, rgb_color=(255, 255, 255), white_value=255 + ) + await common.async_turn_on(hass, ent3.entity_id, xy_color=(0.4, 0.6)) + await hass.async_block_till_done() + + _, data = ent1.last_call("turn_on") + assert { + light.ATTR_TRANSITION: 10, + light.ATTR_BRIGHTNESS: 20, + light.ATTR_HS_COLOR: (240, 100), + } == data + + _, data = ent2.last_call("turn_on") + assert {light.ATTR_HS_COLOR: (0, 0), light.ATTR_WHITE_VALUE: 255} == data + + _, data = ent3.last_call("turn_on") + assert {light.ATTR_HS_COLOR: (71.059, 100)} == data + + # Ensure attributes are filtered when light is turned off + await common.async_turn_on( + hass, ent1.entity_id, transition=10, brightness=0, color_name="blue" + ) + await common.async_turn_on( + hass, + ent2.entity_id, + brightness=0, + rgb_color=(255, 255, 255), + white_value=0, + ) + await common.async_turn_on(hass, ent3.entity_id, brightness=0, xy_color=(0.4, 0.6)) + + await hass.async_block_till_done() + + assert not light.is_on(hass, ent1.entity_id) + assert not light.is_on(hass, ent2.entity_id) + assert not light.is_on(hass, ent3.entity_id) + + _, data = ent1.last_call("turn_off") + assert {light.ATTR_TRANSITION: 10} == data + + _, data = ent2.last_call("turn_off") + assert {} == data + + _, data = ent3.last_call("turn_off") + assert {} == data + + # One of the light profiles + prof_name, prof_h, prof_s, prof_bri, prof_t = "relax", 35.932, 69.412, 144, 0 + + # Test light profiles + await common.async_turn_on(hass, ent1.entity_id, profile=prof_name) + # Specify a profile and a brightness attribute to overwrite it + await common.async_turn_on( + hass, ent2.entity_id, profile=prof_name, brightness=100, transition=1 + ) + + await hass.async_block_till_done() + + _, data = ent1.last_call("turn_on") + assert { + light.ATTR_BRIGHTNESS: prof_bri, + light.ATTR_HS_COLOR: (prof_h, prof_s), + light.ATTR_TRANSITION: prof_t, + } == data + + _, data = ent2.last_call("turn_on") + assert { + light.ATTR_BRIGHTNESS: 100, + light.ATTR_HS_COLOR: (prof_h, prof_s), + light.ATTR_TRANSITION: 1, + } == data + + # Test toggle with parameters + await common.async_toggle( + hass, ent3.entity_id, profile=prof_name, brightness_pct=100 + ) + await hass.async_block_till_done() + _, data = ent3.last_call("turn_on") + assert { + light.ATTR_BRIGHTNESS: 255, + light.ATTR_HS_COLOR: (prof_h, prof_s), + light.ATTR_TRANSITION: prof_t, + } == data + + # Test bad data + await common.async_turn_on(hass) + await common.async_turn_on(hass, ent1.entity_id, profile="nonexisting") + with pytest.raises(vol.MultipleInvalid): + await common.async_turn_on(hass, ent2.entity_id, xy_color=["bla-di-bla", 5]) + with pytest.raises(vol.MultipleInvalid): + await common.async_turn_on(hass, ent3.entity_id, rgb_color=[255, None, 2]) + + await hass.async_block_till_done() + + _, data = ent1.last_call("turn_on") + assert {} == data + + _, data = ent2.last_call("turn_on") + assert {} == data + + _, data = ent3.last_call("turn_on") + assert {} == data + + # faulty attributes will not trigger a service call + with pytest.raises(vol.MultipleInvalid): + await common.async_turn_on( + hass, ent1.entity_id, profile=prof_name, brightness="bright" ) + with pytest.raises(vol.MultipleInvalid): + await common.async_turn_on(hass, ent1.entity_id, rgb_color="yellowish") + with pytest.raises(vol.MultipleInvalid): + await common.async_turn_on(hass, ent2.entity_id, white_value="high") - self.hass.block_till_done() + await hass.async_block_till_done() - assert 1 == len(turn_on_calls) - call = turn_on_calls[-1] + _, data = ent1.last_call("turn_on") + assert {} == data - assert light.DOMAIN == call.domain - assert SERVICE_TURN_ON == call.service - assert "entity_id_val" == call.data.get(ATTR_ENTITY_ID) - assert "transition_val" == call.data.get(light.ATTR_TRANSITION) - assert "brightness_val" == call.data.get(light.ATTR_BRIGHTNESS) - assert "rgb_color_val" == call.data.get(light.ATTR_RGB_COLOR) - assert "xy_color_val" == call.data.get(light.ATTR_XY_COLOR) - assert "profile_val" == call.data.get(light.ATTR_PROFILE) - assert "color_name_val" == call.data.get(light.ATTR_COLOR_NAME) - assert "white_val" == call.data.get(light.ATTR_WHITE_VALUE) + _, data = ent2.last_call("turn_on") + assert {} == data - # Test turn_off - turn_off_calls = mock_service(self.hass, light.DOMAIN, SERVICE_TURN_OFF) - common.turn_off( - self.hass, entity_id="entity_id_val", transition="transition_val" +async def test_broken_light_profiles(hass, mock_storage): + """Test light profiles.""" + platform = getattr(hass.components, "test.light") + platform.init() + + user_light_file = hass.config.path(light.LIGHT_PROFILES_FILE) + + # Setup a wrong light file + with open(user_light_file, "w") as user_file: + user_file.write("id,x,y,brightness,transition\n") + user_file.write("I,WILL,NOT,WORK,EVER\n") + + assert not await async_setup_component( + hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} + ) + + +async def test_light_profiles(hass, mock_storage): + """Test light profiles.""" + platform = getattr(hass.components, "test.light") + platform.init() + + user_light_file = hass.config.path(light.LIGHT_PROFILES_FILE) + + with open(user_light_file, "w") as user_file: + user_file.write("id,x,y,brightness\n") + user_file.write("test,.4,.6,100\n") + user_file.write("test_off,0,0,0\n") + + assert await async_setup_component( + hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} + ) + await hass.async_block_till_done() + + ent1, _, _ = platform.ENTITIES + + await common.async_turn_on(hass, ent1.entity_id, profile="test") + + await hass.async_block_till_done() + + _, data = ent1.last_call("turn_on") + + assert light.is_on(hass, ent1.entity_id) + assert data == { + light.ATTR_HS_COLOR: (71.059, 100), + light.ATTR_BRIGHTNESS: 100, + light.ATTR_TRANSITION: 0, + } + + await common.async_turn_on(hass, ent1.entity_id, profile="test_off") + + await hass.async_block_till_done() + + _, data = ent1.last_call("turn_off") + + assert not light.is_on(hass, ent1.entity_id) + assert data == {light.ATTR_TRANSITION: 0} + + +async def test_light_profiles_with_transition(hass, mock_storage): + """Test light profiles with transition.""" + platform = getattr(hass.components, "test.light") + platform.init() + + user_light_file = hass.config.path(light.LIGHT_PROFILES_FILE) + + with open(user_light_file, "w") as user_file: + user_file.write("id,x,y,brightness,transition\n") + user_file.write("test,.4,.6,100,2\n") + user_file.write("test_off,0,0,0,0\n") + + assert await async_setup_component( + hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} + ) + await hass.async_block_till_done() + + ent1, _, _ = platform.ENTITIES + + await common.async_turn_on(hass, ent1.entity_id, profile="test") + + await hass.async_block_till_done() + + _, data = ent1.last_call("turn_on") + + assert light.is_on(hass, ent1.entity_id) + assert data == { + light.ATTR_HS_COLOR: (71.059, 100), + light.ATTR_BRIGHTNESS: 100, + light.ATTR_TRANSITION: 2, + } + + await common.async_turn_on(hass, ent1.entity_id, profile="test_off") + + await hass.async_block_till_done() + + _, data = ent1.last_call("turn_off") + + assert not light.is_on(hass, ent1.entity_id) + assert data == {light.ATTR_TRANSITION: 0} + + +async def test_default_profiles_group(hass, mock_storage): + """Test default turn-on light profile for all lights.""" + platform = getattr(hass.components, "test.light") + platform.init() + + user_light_file = hass.config.path(light.LIGHT_PROFILES_FILE) + real_isfile = os.path.isfile + real_open = open + + def _mock_isfile(path): + if path == user_light_file: + return True + return real_isfile(path) + + def _mock_open(path, *args, **kwargs): + if path == user_light_file: + return StringIO(profile_data) + return real_open(path, *args, **kwargs) + + profile_data = "id,x,y,brightness,transition\ngroup.all_lights.default,.4,.6,99,2\n" + with mock.patch("os.path.isfile", side_effect=_mock_isfile), mock.patch( + "builtins.open", side_effect=_mock_open + ): + assert await async_setup_component( + hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + await hass.async_block_till_done() - self.hass.block_till_done() + ent, _, _ = platform.ENTITIES + await common.async_turn_on(hass, ent.entity_id) + await hass.async_block_till_done() + _, data = ent.last_call("turn_on") + assert data == { + light.ATTR_HS_COLOR: (71.059, 100), + light.ATTR_BRIGHTNESS: 99, + light.ATTR_TRANSITION: 2, + } - assert 1 == len(turn_off_calls) - call = turn_off_calls[-1] - assert light.DOMAIN == call.domain - assert SERVICE_TURN_OFF == call.service - assert "entity_id_val" == call.data[ATTR_ENTITY_ID] - assert "transition_val" == call.data[light.ATTR_TRANSITION] +async def test_default_profiles_light(hass, mock_storage): + """Test default turn-on light profile for a specific light.""" + platform = getattr(hass.components, "test.light") + platform.init() - # Test toggle - toggle_calls = mock_service(self.hass, light.DOMAIN, SERVICE_TOGGLE) + user_light_file = hass.config.path(light.LIGHT_PROFILES_FILE) + real_isfile = os.path.isfile + real_open = open - common.toggle(self.hass, entity_id="entity_id_val", transition="transition_val") + def _mock_isfile(path): + if path == user_light_file: + return True + return real_isfile(path) - self.hass.block_till_done() + def _mock_open(path, *args, **kwargs): + if path == user_light_file: + return StringIO(profile_data) + return real_open(path, *args, **kwargs) - assert 1 == len(toggle_calls) - call = toggle_calls[-1] - - assert light.DOMAIN == call.domain - assert SERVICE_TOGGLE == call.service - assert "entity_id_val" == call.data[ATTR_ENTITY_ID] - assert "transition_val" == call.data[light.ATTR_TRANSITION] - - def test_services(self): - """Test the provided services.""" - platform = getattr(self.hass.components, "test.light") - - platform.init() - assert setup_component( - self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} + profile_data = ( + "id,x,y,brightness,transition\n" + + "group.all_lights.default,.3,.5,200,0\n" + + "light.ceiling_2.default,.6,.6,100,3\n" + ) + with mock.patch("os.path.isfile", side_effect=_mock_isfile), mock.patch( + "builtins.open", side_effect=_mock_open + ): + assert await async_setup_component( + hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - self.hass.block_till_done() - - ent1, ent2, ent3 = platform.ENTITIES - - # Test init - assert light.is_on(self.hass, ent1.entity_id) - assert not light.is_on(self.hass, ent2.entity_id) - assert not light.is_on(self.hass, ent3.entity_id) - - # Test basic turn_on, turn_off, toggle services - common.turn_off(self.hass, entity_id=ent1.entity_id) - common.turn_on(self.hass, entity_id=ent2.entity_id) - - self.hass.block_till_done() - - assert not light.is_on(self.hass, ent1.entity_id) - assert light.is_on(self.hass, ent2.entity_id) - - # turn on all lights - common.turn_on(self.hass) - - self.hass.block_till_done() - - assert light.is_on(self.hass, ent1.entity_id) - assert light.is_on(self.hass, ent2.entity_id) - assert light.is_on(self.hass, ent3.entity_id) - - # turn off all lights - common.turn_off(self.hass) - - self.hass.block_till_done() - - assert not light.is_on(self.hass, ent1.entity_id) - assert not light.is_on(self.hass, ent2.entity_id) - assert not light.is_on(self.hass, ent3.entity_id) - - # turn off all lights by setting brightness to 0 - common.turn_on(self.hass) - - self.hass.block_till_done() - - common.turn_on(self.hass, brightness=0) - - self.hass.block_till_done() - - assert not light.is_on(self.hass, ent1.entity_id) - assert not light.is_on(self.hass, ent2.entity_id) - assert not light.is_on(self.hass, ent3.entity_id) - - # toggle all lights - common.toggle(self.hass) - - self.hass.block_till_done() - - assert light.is_on(self.hass, ent1.entity_id) - assert light.is_on(self.hass, ent2.entity_id) - assert light.is_on(self.hass, ent3.entity_id) - - # toggle all lights - common.toggle(self.hass) - - self.hass.block_till_done() - - assert not light.is_on(self.hass, ent1.entity_id) - assert not light.is_on(self.hass, ent2.entity_id) - assert not light.is_on(self.hass, ent3.entity_id) - - # Ensure all attributes process correctly - common.turn_on( - self.hass, ent1.entity_id, transition=10, brightness=20, color_name="blue" - ) - common.turn_on( - self.hass, ent2.entity_id, rgb_color=(255, 255, 255), white_value=255 - ) - common.turn_on(self.hass, ent3.entity_id, xy_color=(0.4, 0.6)) - - self.hass.block_till_done() - - _, data = ent1.last_call("turn_on") - assert { - light.ATTR_TRANSITION: 10, - light.ATTR_BRIGHTNESS: 20, - light.ATTR_HS_COLOR: (240, 100), - } == data - - _, data = ent2.last_call("turn_on") - assert {light.ATTR_HS_COLOR: (0, 0), light.ATTR_WHITE_VALUE: 255} == data - - _, data = ent3.last_call("turn_on") - assert {light.ATTR_HS_COLOR: (71.059, 100)} == data - - # Ensure attributes are filtered when light is turned off - common.turn_on( - self.hass, ent1.entity_id, transition=10, brightness=0, color_name="blue" - ) - common.turn_on( - self.hass, - ent2.entity_id, - brightness=0, - rgb_color=(255, 255, 255), - white_value=0, - ) - common.turn_on(self.hass, ent3.entity_id, brightness=0, xy_color=(0.4, 0.6)) - - self.hass.block_till_done() - - assert not light.is_on(self.hass, ent1.entity_id) - assert not light.is_on(self.hass, ent2.entity_id) - assert not light.is_on(self.hass, ent3.entity_id) - - _, data = ent1.last_call("turn_off") - assert {light.ATTR_TRANSITION: 10} == data - - _, data = ent2.last_call("turn_off") - assert {} == data - - _, data = ent3.last_call("turn_off") - assert {} == data - - # One of the light profiles - prof_name, prof_h, prof_s, prof_bri, prof_t = "relax", 35.932, 69.412, 144, 0 - - # Test light profiles - common.turn_on(self.hass, ent1.entity_id, profile=prof_name) - # Specify a profile and a brightness attribute to overwrite it - common.turn_on( - self.hass, ent2.entity_id, profile=prof_name, brightness=100, transition=1 - ) - - self.hass.block_till_done() - - _, data = ent1.last_call("turn_on") - assert { - light.ATTR_BRIGHTNESS: prof_bri, - light.ATTR_HS_COLOR: (prof_h, prof_s), - light.ATTR_TRANSITION: prof_t, - } == data - - _, data = ent2.last_call("turn_on") - assert { - light.ATTR_BRIGHTNESS: 100, - light.ATTR_HS_COLOR: (prof_h, prof_s), - light.ATTR_TRANSITION: 1, - } == data - - # Test toggle with parameters - common.toggle(self.hass, ent3.entity_id, profile=prof_name, brightness_pct=100) - self.hass.block_till_done() - _, data = ent3.last_call("turn_on") - assert { - light.ATTR_BRIGHTNESS: 255, - light.ATTR_HS_COLOR: (prof_h, prof_s), - light.ATTR_TRANSITION: prof_t, - } == data - - # Test bad data - common.turn_on(self.hass) - common.turn_on(self.hass, ent1.entity_id, profile="nonexisting") - common.turn_on(self.hass, ent2.entity_id, xy_color=["bla-di-bla", 5]) - common.turn_on(self.hass, ent3.entity_id, rgb_color=[255, None, 2]) - - self.hass.block_till_done() - - _, data = ent1.last_call("turn_on") - assert {} == data - - _, data = ent2.last_call("turn_on") - assert {} == data - - _, data = ent3.last_call("turn_on") - assert {} == data - - # faulty attributes will not trigger a service call - common.turn_on( - self.hass, ent1.entity_id, profile=prof_name, brightness="bright" - ) - common.turn_on(self.hass, ent1.entity_id, rgb_color="yellowish") - common.turn_on(self.hass, ent2.entity_id, white_value="high") - - self.hass.block_till_done() - - _, data = ent1.last_call("turn_on") - assert {} == data - - _, data = ent2.last_call("turn_on") - assert {} == data - - def test_broken_light_profiles(self): - """Test light profiles.""" - platform = getattr(self.hass.components, "test.light") - platform.init() - - user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE) - - # Setup a wrong light file - with open(user_light_file, "w") as user_file: - user_file.write("id,x,y,brightness,transition\n") - user_file.write("I,WILL,NOT,WORK,EVER\n") - - assert not setup_component( - self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} - ) - - def test_light_profiles(self): - """Test light profiles.""" - platform = getattr(self.hass.components, "test.light") - platform.init() - - user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE) - - with open(user_light_file, "w") as user_file: - user_file.write("id,x,y,brightness\n") - user_file.write("test,.4,.6,100\n") - user_file.write("test_off,0,0,0\n") - - assert setup_component( - self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} - ) - self.hass.block_till_done() - - ent1, _, _ = platform.ENTITIES - - common.turn_on(self.hass, ent1.entity_id, profile="test") - - self.hass.block_till_done() - - _, data = ent1.last_call("turn_on") - - assert light.is_on(self.hass, ent1.entity_id) - assert { - light.ATTR_HS_COLOR: (71.059, 100), - light.ATTR_BRIGHTNESS: 100, - light.ATTR_TRANSITION: 0, - } == data - - common.turn_on(self.hass, ent1.entity_id, profile="test_off") - - self.hass.block_till_done() - - _, data = ent1.last_call("turn_off") - - assert not light.is_on(self.hass, ent1.entity_id) - assert {light.ATTR_TRANSITION: 0} == data - - def test_light_profiles_with_transition(self): - """Test light profiles with transition.""" - platform = getattr(self.hass.components, "test.light") - platform.init() - - user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE) - - with open(user_light_file, "w") as user_file: - user_file.write("id,x,y,brightness,transition\n") - user_file.write("test,.4,.6,100,2\n") - user_file.write("test_off,0,0,0,0\n") - - assert setup_component( - self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} - ) - self.hass.block_till_done() - - ent1, _, _ = platform.ENTITIES - - common.turn_on(self.hass, ent1.entity_id, profile="test") - - self.hass.block_till_done() - - _, data = ent1.last_call("turn_on") - - assert light.is_on(self.hass, ent1.entity_id) - assert { - light.ATTR_HS_COLOR: (71.059, 100), - light.ATTR_BRIGHTNESS: 100, - light.ATTR_TRANSITION: 2, - } == data - - common.turn_on(self.hass, ent1.entity_id, profile="test_off") - - self.hass.block_till_done() - - _, data = ent1.last_call("turn_off") - - assert not light.is_on(self.hass, ent1.entity_id) - assert {light.ATTR_TRANSITION: 0} == data - - def test_default_profiles_group(self): - """Test default turn-on light profile for all lights.""" - platform = getattr(self.hass.components, "test.light") - platform.init() - - user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE) - real_isfile = os.path.isfile - real_open = open - - def _mock_isfile(path): - if path == user_light_file: - return True - return real_isfile(path) - - def _mock_open(path, *args, **kwargs): - if path == user_light_file: - return StringIO(profile_data) - return real_open(path, *args, **kwargs) - - profile_data = ( - "id,x,y,brightness,transition\ngroup.all_lights.default,.4,.6,99,2\n" - ) - with mock.patch("os.path.isfile", side_effect=_mock_isfile), mock.patch( - "builtins.open", side_effect=_mock_open - ), mock_storage(): - assert setup_component( - self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} - ) - self.hass.block_till_done() - - ent, _, _ = platform.ENTITIES - common.turn_on(self.hass, ent.entity_id) - self.hass.block_till_done() - _, data = ent.last_call("turn_on") - assert { - light.ATTR_HS_COLOR: (71.059, 100), - light.ATTR_BRIGHTNESS: 99, - light.ATTR_TRANSITION: 2, - } == data - - def test_default_profiles_light(self): - """Test default turn-on light profile for a specific light.""" - platform = getattr(self.hass.components, "test.light") - platform.init() - - user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE) - real_isfile = os.path.isfile - real_open = open - - def _mock_isfile(path): - if path == user_light_file: - return True - return real_isfile(path) - - def _mock_open(path, *args, **kwargs): - if path == user_light_file: - return StringIO(profile_data) - return real_open(path, *args, **kwargs) - - profile_data = ( - "id,x,y,brightness,transition\n" - + "group.all_lights.default,.3,.5,200,0\n" - + "light.ceiling_2.default,.6,.6,100,3\n" - ) - with mock.patch("os.path.isfile", side_effect=_mock_isfile), mock.patch( - "builtins.open", side_effect=_mock_open - ), mock_storage(): - assert setup_component( - self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} - ) - self.hass.block_till_done() - - dev = next( - filter(lambda x: x.entity_id == "light.ceiling_2", platform.ENTITIES) - ) - common.turn_on(self.hass, dev.entity_id) - self.hass.block_till_done() - _, data = dev.last_call("turn_on") - assert { - light.ATTR_HS_COLOR: (50.353, 100), - light.ATTR_BRIGHTNESS: 100, - light.ATTR_TRANSITION: 3, - } == data + await hass.async_block_till_done() + + dev = next(filter(lambda x: x.entity_id == "light.ceiling_2", platform.ENTITIES)) + await common.async_turn_on(hass, dev.entity_id) + await hass.async_block_till_done() + _, data = dev.last_call("turn_on") + assert data == { + light.ATTR_HS_COLOR: (50.353, 100), + light.ATTR_BRIGHTNESS: 100, + light.ATTR_TRANSITION: 3, + } async def test_light_context(hass, hass_admin_user): From 203c556ba34310322324f512d7464b5591f978e7 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 25 Sep 2020 22:49:28 +0200 Subject: [PATCH 354/514] Improve tracking of existing entities in deconz (#40265) * Store all entities in dict * Use stored unique id to select if to create entity or not * Remove unnecessary init * Change so same physical sensor doesnt try to create multiple battery sensors Change so groups get created properly * Add controls in tests that entities are logged correctly --- .../components/deconz/binary_sensor.py | 10 +++-- homeassistant/components/deconz/climate.py | 11 +++-- homeassistant/components/deconz/cover.py | 11 ++++- .../components/deconz/deconz_device.py | 19 ++++----- .../components/deconz/deconz_event.py | 19 ++++++--- homeassistant/components/deconz/gateway.py | 2 + homeassistant/components/deconz/light.py | 29 +++++++++---- homeassistant/components/deconz/sensor.py | 42 ++++++++++--------- homeassistant/components/deconz/switch.py | 18 ++++++-- tests/components/deconz/test_binary_sensor.py | 8 ++++ tests/components/deconz/test_climate.py | 8 ++++ tests/components/deconz/test_cover.py | 3 ++ tests/components/deconz/test_deconz_event.py | 4 +- tests/components/deconz/test_light.py | 6 +++ tests/components/deconz/test_sensor.py | 12 ++++++ tests/components/deconz/test_switch.py | 3 ++ 16 files changed, 147 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index c8917934dd7..eefd66778ad 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE, DEVICE_CLASS_VIBRATION, + DOMAIN, BinarySensorEntity, ) from homeassistant.const import ATTR_TEMPERATURE @@ -39,17 +40,18 @@ 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 the deCONZ binary sensor.""" gateway = get_gateway_from_config_entry(hass, config_entry) + gateway.entities[DOMAIN] = set() @callback - def async_add_sensor(sensors, new=True): + def async_add_sensor(sensors): """Add binary sensor from deCONZ.""" entities = [] for sensor in sensors: if ( - new - and sensor.BINARY + sensor.BINARY + and sensor.uniqueid not in gateway.entities[DOMAIN] and ( gateway.option_allow_clip_sensor or not sensor.type.startswith("CLIP") @@ -73,6 +75,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): """Representation of a deCONZ binary sensor.""" + TYPE = DOMAIN + @callback def async_update_callback(self, force_update=False, ignore_update=False): """Update the sensor's state.""" diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 424693505ca..10ea7173f8a 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -1,7 +1,7 @@ """Support for deCONZ climate devices.""" from pydeconz.sensor import Thermostat -from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate import DOMAIN, ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_HEAT, @@ -29,17 +29,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): Thermostats are based on the same device class as sensors in deCONZ. """ gateway = get_gateway_from_config_entry(hass, config_entry) + gateway.entities[DOMAIN] = set() @callback - def async_add_climate(sensors, new=True): + def async_add_climate(sensors): """Add climate devices from deCONZ.""" entities = [] for sensor in sensors: if ( - new - and sensor.type in Thermostat.ZHATYPE + sensor.type in Thermostat.ZHATYPE + and sensor.uniqueid not in gateway.entities[DOMAIN] and ( gateway.option_allow_clip_sensor or not sensor.type.startswith("CLIP") @@ -61,6 +62,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DeconzThermostat(DeconzDevice, ClimateEntity): """Representation of a deCONZ thermostat.""" + TYPE = DOMAIN + @property def supported_features(self): """Return the list of supported features.""" diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 996727d366b..7bcd821a344 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -2,6 +2,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, DEVICE_CLASS_WINDOW, + DOMAIN, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, @@ -23,9 +24,10 @@ 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 covers for deCONZ component. - Covers are based on same device class as lights in deCONZ. + Covers are based on the same device class as lights in deCONZ. """ gateway = get_gateway_from_config_entry(hass, config_entry) + gateway.entities[DOMAIN] = set() @callback def async_add_cover(lights): @@ -33,7 +35,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] for light in lights: - if light.type in COVER_TYPES: + if ( + light.type in COVER_TYPES + and light.uniqueid not in gateway.entities[DOMAIN] + ): entities.append(DeconzCover(light, gateway)) async_add_entities(entities, True) @@ -50,6 +55,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DeconzCover(DeconzDevice, CoverEntity): """Representation of a deCONZ cover.""" + TYPE = DOMAIN + def __init__(self, device, gateway): """Set up cover device.""" super().__init__(device, gateway) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index b77014cc34b..56dc00dab58 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -10,11 +10,17 @@ from .const import DOMAIN as DECONZ_DOMAIN class DeconzBase: """Common base for deconz entities and events.""" + TYPE = "" + def __init__(self, device, gateway): """Set up device and add update callback to get data from websocket.""" self._device = device self.gateway = gateway - self.listeners = [] + self.gateway.entities[self.TYPE].add(self.unique_id) + + async def async_will_remove_from_hass(self) -> None: + """Remove unique id.""" + self.gateway.entities[self.TYPE].remove(self.unique_id) @property def unique_id(self): @@ -51,12 +57,6 @@ class DeconzBase: class DeconzDevice(DeconzBase, Entity): """Representation of a deCONZ device.""" - def __init__(self, device, gateway): - """Set up device and add update callback to get data from websocket.""" - super().__init__(device, gateway) - - self.unsub_dispatcher = None - @property def entity_registry_enabled_default(self): """Return if the entity should be enabled when first added to the entity registry. @@ -72,7 +72,7 @@ class DeconzDevice(DeconzBase, Entity): """Subscribe to device events.""" self._device.register_callback(self.async_update_callback) self.gateway.deconz_ids[self.entity_id] = self._device.deconz_id - self.listeners.append( + self.async_on_remove( async_dispatcher_connect( self.hass, self.gateway.signal_reachable, self.async_update_callback ) @@ -83,8 +83,7 @@ class DeconzDevice(DeconzBase, Entity): self._device.remove_callback(self.async_update_callback) if self.entity_id in self.gateway.deconz_ids: del self.gateway.deconz_ids[self.entity_id] - for unsub_dispatcher in self.listeners: - unsub_dispatcher() + await super().async_will_remove_from_hass() @callback def async_update_callback(self, force_update=False, ignore_update=False): diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index bde2db90663..4b7027b8b36 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -11,19 +11,25 @@ from .deconz_device import DeconzBase CONF_DECONZ_EVENT = "deconz_event" +EVENT = "Event" + async def async_setup_events(gateway) -> None: """Set up the deCONZ events.""" + gateway.entities[EVENT] = set() @callback - def async_add_sensor(sensors, new=True): + def async_add_sensor(sensors): """Create DeconzEvent.""" for sensor in sensors: if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): continue - if not new or sensor.type not in Switch.ZHATYPE: + if ( + sensor.type not in Switch.ZHATYPE + or sensor.uniqueid in gateway.entities[EVENT] + ): continue new_event = DeconzEvent(sensor, gateway) @@ -44,7 +50,7 @@ async def async_setup_events(gateway) -> None: async def async_unload_events(gateway) -> None: """Unload all deCONZ events.""" for event in gateway.events: - event.async_will_remove_from_hass() + await event.async_will_remove_from_hass() gateway.events.clear() @@ -56,6 +62,8 @@ class DeconzEvent(DeconzBase): instead of a sensor entity in hass. """ + TYPE = EVENT + def __init__(self, device, gateway): """Register callback that will be used for signals.""" super().__init__(device, gateway) @@ -71,11 +79,10 @@ class DeconzEvent(DeconzBase): """Return Event device.""" return self._device - @callback - def async_will_remove_from_hass(self) -> None: + async def async_will_remove_from_hass(self) -> None: """Disconnect event object when removed.""" self._device.remove_callback(self.async_update_callback) - self._device = None + await super().async_will_remove_from_hass() @callback def async_update_callback(self, force_update=False, ignore_update=False): diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index d3a781302e5..ceed473a383 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -49,6 +49,8 @@ class DeconzGateway: self.events = [] self.listeners = [] + self.entities = {} + self._current_option_allow_clip_sensor = self.option_allow_clip_sensor self._current_option_allow_deconz_groups = self.option_allow_deconz_groups diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index fc0c01b30df..135a01cd0df 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -6,6 +6,7 @@ from homeassistant.components.light import ( ATTR_FLASH, ATTR_HS_COLOR, ATTR_TRANSITION, + DOMAIN, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, @@ -40,6 +41,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ lights and groups from a config entry.""" gateway = get_gateway_from_config_entry(hass, config_entry) + gateway.entities[DOMAIN] = set() @callback def async_add_light(lights): @@ -47,7 +49,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] for light in lights: - if light.type not in COVER_TYPES + SWITCH_TYPES: + if ( + light.type not in COVER_TYPES + SWITCH_TYPES + and light.uniqueid not in gateway.entities[DOMAIN] + ): entities.append(DeconzLight(light, gateway)) async_add_entities(entities, True) @@ -67,8 +72,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] for group in groups: - if group.lights: - entities.append(DeconzGroup(group, gateway)) + if not group.lights: + continue + + known_groups = list(gateway.entities[DOMAIN]) + new_group = DeconzGroup(group, gateway) + if new_group.unique_id not in known_groups: + entities.append(new_group) async_add_entities(entities, True) @@ -85,6 +95,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DeconzBaseLight(DeconzDevice, LightEntity): """Representation of a deCONZ light.""" + TYPE = DOMAIN + def __init__(self, device, gateway): """Set up light.""" super().__init__(device, gateway) @@ -223,14 +235,13 @@ class DeconzGroup(DeconzBaseLight): def __init__(self, device, gateway): """Set up group and create an unique id.""" + group_id_base = gateway.config_entry.unique_id + if CONF_GROUP_ID_BASE in gateway.config_entry.data: + group_id_base = gateway.config_entry.data[CONF_GROUP_ID_BASE] + self._unique_id = f"{group_id_base}-{device.deconz_id}" + super().__init__(device, gateway) - group_id_base = self.gateway.config_entry.unique_id - if CONF_GROUP_ID_BASE in self.gateway.config_entry.data: - group_id_base = self.gateway.config_entry.data[CONF_GROUP_ID_BASE] - - self._unique_id = f"{group_id_base}-{self._device.deconz_id}" - @property def unique_id(self): """Return a unique identifier for this device.""" diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 72465282421..d7d7b777b41 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -12,6 +12,7 @@ from pydeconz.sensor import ( Thermostat, ) +from homeassistant.components.sensor import DOMAIN from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_VOLTAGE, @@ -74,43 +75,43 @@ 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 the deCONZ sensors.""" gateway = get_gateway_from_config_entry(hass, config_entry) + gateway.entities[DOMAIN] = set() - batteries = set() battery_handler = DeconzBatteryHandler(gateway) @callback - def async_add_sensor(sensors, new=True): + def async_add_sensor(sensors): """Add sensors from deCONZ. - Create DeconzSensor if not a ZHAType and not a binary sensor. Create DeconzBattery if sensor has a battery attribute. - If new is false it means an existing sensor has got a battery state reported. + Create DeconzSensor if not a battery, switch or thermostat and not a binary sensor. """ entities = [] for sensor in sensors: - if ( - new - and sensor.BINARY is False - and sensor.type - not in Battery.ZHATYPE + Switch.ZHATYPE + Thermostat.ZHATYPE - and ( - gateway.option_allow_clip_sensor - or not sensor.type.startswith("CLIP") - ) - ): - entities.append(DeconzSensor(sensor, gateway)) + if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): + continue if sensor.battery is not None: + battery_handler.remove_tracker(sensor) + + known_batteries = list(gateway.entities[DOMAIN]) new_battery = DeconzBattery(sensor, gateway) - if new_battery.unique_id not in batteries: - batteries.add(new_battery.unique_id) + if new_battery.unique_id not in known_batteries: entities.append(new_battery) - battery_handler.remove_tracker(sensor) + else: battery_handler.create_tracker(sensor) + if ( + not sensor.BINARY + and sensor.type + not in Battery.ZHATYPE + Switch.ZHATYPE + Thermostat.ZHATYPE + and sensor.uniqueid not in gateway.entities[DOMAIN] + ): + entities.append(DeconzSensor(sensor, gateway)) + async_add_entities(entities, True) gateway.listeners.append( @@ -127,6 +128,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DeconzSensor(DeconzDevice): """Representation of a deCONZ sensor.""" + TYPE = DOMAIN + @callback def async_update_callback(self, force_update=False, ignore_update=False): """Update the sensor's state.""" @@ -192,6 +195,8 @@ class DeconzSensor(DeconzDevice): class DeconzBattery(DeconzDevice): """Battery class for when a device is only represented as an event.""" + TYPE = DOMAIN + @callback def async_update_callback(self, force_update=False, ignore_update=False): """Update the battery's state, if needed.""" @@ -264,7 +269,6 @@ class DeconzSensorStateTracker: self.gateway.hass, self.gateway.async_signal_new_device(NEW_SENSOR), [self.sensor], - False, ) diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index d7b6b55fbb8..dacae4d4a56 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -1,5 +1,5 @@ """Support for deCONZ switches.""" -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -15,9 +15,10 @@ 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 switches for deCONZ component. - Switches are based same device class as lights in deCONZ. + Switches are based on the same device class as lights in deCONZ. """ gateway = get_gateway_from_config_entry(hass, config_entry) + gateway.entities[DOMAIN] = set() @callback def async_add_switch(lights): @@ -26,10 +27,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for light in lights: - if light.type in POWER_PLUGS: + if ( + light.type in POWER_PLUGS + and light.uniqueid not in gateway.entities[DOMAIN] + ): entities.append(DeconzPowerPlug(light, gateway)) - elif light.type in SIRENS: + elif ( + light.type in SIRENS and light.uniqueid not in gateway.entities[DOMAIN] + ): entities.append(DeconzSiren(light, gateway)) async_add_entities(entities, True) @@ -46,6 +52,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DeconzPowerPlug(DeconzDevice, SwitchEntity): """Representation of a deCONZ power plug.""" + TYPE = DOMAIN + @property def is_on(self): """Return true if switch is on.""" @@ -65,6 +73,8 @@ class DeconzPowerPlug(DeconzDevice, SwitchEntity): class DeconzSiren(DeconzDevice, SwitchEntity): """Representation of a deCONZ siren.""" + TYPE = DOMAIN + @property def is_on(self): """Return true if switch is on.""" diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 30f7251e067..1ced3b8ed2d 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -67,6 +67,7 @@ async def test_no_binary_sensors(hass): """Test that no sensors in deconz results in no sensor entities.""" gateway = await setup_deconz_integration(hass) assert len(gateway.deconz_ids) == 0 + assert len(gateway.entities[binary_sensor.DOMAIN]) == 0 assert len(hass.states.async_all()) == 0 @@ -80,6 +81,7 @@ async def test_binary_sensors(hass): assert "binary_sensor.clip_presence_sensor" not in gateway.deconz_ids assert "binary_sensor.vibration_sensor" in gateway.deconz_ids assert len(hass.states.async_all()) == 3 + assert len(gateway.entities[binary_sensor.DOMAIN]) == 2 presence_sensor = hass.states.get("binary_sensor.presence_sensor") assert presence_sensor.state == "off" @@ -111,6 +113,7 @@ async def test_binary_sensors(hass): await gateway.async_reset() assert len(hass.states.async_all()) == 0 + assert len(gateway.entities[binary_sensor.DOMAIN]) == 0 async def test_allow_clip_sensor(hass): @@ -127,6 +130,7 @@ async def test_allow_clip_sensor(hass): assert "binary_sensor.clip_presence_sensor" in gateway.deconz_ids assert "binary_sensor.vibration_sensor" in gateway.deconz_ids assert len(hass.states.async_all()) == 4 + assert len(gateway.entities[binary_sensor.DOMAIN]) == 3 presence_sensor = hass.states.get("binary_sensor.presence_sensor") assert presence_sensor.state == "off" @@ -150,6 +154,7 @@ async def test_allow_clip_sensor(hass): assert "binary_sensor.clip_presence_sensor" not in gateway.deconz_ids assert "binary_sensor.vibration_sensor" in gateway.deconz_ids assert len(hass.states.async_all()) == 3 + assert len(gateway.entities[binary_sensor.DOMAIN]) == 2 hass.config_entries.async_update_entry( gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True} @@ -161,12 +166,14 @@ async def test_allow_clip_sensor(hass): assert "binary_sensor.clip_presence_sensor" in gateway.deconz_ids assert "binary_sensor.vibration_sensor" in gateway.deconz_ids assert len(hass.states.async_all()) == 4 + assert len(gateway.entities[binary_sensor.DOMAIN]) == 3 async def test_add_new_binary_sensor(hass): """Test that adding a new binary sensor works.""" gateway = await setup_deconz_integration(hass) assert len(gateway.deconz_ids) == 0 + assert len(gateway.entities[binary_sensor.DOMAIN]) == 0 state_added_event = { "t": "event", @@ -182,3 +189,4 @@ async def test_add_new_binary_sensor(hass): presence_sensor = hass.states.get("binary_sensor.presence_sensor") assert presence_sensor.state == "off" + assert len(gateway.entities[binary_sensor.DOMAIN]) == 1 diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index ddc89295cba..361e182fe98 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -59,6 +59,7 @@ async def test_no_sensors(hass): gateway = await setup_deconz_integration(hass) assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 + assert len(gateway.entities[climate.DOMAIN]) == 0 async def test_climate_devices(hass): @@ -72,6 +73,7 @@ async def test_climate_devices(hass): assert "climate.presence_sensor" not in gateway.deconz_ids assert "climate.clip_thermostat" not in gateway.deconz_ids assert len(hass.states.async_all()) == 3 + assert len(gateway.entities[climate.DOMAIN]) == 1 thermostat = hass.states.get("climate.thermostat") assert thermostat.state == "auto" @@ -181,6 +183,7 @@ async def test_climate_devices(hass): await gateway.async_reset() assert len(hass.states.async_all()) == 0 + assert len(gateway.entities[climate.DOMAIN]) == 0 async def test_clip_climate_device(hass): @@ -198,6 +201,7 @@ async def test_clip_climate_device(hass): assert "climate.presence_sensor" not in gateway.deconz_ids assert "climate.clip_thermostat" in gateway.deconz_ids assert len(hass.states.async_all()) == 4 + assert len(gateway.entities[climate.DOMAIN]) == 2 thermostat = hass.states.get("climate.thermostat") assert thermostat.state == "auto" @@ -225,6 +229,7 @@ async def test_clip_climate_device(hass): assert "climate.presence_sensor" not in gateway.deconz_ids assert "climate.clip_thermostat" not in gateway.deconz_ids assert len(hass.states.async_all()) == 3 + assert len(gateway.entities[climate.DOMAIN]) == 1 hass.config_entries.async_update_entry( gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True} @@ -237,6 +242,7 @@ async def test_clip_climate_device(hass): assert "climate.presence_sensor" not in gateway.deconz_ids assert "climate.clip_thermostat" in gateway.deconz_ids assert len(hass.states.async_all()) == 4 + assert len(gateway.entities[climate.DOMAIN]) == 2 async def test_verify_state_update(hass): @@ -268,6 +274,7 @@ async def test_add_new_climate_device(hass): """Test that adding a new climate device works.""" gateway = await setup_deconz_integration(hass) assert len(gateway.deconz_ids) == 0 + assert len(gateway.entities[climate.DOMAIN]) == 0 state_added_event = { "t": "event", @@ -283,3 +290,4 @@ async def test_add_new_climate_device(hass): thermostat = hass.states.get("climate.thermostat") assert thermostat.state == "auto" + assert len(gateway.entities[climate.DOMAIN]) == 1 diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 095ae7e4bc5..28a6f150fc4 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -67,6 +67,7 @@ async def test_no_covers(hass): """Test that no cover entities are created.""" gateway = await setup_deconz_integration(hass) assert len(gateway.deconz_ids) == 0 + assert len(gateway.entities[cover.DOMAIN]) == 0 assert len(hass.states.async_all()) == 0 @@ -81,6 +82,7 @@ async def test_cover(hass): assert "cover.deconz_old_brightness_cover" in gateway.deconz_ids assert "cover.window_covering_controller" in gateway.deconz_ids assert len(hass.states.async_all()) == 5 + assert len(gateway.entities[cover.DOMAIN]) == 4 level_controllable_cover = hass.states.get("cover.level_controllable_cover") assert level_controllable_cover.state == "open" @@ -158,3 +160,4 @@ async def test_cover(hass): await gateway.async_reset() assert len(hass.states.async_all()) == 0 + assert len(gateway.entities[cover.DOMAIN]) == 0 diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 525821e389f..717c783d0ed 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -1,7 +1,7 @@ """Test deCONZ remote events.""" from copy import deepcopy -from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT +from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT, EVENT from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -62,6 +62,7 @@ async def test_deconz_events(hass): assert "sensor.switch_2_battery_level" in gateway.deconz_ids assert len(hass.states.async_all()) == 3 assert len(gateway.events) == 5 + assert len(gateway.entities[EVENT]) == 5 switch_1 = hass.states.get("sensor.switch_1") assert switch_1 is None @@ -127,3 +128,4 @@ async def test_deconz_events(hass): assert len(hass.states.async_all()) == 0 assert len(gateway.events) == 0 + assert len(gateway.entities[EVENT]) == 0 diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 4e9f6b3d512..dac1b071e93 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -95,6 +95,7 @@ async def test_no_lights_or_groups(hass): gateway = await setup_deconz_integration(hass) assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 + assert len(gateway.entities[light.DOMAIN]) == 0 async def test_lights_and_groups(hass): @@ -111,6 +112,7 @@ async def test_lights_and_groups(hass): assert "light.on_off_light" in gateway.deconz_ids assert len(hass.states.async_all()) == 6 + assert len(gateway.entities[light.DOMAIN]) == 5 rgb_light = hass.states.get("light.rgb_light") assert rgb_light.state == "on" @@ -256,6 +258,7 @@ async def test_lights_and_groups(hass): await gateway.async_reset() assert len(hass.states.async_all()) == 0 + assert len(gateway.entities[light.DOMAIN]) == 0 async def test_disable_light_groups(hass): @@ -275,6 +278,7 @@ async def test_disable_light_groups(hass): assert "light.on_off_switch" not in gateway.deconz_ids # 3 entities assert len(hass.states.async_all()) == 5 + assert len(gateway.entities[light.DOMAIN]) == 4 rgb_light = hass.states.get("light.rgb_light") assert rgb_light is not None @@ -300,6 +304,7 @@ async def test_disable_light_groups(hass): assert "light.on_off_switch" not in gateway.deconz_ids # 3 entities assert len(hass.states.async_all()) == 6 + assert len(gateway.entities[light.DOMAIN]) == 5 hass.config_entries.async_update_entry( gateway.config_entry, options={deconz.gateway.CONF_ALLOW_DECONZ_GROUPS: False} @@ -313,3 +318,4 @@ async def test_disable_light_groups(hass): assert "light.on_off_switch" not in gateway.deconz_ids # 3 entities assert len(hass.states.async_all()) == 5 + assert len(gateway.entities[light.DOMAIN]) == 4 diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 9d87c7b91cb..8c7829586e4 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -2,6 +2,7 @@ from copy import deepcopy from homeassistant.components import deconz +from homeassistant.components.deconz.deconz_event import EVENT import homeassistant.components.sensor as sensor from homeassistant.const import ( DEVICE_CLASS_BATTERY, @@ -96,6 +97,7 @@ async def test_no_sensors(hass): gateway = await setup_deconz_integration(hass) assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 + assert len(gateway.entities[sensor.DOMAIN]) == 0 async def test_sensors(hass): @@ -114,6 +116,7 @@ async def test_sensors(hass): assert "sensor.consumption_sensor" in gateway.deconz_ids assert "sensor.clip_light_level_sensor" not in gateway.deconz_ids assert len(hass.states.async_all()) == 5 + assert len(gateway.entities[sensor.DOMAIN]) == 5 light_level_sensor = hass.states.get("sensor.light_level_sensor") assert light_level_sensor.state == "999.8" @@ -174,6 +177,8 @@ async def test_sensors(hass): await gateway.async_reset() assert len(hass.states.async_all()) == 0 + # Daylight sensor from deCONZ is added to set but is disabled by default + assert len(gateway.entities[sensor.DOMAIN]) == 1 async def test_allow_clip_sensors(hass): @@ -196,6 +201,7 @@ async def test_allow_clip_sensors(hass): assert "sensor.consumption_sensor" in gateway.deconz_ids assert "sensor.clip_light_level_sensor" in gateway.deconz_ids assert len(hass.states.async_all()) == 6 + assert len(gateway.entities[sensor.DOMAIN]) == 6 light_level_sensor = hass.states.get("sensor.light_level_sensor") assert light_level_sensor.state == "999.8" @@ -243,6 +249,7 @@ async def test_allow_clip_sensors(hass): assert "sensor.consumption_sensor" in gateway.deconz_ids assert "sensor.clip_light_level_sensor" not in gateway.deconz_ids assert len(hass.states.async_all()) == 5 + assert len(gateway.entities[sensor.DOMAIN]) == 5 hass.config_entries.async_update_entry( gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True} @@ -260,6 +267,7 @@ async def test_allow_clip_sensors(hass): assert "sensor.consumption_sensor" in gateway.deconz_ids assert "sensor.clip_light_level_sensor" in gateway.deconz_ids assert len(hass.states.async_all()) == 6 + assert len(gateway.entities[sensor.DOMAIN]) == 6 async def test_add_new_sensor(hass): @@ -292,6 +300,8 @@ async def test_add_battery_later(hass): assert len(gateway.deconz_ids) == 0 assert len(gateway.events) == 1 assert len(remote._callbacks) == 2 + assert len(gateway.entities[sensor.DOMAIN]) == 0 + assert len(gateway.entities[EVENT]) == 1 remote.update({"config": {"battery": 50}}) await hass.async_block_till_done() @@ -302,3 +312,5 @@ async def test_add_battery_later(hass): battery_sensor = hass.states.get("sensor.switch_1_battery_level") assert battery_sensor is not None + assert len(gateway.entities[sensor.DOMAIN]) == 1 + assert len(gateway.entities[EVENT]) == 1 diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index b441868859b..7b1e0e6121c 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -64,6 +64,7 @@ async def test_no_switches(hass): gateway = await setup_deconz_integration(hass) assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 + assert len(gateway.entities[switch.DOMAIN]) == 0 async def test_switches(hass): @@ -77,6 +78,7 @@ async def test_switches(hass): assert "switch.unsupported_switch" not in gateway.deconz_ids assert "switch.on_off_relay" in gateway.deconz_ids assert len(hass.states.async_all()) == 5 + assert len(gateway.entities[switch.DOMAIN]) == 4 on_off_switch = hass.states.get("switch.on_off_switch") assert on_off_switch.state == "on" @@ -173,3 +175,4 @@ async def test_switches(hass): await gateway.async_reset() assert len(hass.states.async_all()) == 0 + assert len(gateway.entities[switch.DOMAIN]) == 0 From f09f7feb940cebf82920e19bf1e2ab431947745a Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 25 Sep 2020 18:36:55 -0400 Subject: [PATCH 355/514] Update schema for zha.set_zigbee_cluster_attribute service (#40600) --- homeassistant/components/zha/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index bdfeb7815c5..73613c04371 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -99,7 +99,7 @@ SERVICE_SCHEMAS = { vol.Required(ATTR_ENDPOINT_ID): cv.positive_int, vol.Required(ATTR_CLUSTER_ID): cv.positive_int, vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, - vol.Required(ATTR_ATTRIBUTE): cv.positive_int, + vol.Required(ATTR_ATTRIBUTE): vol.Any(int, cv.boolean, cv.string), vol.Required(ATTR_VALUE): cv.string, vol.Optional(ATTR_MANUFACTURER): cv.positive_int, } From c3afe39ea0e553ce3668c239dbfd89cc9fffadf3 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 26 Sep 2020 00:05:34 +0000 Subject: [PATCH 356/514] [ci skip] Translation update --- .../alarm_control_panel/translations/nl.json | 2 +- .../alarmdecoder/translations/lb.json | 44 +++++++++++++++++++ .../components/almond/translations/lb.json | 3 +- .../components/august/translations/lb.json | 3 +- .../components/canary/translations/lb.json | 28 +++++++++++- .../home_connect/translations/lb.json | 3 +- .../homekit_controller/translations/lb.json | 8 +++- .../components/insteon/translations/lb.json | 24 ++++++++++ .../components/kodi/translations/lb.json | 6 +++ .../components/netatmo/translations/lb.json | 1 + .../components/nzbget/translations/lb.json | 35 +++++++++++++++ .../components/omnilogic/translations/en.json | 12 ++--- .../components/omnilogic/translations/ru.json | 30 +++++++++++++ .../openweathermap/translations/lb.json | 16 ++++++- .../components/plugwise/translations/lb.json | 12 ++++- .../progettihwsw/translations/lb.json | 25 +++++++++++ .../components/remote/translations/lb.json | 2 + .../components/risco/translations/lb.json | 15 +++++++ .../components/rpi_power/translations/lb.json | 7 +++ .../components/sharkiq/translations/lb.json | 25 +++++++++++ .../components/shelly/translations/lb.json | 7 +++ .../components/smappee/translations/lb.json | 3 +- .../components/somfy/translations/lb.json | 3 +- .../components/spotify/translations/lb.json | 3 +- .../synology_dsm/translations/lb.json | 3 +- .../components/toon/translations/lb.json | 3 +- .../components/unifi/translations/lb.json | 3 +- .../components/withings/translations/lb.json | 3 +- .../components/yeelight/translations/lb.json | 19 ++++++++ .../zoneminder/translations/lb.json | 21 ++++++++- 30 files changed, 347 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/nzbget/translations/lb.json create mode 100644 homeassistant/components/omnilogic/translations/ru.json create mode 100644 homeassistant/components/progettihwsw/translations/lb.json create mode 100644 homeassistant/components/rpi_power/translations/lb.json create mode 100644 homeassistant/components/sharkiq/translations/lb.json diff --git a/homeassistant/components/alarm_control_panel/translations/nl.json b/homeassistant/components/alarm_control_panel/translations/nl.json index 15b5fd8457c..0a0f33d6181 100644 --- a/homeassistant/components/alarm_control_panel/translations/nl.json +++ b/homeassistant/components/alarm_control_panel/translations/nl.json @@ -25,7 +25,7 @@ "state": { "_": { "armed": "Ingeschakeld", - "armed_away": "Afwezig Ingeschakeld", + "armed_away": "Ingeschakeld afwezig", "armed_custom_bypass": "Ingeschakeld met overbrugging(en)", "armed_home": "Ingeschakeld thuis", "armed_night": "Ingeschakeld nacht", diff --git a/homeassistant/components/alarmdecoder/translations/lb.json b/homeassistant/components/alarmdecoder/translations/lb.json index 49cc4cd37f3..08dc02d6ea7 100644 --- a/homeassistant/components/alarmdecoder/translations/lb.json +++ b/homeassistant/components/alarmdecoder/translations/lb.json @@ -1,6 +1,20 @@ { "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "service_unavailable": "Feeler beim verbannen" + }, "step": { + "protocol": { + "data": { + "device_baudrate": "Apparat Baudrate", + "device_path": "Pad vum Apparat", + "host": "Host", + "port": "Port" + } + }, "user": { "data": { "protocol": "Protokoll" @@ -9,10 +23,40 @@ } }, "options": { + "error": { + "int": "D'Feld hei \u00ebnnen muss eng ganz Zuel sinn.", + "relay_inclusive": "Relais Adress a Relais Kanal sin vuneneen ofh\u00e4ngeg a musse mat abegraff sinn." + }, "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternative Nuecht Modus", + "auto_bypass": "Auto Bypass beim aktiv\u00e9ieren", + "code_arm_required": "Code erfuerderlech fir d'Aktiv\u00e9ierung" + }, + "title": "AlarmDecoder konfigur\u00e9ieren" + }, "init": { "data": { "edit_select": "\u00c4nneren" + }, + "description": "Wat w\u00eblls du \u00e4nneren?", + "title": "AlarmDecoder konfigur\u00e9ieren" + }, + "zone_details": { + "data": { + "zone_loop": "RF Schleef", + "zone_name": "Numm vun der Zone", + "zone_relayaddr": "Relais Adresse", + "zone_relaychan": "Relais Kanal", + "zone_rfid": "RF Serielle", + "zone_type": "Type vun der Zone" + }, + "title": "AlarmDecoder konfigur\u00e9ieren" + }, + "zone_select": { + "data": { + "zone_number": "Zone Nummer" } } } diff --git a/homeassistant/components/almond/translations/lb.json b/homeassistant/components/almond/translations/lb.json index 3b866a326be..bfcdad87e52 100644 --- a/homeassistant/components/almond/translations/lb.json +++ b/homeassistant/components/almond/translations/lb.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Almond Kont konfigur\u00e9ieren.", "cannot_connect": "Kann sech net mam Almond Server verbannen.", - "missing_configuration": "Kuckt w.e.g. Dokumentatioun iwwert d'ariichten vun Almond." + "missing_configuration": "Kuckt w.e.g. Dokumentatioun iwwert d'ariichten vun Almond.", + "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/august/translations/lb.json b/homeassistant/components/august/translations/lb.json index 501af05c2df..dbc71325a81 100644 --- a/homeassistant/components/august/translations/lb.json +++ b/homeassistant/components/august/translations/lb.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Kont ass scho konfigur\u00e9iert" + "already_configured": "Kont ass scho konfigur\u00e9iert", + "reauth_successful": "Re-Authentifikatioun erfollegr\u00e4ich" }, "error": { "cannot_connect": "Feeler beim verbannen, prob\u00e9iert w.e.g. nach emol.", diff --git a/homeassistant/components/canary/translations/lb.json b/homeassistant/components/canary/translations/lb.json index 4f17a03ff4b..0ad059e1fcc 100644 --- a/homeassistant/components/canary/translations/lb.json +++ b/homeassistant/components/canary/translations/lb.json @@ -1,5 +1,31 @@ { "config": { - "flow_title": "Canary: {name}" + "abort": { + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech.", + "unknown": "Onerwaarte Feeler" + }, + "error": { + "cannot_connect": "Feeler beim verbannen" + }, + "flow_title": "Canary: {name}", + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm" + }, + "title": "Mat Canary verbannen" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Argumenter fir ffmpeg fir Kamera", + "timeout": "Ufro Z\u00e4itiwwerscheidung (sekonnen)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/lb.json b/homeassistant/components/home_connect/translations/lb.json index 1820e1e2788..210c871a1b8 100644 --- a/homeassistant/components/home_connect/translations/lb.json +++ b/homeassistant/components/home_connect/translations/lb.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "missing_configuration": "Home Connecz Komponent ass nach net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun." + "missing_configuration": "Home Connecz Komponent ass nach net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.", + "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})" }, "create_entry": { "default": "Erfollegr\u00e4ich mat Home Connect authentifiz\u00e9iert." diff --git a/homeassistant/components/homekit_controller/translations/lb.json b/homeassistant/components/homekit_controller/translations/lb.json index c4c0d41117a..7aef753e0dd 100644 --- a/homeassistant/components/homekit_controller/translations/lb.json +++ b/homeassistant/components/homekit_controller/translations/lb.json @@ -64,7 +64,13 @@ "button6": "Kn\u00e4ppchen 6", "button7": "Kn\u00e4ppchen 7", "button8": "Kn\u00e4ppchen 8", - "button9": "Kn\u00e4ppchen 9" + "button9": "Kn\u00e4ppchen 9", + "doorbell": "Schell" + }, + "trigger_type": { + "double_press": "\"{subtype}\" zwee mol gedr\u00e9ckt", + "long_press": "\"{subtype}\" gedr\u00e9ckt an ugehal", + "single_press": "\"{subtype}\" gedr\u00e9ckt" } }, "title": "HomeKit Kontroller" diff --git a/homeassistant/components/insteon/translations/lb.json b/homeassistant/components/insteon/translations/lb.json index bff6ff757c1..367b626a6d0 100644 --- a/homeassistant/components/insteon/translations/lb.json +++ b/homeassistant/components/insteon/translations/lb.json @@ -27,6 +27,24 @@ "description": "Insteon Hub Versioun 2 konfigur\u00e9ieren.", "title": "Insteon Hub Versioun 2" }, + "hubv1": { + "data": { + "host": "IP Adresse", + "port": "Port" + }, + "description": "Insteon Hub Versioun 1 (pre-2014) konfigur\u00e9ieren.", + "title": "Insteon Hub Versioun 1" + }, + "hubv2": { + "data": { + "host": "IP Adresse", + "password": "Passwuert", + "port": "Port", + "username": "Benotzernumm" + }, + "description": "Insteon Hub Versioun 2 konfigur\u00e9ieren.", + "title": "Insteon Hub Versioun 2" + }, "init": { "data": { "hubv1": "Hub Versioun 1 (Pre-2014)", @@ -42,6 +60,12 @@ }, "description": "Insteon PowerLink Modem (PLM) konfigur\u00e9ieren.", "title": "Insteon PLM" + }, + "user": { + "data": { + "modem_type": "Typ vu Modem." + }, + "title": "Insteon" } } }, diff --git a/homeassistant/components/kodi/translations/lb.json b/homeassistant/components/kodi/translations/lb.json index 872615f5bea..adbae8fafe0 100644 --- a/homeassistant/components/kodi/translations/lb.json +++ b/homeassistant/components/kodi/translations/lb.json @@ -46,5 +46,11 @@ } } } + }, + "device_automation": { + "trigger_type": { + "turn_off": "{entity_name} soll ugeschalt ginn", + "turn_on": "{entity_name} soll ugeschalt ginn" + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/lb.json b/homeassistant/components/netatmo/translations/lb.json index f46b8627e0c..7dad13d085a 100644 --- a/homeassistant/components/netatmo/translations/lb.json +++ b/homeassistant/components/netatmo/translations/lb.json @@ -3,6 +3,7 @@ "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.", + "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})", "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." }, "create_entry": { diff --git a/homeassistant/components/nzbget/translations/lb.json b/homeassistant/components/nzbget/translations/lb.json new file mode 100644 index 00000000000..5da36a5d859 --- /dev/null +++ b/homeassistant/components/nzbget/translations/lb.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "unknown": "Onerwaarte Feeler" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun" + }, + "flow_title": "NZBGet: {name}", + "step": { + "user": { + "data": { + "host": "Host", + "name": "Numm", + "password": "Passwuert", + "port": "Port", + "ssl": "NZBGet benotzt een SSL Zertifikat", + "username": "Benotzernumm", + "verify_ssl": "NZBGet benotzt ee g\u00ebltegen SSL Zertifikat" + }, + "title": "Mat NZBGet verbannen" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalle vun de Mise \u00e0 jour (sekonnen)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/en.json b/homeassistant/components/omnilogic/translations/en.json index 8dde40be8fc..858cfe31323 100644 --- a/homeassistant/components/omnilogic/translations/en.json +++ b/homeassistant/components/omnilogic/translations/en.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "single_instance_allowed": "Already configured. Only a single configuration possible." }, "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": { - "password": "[%key:common::config_flow::data::password%]", - "username": "[%key:common::config_flow::data::username%]" + "password": "Password", + "username": "Username" } } } diff --git a/homeassistant/components/omnilogic/translations/ru.json b/homeassistant/components/omnilogic/translations/ru.json new file mode 100644 index 00000000000..3b05c74695d --- /dev/null +++ b/homeassistant/components/omnilogic/translations/ru.json @@ -0,0 +1,30 @@ +{ + "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": { + "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" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "polling_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": "Omnilogic" +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/lb.json b/homeassistant/components/openweathermap/translations/lb.json index 0f1669a5050..1bf8fb29988 100644 --- a/homeassistant/components/openweathermap/translations/lb.json +++ b/homeassistant/components/openweathermap/translations/lb.json @@ -6,9 +6,23 @@ "step": { "user": { "data": { + "api_key": "OpenWeatherMap API Schl\u00ebssel", "language": "Sproch", "latitude": "Breedegrad", - "longitude": "L\u00e4ngegrad" + "longitude": "L\u00e4ngegrad", + "mode": "Modus", + "name": "Numm vun der Integratioun" + }, + "title": "OpenWeatherMap API Schl\u00ebssel" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Sproch", + "mode": "Modus" } } } diff --git a/homeassistant/components/plugwise/translations/lb.json b/homeassistant/components/plugwise/translations/lb.json index 8b0ea38c2f6..4ba84f88f11 100644 --- a/homeassistant/components/plugwise/translations/lb.json +++ b/homeassistant/components/plugwise/translations/lb.json @@ -13,11 +13,21 @@ "user": { "data": { "host": "Smile IP Adresse", - "password": "Smile ID" + "password": "Smile ID", + "port": "Smile Port Nummer" }, "description": "Detailler", "title": "Mat Smile verbannen" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Scan Intervall (sekonnen)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/lb.json b/homeassistant/components/progettihwsw/translations/lb.json new file mode 100644 index 00000000000..e5ec74b7ab0 --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/lb.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "relay_modes": { + "data": { + "relay_3": "Relais 3", + "relay_4": "Relais 4", + "relay_5": "Relais 5", + "relay_6": "Relais 6", + "relay_7": "Relais 7", + "relay_8": "Relais 8", + "relay_9": "Relais 9" + }, + "title": "Relais ariichten" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + }, + "title": "ProgettiHWSW Automatisme" +} \ No newline at end of file diff --git a/homeassistant/components/remote/translations/lb.json b/homeassistant/components/remote/translations/lb.json index b5d8f7f6ecf..f9f81a85a29 100644 --- a/homeassistant/components/remote/translations/lb.json +++ b/homeassistant/components/remote/translations/lb.json @@ -1,6 +1,8 @@ { "device_automation": { "action_type": { + "toggle": "{entity_name} \u00ebmschalten", + "turn_off": "{entity_name} ausschalten", "turn_on": "{entity_name} uschalten" }, "condition_type": { diff --git a/homeassistant/components/risco/translations/lb.json b/homeassistant/components/risco/translations/lb.json index 933af1f4a38..985112bf446 100644 --- a/homeassistant/components/risco/translations/lb.json +++ b/homeassistant/components/risco/translations/lb.json @@ -20,12 +20,27 @@ }, "options": { "step": { + "ha_to_risco": { + "data": { + "armed_away": "Aktiv\u00e9iert \u00cbnnerwee", + "armed_home": "Aktiv\u00e9iert Doheem", + "armed_night": "Aktiv\u00e9iert Nuecht" + } + }, "init": { "data": { "code_arm_required": "Pin Code n\u00e9ideg fir unzeschalten", "code_disarm_required": "Pin Code n\u00e9ideg fir auszeschalten" }, "title": "Optioune konfigur\u00e9ieren" + }, + "risco_to_ha": { + "data": { + "A": "Grupp A", + "B": "Grupp B", + "C": "Grupp C", + "D": "Grupp D" + } } } } diff --git a/homeassistant/components/rpi_power/translations/lb.json b/homeassistant/components/rpi_power/translations/lb.json new file mode 100644 index 00000000000..0e08431d1b6 --- /dev/null +++ b/homeassistant/components/rpi_power/translations/lb.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_devices_found": "Kann d\u00e9i Systemklass fir d\u00ebs noutwendeg Komponent net fannen, stell s\u00e9cher dass de Kernel rezent ass an d'Hardware \u00ebnnerst\u00ebtzt g\u00ebtt." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/lb.json b/homeassistant/components/sharkiq/translations/lb.json new file mode 100644 index 00000000000..b9984968cbf --- /dev/null +++ b/homeassistant/components/sharkiq/translations/lb.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "cannot_connect": "Feeler beim verbannen" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "reauth": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm" + } + }, + "user": { + "data": { + "password": "Passwuert" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/lb.json b/homeassistant/components/shelly/translations/lb.json index b50f528c3c0..3694abd8002 100644 --- a/homeassistant/components/shelly/translations/lb.json +++ b/homeassistant/components/shelly/translations/lb.json @@ -6,6 +6,7 @@ "error": { "auth_not_supported": "Shelly Apparaten d\u00e9i eng Authentifikatioun ben\u00e9idegen ginn aktuell net \u00ebnnerst\u00ebtzt.", "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", "unknown": "Onerwaarte Feeler" }, "flow_title": "Shelly: {name}", @@ -13,6 +14,12 @@ "confirm_discovery": { "description": "Soll de {model} um {host} konfigur\u00e9iert ginn?" }, + "credentials": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm" + } + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/smappee/translations/lb.json b/homeassistant/components/smappee/translations/lb.json index 7f514644918..90dd0b10486 100644 --- a/homeassistant/components/smappee/translations/lb.json +++ b/homeassistant/components/smappee/translations/lb.json @@ -6,7 +6,8 @@ "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." + "missing_configuration": "Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun.", + "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})" }, "flow_title": "Smappee: {name}", "step": { diff --git a/homeassistant/components/somfy/translations/lb.json b/homeassistant/components/somfy/translations/lb.json index f34c07efd06..5a7100bbb3f 100644 --- a/homeassistant/components/somfy/translations/lb.json +++ b/homeassistant/components/somfy/translations/lb.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Somfy Kont konfigur\u00e9ieren.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", - "missing_configuration": "D'Somfy Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun." + "missing_configuration": "D'Somfy Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun.", + "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})" }, "create_entry": { "default": "Erfollegr\u00e4ich mat Somfy authentifiz\u00e9iert." diff --git a/homeassistant/components/spotify/translations/lb.json b/homeassistant/components/spotify/translations/lb.json index fedaff526a6..59aaf936dc1 100644 --- a/homeassistant/components/spotify/translations/lb.json +++ b/homeassistant/components/spotify/translations/lb.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Spotify Kont konfigur\u00e9ieren.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", - "missing_configuration": "Spotifiy Integratioun ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun." + "missing_configuration": "Spotifiy Integratioun ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun.", + "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})" }, "create_entry": { "default": "Erfollegr\u00e4ich mat Spotify authentifiz\u00e9iert." diff --git a/homeassistant/components/synology_dsm/translations/lb.json b/homeassistant/components/synology_dsm/translations/lb.json index 9ca8d0cdfa5..63e1be3fa2d 100644 --- a/homeassistant/components/synology_dsm/translations/lb.json +++ b/homeassistant/components/synology_dsm/translations/lb.json @@ -44,7 +44,8 @@ "step": { "init": { "data": { - "scan_interval": "Minutte t\u00ebscht Scannen" + "scan_interval": "Minutte t\u00ebscht Scannen", + "timeout": "Z\u00e4itiwwerscheidung (sekonnen)" } } } diff --git a/homeassistant/components/toon/translations/lb.json b/homeassistant/components/toon/translations/lb.json index 5d7095d0c85..6491c666738 100644 --- a/homeassistant/components/toon/translations/lb.json +++ b/homeassistant/components/toon/translations/lb.json @@ -5,7 +5,8 @@ "authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.", "authorize_url_timeout": "Z\u00e4itiwwerschraidung beim erstellen vun der Autorisatioun's URL.", "missing_configuration": "Komponent ass net konfigur\u00e9iert. Folleg der Dokumentatioun.", - "no_agreements": "D\u00ebse Kont huet keen Toon Ecran." + "no_agreements": "D\u00ebse Kont huet keen Toon Ecran.", + "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})" }, "step": { "agreement": { diff --git a/homeassistant/components/unifi/translations/lb.json b/homeassistant/components/unifi/translations/lb.json index 5f7e7192bd9..992ee8192e3 100644 --- a/homeassistant/components/unifi/translations/lb.json +++ b/homeassistant/components/unifi/translations/lb.json @@ -53,7 +53,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Bandbreet Benotzung Sensore fir Netzwierk Cliente" + "allow_bandwidth_sensors": "Bandbreet Benotzung Sensore fir Netzwierk Cliente", + "allow_uptime_sensors": "Uptime Sensoren fir Netzwierkklienten" }, "description": "Statistik Sensoren konfigur\u00e9ieren", "title": "UniFi Optiounen 3/3" diff --git a/homeassistant/components/withings/translations/lb.json b/homeassistant/components/withings/translations/lb.json index a5102baf917..e3ff23e392a 100644 --- a/homeassistant/components/withings/translations/lb.json +++ b/homeassistant/components/withings/translations/lb.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Konfiguratioun aktualis\u00e9iert fir de Profil.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", - "missing_configuration": "Withings Integratioun ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun." + "missing_configuration": "Withings Integratioun ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun.", + "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})" }, "create_entry": { "default": "Erfollegr\u00e4ich mat Withings authentifiz\u00e9iert." diff --git a/homeassistant/components/yeelight/translations/lb.json b/homeassistant/components/yeelight/translations/lb.json index c7cf37887ef..255df06effe 100644 --- a/homeassistant/components/yeelight/translations/lb.json +++ b/homeassistant/components/yeelight/translations/lb.json @@ -1,3 +1,22 @@ { + "config": { + "step": { + "pick_device": { + "data": { + "device": "Apparat" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "Modell (Optionell)", + "use_music_mode": "Musek Modus aktiv\u00e9ieren" + } + } + } + }, "title": "Yeelight" } \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/lb.json b/homeassistant/components/zoneminder/translations/lb.json index 29b4bffe466..ad0669b1040 100644 --- a/homeassistant/components/zoneminder/translations/lb.json +++ b/homeassistant/components/zoneminder/translations/lb.json @@ -1,10 +1,29 @@ { "config": { + "abort": { + "auth_fail": "Benotzernumm oder Passwuert inkorrekt", + "connection_error": "Feeler beim verbannen mam ZoneMinder Server." + }, + "create_entry": { + "default": "Zoneminder Server dob\u00e4igesat." + }, + "error": { + "auth_fail": "Benotzernumm oder Passwuert inkorrekt", + "connection_error": "Feeler beim verbannen mam ZoneMinder Server." + }, + "flow_title": "ZoneMinder", "step": { "user": { "data": { + "host": "Host a Port (beispill 10.10.0.4:8010)", + "password": "Passwuert", + "path": "ZM Pad", + "path_zms": "ZMS Pad", + "ssl": "Benotz SSL fir d'Verbindung mat ZoneMinder", + "username": "Benotzernumm", "verify_ssl": "SSL Zertifikat iwwerpr\u00e9iwen" - } + }, + "title": "ZoneMinder Server dob\u00e4isetzen." } } } From b3b9c52df21a8420cc3e8d18b6bfa8b396e156ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Sep 2020 22:28:59 -0500 Subject: [PATCH 357/514] Add device info to gogogate2 (#40538) --- homeassistant/components/gogogate2/const.py | 1 + homeassistant/components/gogogate2/cover.py | 14 +- tests/components/gogogate2/test_cover.py | 254 +++++++++++--------- 3 files changed, 149 insertions(+), 120 deletions(-) diff --git a/homeassistant/components/gogogate2/const.py b/homeassistant/components/gogogate2/const.py index 5c0ef55ff3f..2f6ac76122f 100644 --- a/homeassistant/components/gogogate2/const.py +++ b/homeassistant/components/gogogate2/const.py @@ -4,3 +4,4 @@ DOMAIN = "gogogate2" DATA_UPDATE_COORDINATOR = "data_update_coordinator" DEVICE_TYPE_GOGOGATE2 = "gogogate2" DEVICE_TYPE_ISMARTGATE = "ismartgate" +MANUFACTURER = "Remsol" diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index 8e753eb6ae5..e748b420edb 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -34,7 +34,7 @@ from .common import ( cover_unique_id, get_data_update_coordinator, ) -from .const import DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, DOMAIN +from .const import DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) @@ -154,3 +154,15 @@ class DeviceCover(CoordinatorEntity, CoverEntity): door = get_door_by_id(self._door.door_id, self.coordinator.data) self._door = door or self._door return self._door + + @property + def device_info(self): + """Device info for the controller.""" + data = self.coordinator.data + return { + "identifiers": {(DOMAIN, self._config_entry.unique_id)}, + "name": self._config_entry.title, + "manufacturer": MANUFACTURER, + "model": data.model, + "sw_version": data.firmwareversion, + } diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index b1ab2284580..91bffca56ce 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -25,6 +25,7 @@ from homeassistant.components.gogogate2.const import ( DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, DOMAIN, + MANUFACTURER, ) from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN from homeassistant.config import async_process_ha_core_config @@ -49,54 +50,18 @@ from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from tests.async_mock import MagicMock, patch -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed, mock_device_registry -@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 - - 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 - - -@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 = GogoGate2InfoResponse( +def _mocked_gogogate_open_door_response(): + return GogoGate2InfoResponse( user="user1", gogogatename="gogogatename0", - model="", + model="gogogate2", apiversion="", remoteaccessenabled=False, remoteaccess="abc123.blah.blah", - firmwareversion="", + firmwareversion="222", apicode="", door1=GogoGate2Door( door_id=1, @@ -144,17 +109,17 @@ async def test_import( network=Network(ip=""), wifi=Wifi(SSID="", linkquality="", signal=""), ) - gogogate2api_mock.return_value = api0 - api1 = MagicMock(spec=ISmartGateApi) - api1.info.return_value = ISmartGateInfoResponse( + +def _mocked_ismartgate_closed_door_response(): + return ISmartGateInfoResponse( user="user1", ismartgatename="ismartgatename0", - model="", + model="ismartgatePRO", apiversion="", remoteaccessenabled=False, remoteaccess="abc321.blah.blah", - firmwareversion="", + firmwareversion="555", pin=123, lang="en", newfirmware=False, @@ -176,9 +141,9 @@ async def test_import( voltage=40, ), door2=ISmartGateDoor( - door_id=1, + door_id=2, permission=True, - name=None, + name="Door2", gate=True, mode=DoorMode.GARAGE, status=DoorStatus.CLOSED, @@ -193,16 +158,16 @@ async def test_import( voltage=40, ), door3=ISmartGateDoor( - door_id=1, + door_id=3, permission=True, name=None, gate=False, mode=DoorMode.GARAGE, - status=DoorStatus.CLOSED, + status=DoorStatus.UNDEFINED, sensor=True, sensorid=None, camera=False, - events=2, + events=0, temperature=None, enabled=True, apicode="apicode0", @@ -212,6 +177,50 @@ async def test_import( network=Network(ip=""), wifi=Wifi(SSID="", linkquality="", signal=""), ) + + +@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 + + 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 + + +@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 = _mocked_gogogate_open_door_response() + gogogate2api_mock.return_value = api0 + + api1 = MagicMock(spec=ISmartGateApi) + api1.info.return_value = _mocked_ismartgate_closed_door_response() ismartgateapi_mock.return_value = api1 hass_config = { @@ -243,13 +252,14 @@ async def test_import( entity_ids = hass.states.async_entity_ids(COVER_DOMAIN) assert entity_ids is not None - assert len(entity_ids) == 2 + assert len(entity_ids) == 3 assert "cover.door1" in entity_ids assert "cover.door1_2" in entity_ids + assert "cover.door2" in entity_ids @patch("homeassistant.components.gogogate2.common.GogoGate2Api") -async def test_open_close_update(gogogat2api_mock, hass: HomeAssistant) -> None: +async def test_open_close_update(gogogate2api_mock, hass: HomeAssistant) -> None: """Test open and close and data update.""" def info_response(door_status: DoorStatus) -> GogoGate2InfoResponse: @@ -312,7 +322,7 @@ async def test_open_close_update(gogogat2api_mock, hass: HomeAssistant) -> None: api = MagicMock(GogoGate2Api) api.activate.return_value = GogoGate2ActivateResponse(result=True) api.info.return_value = info_response(DoorStatus.OPENED) - gogogat2api_mock.return_value = api + gogogate2api_mock.return_value = api config_entry = MockConfigEntry( domain=DOMAIN, @@ -364,71 +374,7 @@ async def test_open_close_update(gogogat2api_mock, hass: HomeAssistant) -> None: @patch("homeassistant.components.gogogate2.common.ISmartGateApi") async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: """Test availability.""" - closed_door_response = ISmartGateInfoResponse( - user="user1", - ismartgatename="ismartgatename0", - model="", - apiversion="", - remoteaccessenabled=False, - remoteaccess="abc123.blah.blah", - firmwareversion="", - pin=123, - lang="en", - newfirmware=False, - door1=ISmartGateDoor( - door_id=1, - permission=True, - name="Door1", - gate=False, - mode=DoorMode.GARAGE, - status=DoorStatus.CLOSED, - sensor=True, - sensorid=None, - camera=False, - events=2, - temperature=None, - enabled=True, - apicode="apicode0", - customimage=False, - voltage=40, - ), - door2=ISmartGateDoor( - door_id=2, - permission=True, - name="Door2", - gate=True, - mode=DoorMode.GARAGE, - status=DoorStatus.CLOSED, - sensor=True, - sensorid=None, - camera=False, - events=2, - temperature=None, - enabled=True, - apicode="apicode0", - customimage=False, - voltage=40, - ), - door3=ISmartGateDoor( - door_id=3, - permission=True, - name=None, - gate=False, - mode=DoorMode.GARAGE, - status=DoorStatus.UNDEFINED, - sensor=True, - sensorid=None, - camera=False, - events=0, - temperature=None, - enabled=True, - apicode="apicode0", - customimage=False, - voltage=40, - ), - network=Network(ip=""), - wifi=Wifi(SSID="", linkquality="", signal=""), - ) + closed_door_response = _mocked_ismartgate_closed_door_response() api = MagicMock(ISmartGateApi) api.info.return_value = closed_door_response @@ -470,3 +416,73 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_CLOSED + + +@patch("homeassistant.components.gogogate2.common.ISmartGateApi") +async def test_device_info_ismartgate(ismartgateapi_mock, hass: HomeAssistant) -> None: + """Test device info.""" + device_registry = mock_device_registry(hass) + + closed_door_response = _mocked_ismartgate_closed_door_response() + + api = MagicMock(ISmartGateApi) + api.info.return_value = closed_door_response + ismartgateapi_mock.return_value = api + + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + title="mycontroller", + unique_id="xyz", + 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 await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + device = device_registry.async_get_device({(DOMAIN, "xyz")}, set()) + assert device + assert device.manufacturer == MANUFACTURER + assert device.name == "mycontroller" + assert device.model == "ismartgatePRO" + assert device.sw_version == "555" + + +@patch("homeassistant.components.gogogate2.common.GogoGate2Api") +async def test_device_info_gogogate2(gogogate2api_mock, hass: HomeAssistant) -> None: + """Test device info.""" + device_registry = mock_device_registry(hass) + + closed_door_response = _mocked_gogogate_open_door_response() + + api = MagicMock(GogoGate2Api) + api.info.return_value = closed_door_response + gogogate2api_mock.return_value = api + + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + title="mycontroller", + unique_id="xyz", + data={ + CONF_DEVICE: DEVICE_TYPE_GOGOGATE2, + CONF_IP_ADDRESS: "127.0.0.1", + CONF_USERNAME: "admin", + CONF_PASSWORD: "password", + }, + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + device = device_registry.async_get_device({(DOMAIN, "xyz")}, set()) + assert device + assert device.manufacturer == MANUFACTURER + assert device.name == "mycontroller" + assert device.model == "gogogate2" + assert device.sw_version == "222" From 9cd8f66e142b8359b908ce5b4b651490f3940ed2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 26 Sep 2020 08:21:12 +0200 Subject: [PATCH 358/514] Upgrade spotipy to 2.16.0 (#40606) --- homeassistant/components/spotify/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index d17cd43c47f..db2f35ded91 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.14.0"], + "requirements": ["spotipy==2.16.0"], "zeroconf": ["_spotify-connect._tcp.local."], "dependencies": ["http"], "codeowners": ["@frenck"], diff --git a/requirements_all.txt b/requirements_all.txt index 0032fa2fdc3..b449142f651 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2065,7 +2065,7 @@ spiderpy==1.3.1 spotcrime==1.0.4 # homeassistant.components.spotify -spotipy==2.14.0 +spotipy==2.16.0 # homeassistant.components.recorder # homeassistant.components.sql diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e0d14bd9ea2..f2a42332b5d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -966,7 +966,7 @@ speedtest-cli==2.1.2 spiderpy==1.3.1 # homeassistant.components.spotify -spotipy==2.14.0 +spotipy==2.16.0 # homeassistant.components.recorder # homeassistant.components.sql From 6ddc6a44a2612b3512a99e62f2a900beff581ade Mon Sep 17 00:00:00 2001 From: Daniel Kucera Date: Sat, 26 Sep 2020 09:11:32 +0200 Subject: [PATCH 359/514] Throttle ebusd sensors instead of the whole component (#40610) --- homeassistant/components/ebusd/__init__.py | 7 +------ homeassistant/components/ebusd/sensor.py | 3 +++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py index 855e62727b5..00c40344d6e 100644 --- a/homeassistant/components/ebusd/__init__.py +++ b/homeassistant/components/ebusd/__init__.py @@ -1,5 +1,4 @@ """Support for Ebusd daemon for communication with eBUS heating systems.""" -from datetime import timedelta import logging import socket @@ -14,7 +13,6 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform -from homeassistant.util import Throttle from .const import DOMAIN, SENSOR_TYPES @@ -26,8 +24,6 @@ CONF_CIRCUIT = "circuit" CACHE_TTL = 900 SERVICE_EBUSD_WRITE = "ebusd_write" -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=15) - def verify_ebusd_config(config): """Verify eBusd config.""" @@ -59,6 +55,7 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the eBusd component.""" + _LOGGER.debug("Integration setup started") conf = config[DOMAIN] name = conf[CONF_NAME] circuit = conf[CONF_CIRCUIT] @@ -66,7 +63,6 @@ def setup(hass, config): server_address = (conf.get(CONF_HOST), conf.get(CONF_PORT)) try: - _LOGGER.debug("Ebusd integration setup started") ebusdpy.init(server_address) hass.data[DOMAIN] = EbusdData(server_address, circuit) @@ -95,7 +91,6 @@ class EbusdData: self._address = address self.value = {} - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self, name, stype): """Call the Ebusd API to update the data.""" try: diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py index 63f72a89ccd..badb94a6f85 100644 --- a/homeassistant/components/ebusd/sensor.py +++ b/homeassistant/components/ebusd/sensor.py @@ -3,6 +3,7 @@ import datetime import logging from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle import homeassistant.util.dt as dt_util from .const import DOMAIN @@ -13,6 +14,7 @@ TIME_FRAME2_BEGIN = "time_frame2_begin" TIME_FRAME2_END = "time_frame2_end" TIME_FRAME3_BEGIN = "time_frame3_begin" TIME_FRAME3_END = "time_frame3_end" +MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=15) _LOGGER = logging.getLogger(__name__) @@ -85,6 +87,7 @@ class EbusdSensor(Entity): """Return the unit of measurement.""" return self._unit_of_measurement + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Fetch new state data for the sensor.""" try: From 4cb118ca299d595cb4aaa96eb41867c0f2696107 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 26 Sep 2020 09:15:09 +0200 Subject: [PATCH 360/514] Bump pychromecast to 7.4.1 (#40587) * Bump pychromecast to 7.4.0 * Bump pychromecast to 7.4.1 --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 49d26431f5b..9d9a7bacb7f 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==7.2.1"], + "requirements": ["pychromecast==7.4.1"], "after_dependencies": ["cloud", "http", "media_source", "tts", "zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index b449142f651..771d31c80be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1274,7 +1274,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==7.2.1 +pychromecast==7.4.1 # homeassistant.components.cmus pycmus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f2a42332b5d..6a86f7538ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -622,7 +622,7 @@ pyblackbird==0.5 pybotvac==0.0.17 # homeassistant.components.cast -pychromecast==7.2.1 +pychromecast==7.4.1 # homeassistant.components.coolmaster pycoolmasternet-async==0.1.2 From 4a63b83caa9b728e4acf87f492f5e7cbfd5c88f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 26 Sep 2020 09:17:04 +0200 Subject: [PATCH 361/514] Add installation type to discovery endpoint (#40585) --- homeassistant/components/api/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index c1f692a76ad..34d899e996c 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -38,6 +38,7 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.helpers.state import AsyncTrackStates +from homeassistant.helpers.system_info import async_get_system_info _LOGGER = logging.getLogger(__name__) @@ -45,6 +46,7 @@ ATTR_BASE_URL = "base_url" ATTR_EXTERNAL_URL = "external_url" ATTR_INTERNAL_URL = "internal_url" ATTR_LOCATION_NAME = "location_name" +ATTR_INSTALLATION_TYPE = "installation_type" ATTR_REQUIRES_API_PASSWORD = "requires_api_password" ATTR_UUID = "uuid" ATTR_VERSION = "version" @@ -181,6 +183,7 @@ class APIDiscoveryView(HomeAssistantView): """Get discovery information.""" hass = request.app["hass"] uuid = await hass.helpers.instance_id.async_get() + system_info = await async_get_system_info(hass) data = { ATTR_UUID: uuid, @@ -188,6 +191,7 @@ class APIDiscoveryView(HomeAssistantView): ATTR_EXTERNAL_URL: None, ATTR_INTERNAL_URL: None, ATTR_LOCATION_NAME: hass.config.location_name, + ATTR_INSTALLATION_TYPE: system_info[ATTR_INSTALLATION_TYPE], # always needs authentication ATTR_REQUIRES_API_PASSWORD: True, ATTR_VERSION: __version__, From a42736e4379c7fc404d8c9588f19167e7240df4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 26 Sep 2020 09:26:02 +0200 Subject: [PATCH 362/514] Allow non-authenticated calls to snapshots during onboarding (#40582) --- homeassistant/components/hassio/http.py | 8 ++++++-- tests/components/hassio/test_http.py | 11 +++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index aba0dac6494..60888d8d301 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -12,6 +12,7 @@ from aiohttp.web_exceptions import HTTPBadGateway import async_timeout from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView +from homeassistant.components.onboarding import async_is_onboarded from homeassistant.const import HTTP_UNAUTHORIZED from .const import X_HASS_IS_ADMIN, X_HASS_USER_ID, X_HASSIO @@ -54,7 +55,8 @@ class HassIOView(HomeAssistantView): self, request: web.Request, path: str ) -> Union[web.Response, web.StreamResponse]: """Route data to Hass.io.""" - if _need_auth(path) and not request[KEY_AUTHENTICATED]: + hass = request.app["hass"] + if _need_auth(hass, path) and not request[KEY_AUTHENTICATED]: return web.Response(status=HTTP_UNAUTHORIZED) return await self._command_proxy(path, request) @@ -145,8 +147,10 @@ def _get_timeout(path: str) -> int: return 300 -def _need_auth(path: str) -> bool: +def _need_auth(hass, path: str) -> bool: """Return if a path need authentication.""" + if not async_is_onboarded(hass) and path.startswith("snapshots"): + return False if NO_AUTH.match(path): return False return True diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 195a8652e2f..db6e9d1f85e 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -3,6 +3,8 @@ import asyncio import pytest +from homeassistant.components.hassio.http import _need_auth + from tests.async_mock import patch @@ -147,3 +149,12 @@ async def test_snapshot_upload_headers(hassio_client, aioclient_mock): req_headers = aioclient_mock.mock_calls[0][-1] req_headers["Content-Type"] == content_type + + +def test_need_auth(hass): + """Test if the requested path needs authentication.""" + assert not _need_auth(hass, "addons/test/logo") + assert _need_auth(hass, "snapshots/new/upload") + + hass.data["onboarding"] = False + assert not _need_auth(hass, "snapshots/new/upload") From 35cfc80dd791aae2dc58eefd34f810132b54ad6f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 26 Sep 2020 02:32:50 -0500 Subject: [PATCH 363/514] Log the remote ip address for incoming websocket connections when debug is on (#40581) --- homeassistant/components/websocket_api/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 27dac62791e..b71b19d5181 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -130,7 +130,7 @@ class WebSocketHandler: request = self.request wsock = self.wsock = web.WebSocketResponse(heartbeat=55) await wsock.prepare(request) - self._logger.debug("Connected") + self._logger.debug("Connected from %s", request.remote) self._handle_task = asyncio.current_task() @callback From c64eec3238c577d6e9f1c184c4d2ae0b6e711fef Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 26 Sep 2020 09:36:03 +0200 Subject: [PATCH 364/514] Add bare hostname as valid known hostname in get_url helper (#40510) --- homeassistant/helpers/network.py | 8 +++++--- tests/helpers/test_network.py | 8 ++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index 8bdfc286c1a..9cff5058a00 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -88,10 +88,12 @@ def get_url( scheme=scheme, host=request_host, port=hass.config.api.port ) - known_hostname = None + known_hostnames = ["localhost"] if hass.components.hassio.is_hassio(): host_info = hass.components.hassio.get_host_info() - known_hostname = f"{host_info['hostname']}.local" + known_hostnames.extend( + [host_info["hostname"], f"{host_info['hostname']}.local"] + ) if ( ( @@ -100,7 +102,7 @@ def get_url( and is_ip_address(request_host) and is_loopback(ip_address(request_host)) ) - or request_host in ["localhost", known_hostname] + or request_host in known_hostnames ) and (not require_ssl or current_url.scheme == "https") and (not require_standard_port or current_url.is_default_port()) diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index ed97b3e3757..495c9d511bd 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -848,6 +848,14 @@ async def test_get_current_request_url_with_known_host( == "http://homeassistant.local:8123" ) + with patch( + "homeassistant.helpers.network._get_request_host", + return_value="homeassistant", + ): + assert ( + get_url(hass, require_current_request=True) == "http://homeassistant:8123" + ) + with patch( "homeassistant.helpers.network._get_request_host", return_value="unknown.local" ), pytest.raises(NoURLAvailableError): From c011f3fa9518dea378d3a5630bbfca99be0cbc64 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 26 Sep 2020 13:19:59 +0200 Subject: [PATCH 365/514] Use direct service calls in light platform tests (#40604) --- tests/components/light/test_init.py | 388 ++++++++++++++++++---------- 1 file changed, 258 insertions(+), 130 deletions(-) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index f3276313775..e723ac77e03 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -11,6 +11,7 @@ from homeassistant.components import light from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, + ENTITY_MATCH_ALL, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -22,7 +23,6 @@ from homeassistant.setup import async_setup_component import tests.async_mock as mock from tests.common import async_mock_service -from tests.components.light import common @pytest.fixture @@ -47,20 +47,22 @@ async def test_methods(hass): # Test turn_on turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON) - await common.async_turn_on( - hass, - entity_id="entity_id_val", - transition="transition_val", - brightness="brightness_val", - rgb_color="rgb_color_val", - xy_color="xy_color_val", - profile="profile_val", - color_name="color_name_val", - white_value="white_val", + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "entity_id_val", + light.ATTR_TRANSITION: "transition_val", + light.ATTR_BRIGHTNESS: "brightness_val", + light.ATTR_RGB_COLOR: "rgb_color_val", + light.ATTR_XY_COLOR: "xy_color_val", + light.ATTR_PROFILE: "profile_val", + light.ATTR_COLOR_NAME: "color_name_val", + light.ATTR_WHITE_VALUE: "white_val", + }, + blocking=True, ) - await hass.async_block_till_done() - assert len(turn_on_calls) == 1 call = turn_on_calls[-1] @@ -78,29 +80,34 @@ async def test_methods(hass): # Test turn_off turn_off_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_OFF) - await common.async_turn_off( - hass, entity_id="entity_id_val", transition="transition_val" + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: "entity_id_val", + light.ATTR_TRANSITION: "transition_val", + }, + blocking=True, ) - await hass.async_block_till_done() - assert len(turn_off_calls) == 1 call = turn_off_calls[-1] - assert light.DOMAIN == call.domain - assert SERVICE_TURN_OFF == call.service + assert call.domain == light.DOMAIN + assert call.service == SERVICE_TURN_OFF assert call.data[ATTR_ENTITY_ID] == "entity_id_val" assert call.data[light.ATTR_TRANSITION] == "transition_val" # Test toggle toggle_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TOGGLE) - await common.async_toggle( - hass, entity_id="entity_id_val", transition="transition_val" + await hass.services.async_call( + light.DOMAIN, + SERVICE_TOGGLE, + {ATTR_ENTITY_ID: "entity_id_val", light.ATTR_TRANSITION: "transition_val"}, + blocking=True, ) - await hass.async_block_till_done() - assert len(toggle_calls) == 1 call = toggle_calls[-1] @@ -128,186 +135,287 @@ async def test_services(hass): assert not light.is_on(hass, ent3.entity_id) # Test basic turn_on, turn_off, toggle services - await common.async_turn_off(hass, entity_id=ent1.entity_id) - await common.async_turn_on(hass, entity_id=ent2.entity_id) - - await hass.async_block_till_done() + await hass.services.async_call( + light.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ent1.entity_id}, blocking=True + ) + await hass.services.async_call( + light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ent2.entity_id}, blocking=True + ) assert not light.is_on(hass, ent1.entity_id) assert light.is_on(hass, ent2.entity_id) # turn on all lights - await common.async_turn_on(hass) - - await hass.async_block_till_done() + await hass.services.async_call( + light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True + ) assert light.is_on(hass, ent1.entity_id) assert light.is_on(hass, ent2.entity_id) assert light.is_on(hass, ent3.entity_id) # turn off all lights - await common.async_turn_off(hass) - - await hass.async_block_till_done() + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) assert not light.is_on(hass, ent1.entity_id) assert not light.is_on(hass, ent2.entity_id) assert not light.is_on(hass, ent3.entity_id) # turn off all lights by setting brightness to 0 - await common.async_turn_on(hass) - await hass.async_block_till_done() - - await common.async_turn_on(hass, brightness=0) - await hass.async_block_till_done() + await hass.services.async_call( + light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True + ) + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL, light.ATTR_BRIGHTNESS: 0}, + blocking=True, + ) assert not light.is_on(hass, ent1.entity_id) assert not light.is_on(hass, ent2.entity_id) assert not light.is_on(hass, ent3.entity_id) # toggle all lights - await common.async_toggle(hass) - - await hass.async_block_till_done() + await hass.services.async_call( + light.DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True + ) assert light.is_on(hass, ent1.entity_id) assert light.is_on(hass, ent2.entity_id) assert light.is_on(hass, ent3.entity_id) # toggle all lights - await common.async_toggle(hass) - - await hass.async_block_till_done() + await hass.services.async_call( + light.DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True + ) assert not light.is_on(hass, ent1.entity_id) assert not light.is_on(hass, ent2.entity_id) assert not light.is_on(hass, ent3.entity_id) # Ensure all attributes process correctly - await common.async_turn_on( - hass, ent1.entity_id, transition=10, brightness=20, color_name="blue" + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_TRANSITION: 10, + light.ATTR_BRIGHTNESS: 20, + light.ATTR_COLOR_NAME: "blue", + }, + blocking=True, ) - await common.async_turn_on( - hass, ent2.entity_id, rgb_color=(255, 255, 255), white_value=255 + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent2.entity_id, + light.ATTR_RGB_COLOR: (255, 255, 255), + light.ATTR_WHITE_VALUE: 255, + }, + blocking=True, + ) + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent3.entity_id, + light.ATTR_XY_COLOR: (0.4, 0.6), + }, + blocking=True, ) - await common.async_turn_on(hass, ent3.entity_id, xy_color=(0.4, 0.6)) - await hass.async_block_till_done() _, data = ent1.last_call("turn_on") - assert { + assert data == { light.ATTR_TRANSITION: 10, light.ATTR_BRIGHTNESS: 20, light.ATTR_HS_COLOR: (240, 100), - } == data + } _, data = ent2.last_call("turn_on") - assert {light.ATTR_HS_COLOR: (0, 0), light.ATTR_WHITE_VALUE: 255} == data + assert data == {light.ATTR_HS_COLOR: (0, 0), light.ATTR_WHITE_VALUE: 255} _, data = ent3.last_call("turn_on") - assert {light.ATTR_HS_COLOR: (71.059, 100)} == data + assert data == {light.ATTR_HS_COLOR: (71.059, 100)} # Ensure attributes are filtered when light is turned off - await common.async_turn_on( - hass, ent1.entity_id, transition=10, brightness=0, color_name="blue" + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_TRANSITION: 10, + light.ATTR_BRIGHTNESS: 0, + light.ATTR_COLOR_NAME: "blue", + }, + blocking=True, ) - await common.async_turn_on( - hass, - ent2.entity_id, - brightness=0, - rgb_color=(255, 255, 255), - white_value=0, + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent2.entity_id, + light.ATTR_BRIGHTNESS: 0, + light.ATTR_RGB_COLOR: (255, 255, 255), + light.ATTR_WHITE_VALUE: 0, + }, + blocking=True, + ) + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent3.entity_id, + light.ATTR_BRIGHTNESS: 0, + light.ATTR_XY_COLOR: (0.4, 0.6), + }, + blocking=True, ) - await common.async_turn_on(hass, ent3.entity_id, brightness=0, xy_color=(0.4, 0.6)) - - await hass.async_block_till_done() assert not light.is_on(hass, ent1.entity_id) assert not light.is_on(hass, ent2.entity_id) assert not light.is_on(hass, ent3.entity_id) _, data = ent1.last_call("turn_off") - assert {light.ATTR_TRANSITION: 10} == data + assert data == {light.ATTR_TRANSITION: 10} _, data = ent2.last_call("turn_off") - assert {} == data + assert data == {} _, data = ent3.last_call("turn_off") - assert {} == data + assert data == {} # One of the light profiles prof_name, prof_h, prof_s, prof_bri, prof_t = "relax", 35.932, 69.412, 144, 0 # Test light profiles - await common.async_turn_on(hass, ent1.entity_id, profile=prof_name) + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ent1.entity_id, light.ATTR_PROFILE: prof_name}, + blocking=True, + ) # Specify a profile and a brightness attribute to overwrite it - await common.async_turn_on( - hass, ent2.entity_id, profile=prof_name, brightness=100, transition=1 + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent2.entity_id, + light.ATTR_PROFILE: prof_name, + light.ATTR_BRIGHTNESS: 100, + light.ATTR_TRANSITION: 1, + }, + blocking=True, ) - await hass.async_block_till_done() - _, data = ent1.last_call("turn_on") - assert { + assert data == { light.ATTR_BRIGHTNESS: prof_bri, light.ATTR_HS_COLOR: (prof_h, prof_s), light.ATTR_TRANSITION: prof_t, - } == data + } _, data = ent2.last_call("turn_on") - assert { + assert data == { light.ATTR_BRIGHTNESS: 100, light.ATTR_HS_COLOR: (prof_h, prof_s), light.ATTR_TRANSITION: 1, - } == data + } # Test toggle with parameters - await common.async_toggle( - hass, ent3.entity_id, profile=prof_name, brightness_pct=100 + await hass.services.async_call( + light.DOMAIN, + SERVICE_TOGGLE, + { + ATTR_ENTITY_ID: ent3.entity_id, + light.ATTR_PROFILE: prof_name, + light.ATTR_BRIGHTNESS_PCT: 100, + }, + blocking=True, ) - await hass.async_block_till_done() + _, data = ent3.last_call("turn_on") - assert { + assert data == { light.ATTR_BRIGHTNESS: 255, light.ATTR_HS_COLOR: (prof_h, prof_s), light.ATTR_TRANSITION: prof_t, - } == data + } # Test bad data - await common.async_turn_on(hass) - await common.async_turn_on(hass, ent1.entity_id, profile="nonexisting") + await hass.services.async_call( + light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True + ) + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ent1.entity_id, light.ATTR_PROFILE: -1}, + blocking=True, + ) with pytest.raises(vol.MultipleInvalid): - await common.async_turn_on(hass, ent2.entity_id, xy_color=["bla-di-bla", 5]) + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ent2.entity_id, light.ATTR_XY_COLOR: ["bla-di-bla", 5]}, + blocking=True, + ) with pytest.raises(vol.MultipleInvalid): - await common.async_turn_on(hass, ent3.entity_id, rgb_color=[255, None, 2]) - - await hass.async_block_till_done() + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ent3.entity_id, light.ATTR_RGB_COLOR: [255, None, 2]}, + blocking=True, + ) _, data = ent1.last_call("turn_on") - assert {} == data + assert data == {} _, data = ent2.last_call("turn_on") - assert {} == data + assert data == {} _, data = ent3.last_call("turn_on") - assert {} == data + assert data == {} # faulty attributes will not trigger a service call with pytest.raises(vol.MultipleInvalid): - await common.async_turn_on( - hass, ent1.entity_id, profile=prof_name, brightness="bright" + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_PROFILE: prof_name, + light.ATTR_BRIGHTNESS: "bright", + }, + blocking=True, ) with pytest.raises(vol.MultipleInvalid): - await common.async_turn_on(hass, ent1.entity_id, rgb_color="yellowish") + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_RGB_COLOR: "yellowish", + }, + blocking=True, + ) with pytest.raises(vol.MultipleInvalid): - await common.async_turn_on(hass, ent2.entity_id, white_value="high") - - await hass.async_block_till_done() + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ent2.entity_id, light.ATTR_WHITE_VALUE: "high"}, + blocking=True, + ) _, data = ent1.last_call("turn_on") - assert {} == data + assert data == {} _, data = ent2.last_call("turn_on") - assert {} == data + assert data == {} async def test_broken_light_profiles(hass, mock_storage): @@ -346,12 +454,17 @@ async def test_light_profiles(hass, mock_storage): ent1, _, _ = platform.ENTITIES - await common.async_turn_on(hass, ent1.entity_id, profile="test") - - await hass.async_block_till_done() + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_PROFILE: "test", + }, + blocking=True, + ) _, data = ent1.last_call("turn_on") - assert light.is_on(hass, ent1.entity_id) assert data == { light.ATTR_HS_COLOR: (71.059, 100), @@ -359,12 +472,14 @@ async def test_light_profiles(hass, mock_storage): light.ATTR_TRANSITION: 0, } - await common.async_turn_on(hass, ent1.entity_id, profile="test_off") - - await hass.async_block_till_done() + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ent1.entity_id, light.ATTR_PROFILE: "test_off"}, + blocking=True, + ) _, data = ent1.last_call("turn_off") - assert not light.is_on(hass, ent1.entity_id) assert data == {light.ATTR_TRANSITION: 0} @@ -388,12 +503,14 @@ async def test_light_profiles_with_transition(hass, mock_storage): ent1, _, _ = platform.ENTITIES - await common.async_turn_on(hass, ent1.entity_id, profile="test") - - await hass.async_block_till_done() + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ent1.entity_id, light.ATTR_PROFILE: "test"}, + blocking=True, + ) _, data = ent1.last_call("turn_on") - assert light.is_on(hass, ent1.entity_id) assert data == { light.ATTR_HS_COLOR: (71.059, 100), @@ -401,12 +518,14 @@ async def test_light_profiles_with_transition(hass, mock_storage): light.ATTR_TRANSITION: 2, } - await common.async_turn_on(hass, ent1.entity_id, profile="test_off") - - await hass.async_block_till_done() + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ent1.entity_id, light.ATTR_PROFILE: "test_off"}, + blocking=True, + ) _, data = ent1.last_call("turn_off") - assert not light.is_on(hass, ent1.entity_id) assert data == {light.ATTR_TRANSITION: 0} @@ -440,8 +559,10 @@ async def test_default_profiles_group(hass, mock_storage): await hass.async_block_till_done() ent, _, _ = platform.ENTITIES - await common.async_turn_on(hass, ent.entity_id) - await hass.async_block_till_done() + await hass.services.async_call( + light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ent.entity_id}, blocking=True + ) + _, data = ent.last_call("turn_on") assert data == { light.ATTR_HS_COLOR: (71.059, 100), @@ -483,8 +604,15 @@ async def test_default_profiles_light(hass, mock_storage): await hass.async_block_till_done() dev = next(filter(lambda x: x.entity_id == "light.ceiling_2", platform.ENTITIES)) - await common.async_turn_on(hass, dev.entity_id) - await hass.async_block_till_done() + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: dev.entity_id, + }, + blocking=True, + ) + _, data = dev.last_call("turn_on") assert data == { light.ATTR_HS_COLOR: (50.353, 100), @@ -507,8 +635,8 @@ async def test_light_context(hass, hass_admin_user): "light", "toggle", {"entity_id": state.entity_id}, - True, - core.Context(user_id=hass_admin_user.id), + blocking=True, + context=core.Context(user_id=hass_admin_user.id), ) state2 = hass.states.get("light.ceiling") @@ -534,8 +662,8 @@ async def test_light_turn_on_auth(hass, hass_admin_user): "light", "turn_on", {"entity_id": state.entity_id}, - True, - core.Context(user_id=hass_admin_user.id), + blocking=True, + context=core.Context(user_id=hass_admin_user.id), ) @@ -557,7 +685,7 @@ async def test_light_brightness_step(hass): "light", "turn_on", {"entity_id": entity.entity_id, "brightness_step": -10}, - True, + blocking=True, ) _, data = entity.last_call("turn_on") @@ -567,7 +695,7 @@ async def test_light_brightness_step(hass): "light", "turn_on", {"entity_id": entity.entity_id, "brightness_step_pct": 10}, - True, + blocking=True, ) _, data = entity.last_call("turn_on") @@ -592,7 +720,7 @@ async def test_light_brightness_pct_conversion(hass): "light", "turn_on", {"entity_id": entity.entity_id, "brightness_pct": 1}, - True, + blocking=True, ) _, data = entity.last_call("turn_on") @@ -602,7 +730,7 @@ async def test_light_brightness_pct_conversion(hass): "light", "turn_on", {"entity_id": entity.entity_id, "brightness_pct": 2}, - True, + blocking=True, ) _, data = entity.last_call("turn_on") @@ -612,7 +740,7 @@ async def test_light_brightness_pct_conversion(hass): "light", "turn_on", {"entity_id": entity.entity_id, "brightness_pct": 50}, - True, + blocking=True, ) _, data = entity.last_call("turn_on") @@ -622,7 +750,7 @@ async def test_light_brightness_pct_conversion(hass): "light", "turn_on", {"entity_id": entity.entity_id, "brightness_pct": 99}, - True, + blocking=True, ) _, data = entity.last_call("turn_on") @@ -632,7 +760,7 @@ async def test_light_brightness_pct_conversion(hass): "light", "turn_on", {"entity_id": entity.entity_id, "brightness_pct": 100}, - True, + blocking=True, ) _, data = entity.last_call("turn_on") From f6435affe93fba6e043b7d19ec84e9a55aaabca7 Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Sat, 26 Sep 2020 13:30:49 +0200 Subject: [PATCH 366/514] Use common strings for somfy config flow (#40594) --- homeassistant/components/somfy/config_flow.py | 2 +- homeassistant/components/somfy/strings.json | 6 ++++-- tests/components/somfy/test_config_flow.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/somfy/config_flow.py b/homeassistant/components/somfy/config_flow.py index 2d143fbd196..80fc2192d8e 100644 --- a/homeassistant/components/somfy/config_flow.py +++ b/homeassistant/components/somfy/config_flow.py @@ -24,6 +24,6 @@ class SomfyFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): async def async_step_user(self, user_input=None): """Handle a flow start.""" if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="already_setup") + return self.async_abort(reason="single_instance_allowed") return await super().async_step_user(user_input) diff --git a/homeassistant/components/somfy/strings.json b/homeassistant/components/somfy/strings.json index d1fa921bb8e..2f54456091c 100644 --- a/homeassistant/components/somfy/strings.json +++ b/homeassistant/components/somfy/strings.json @@ -4,11 +4,13 @@ "pick_implementation": { "title": "Pick Authentication Method" } }, "abort": { - "already_setup": "You can only configure one Somfy account.", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "authorize_url_timeout": "Timeout generating authorize url.", "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." } + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" + } } } diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py index b1df938b3b1..89b4fbe9b13 100644 --- a/tests/components/somfy/test_config_flow.py +++ b/tests/components/somfy/test_config_flow.py @@ -49,7 +49,7 @@ async def test_abort_if_existing_entry(hass): result = await flow.async_step_user() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_setup" + assert result["reason"] == "single_instance_allowed" async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): From 445743930b18949af154c46d72d02f4516b37f59 Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Sat, 26 Sep 2020 17:20:19 +0200 Subject: [PATCH 367/514] Use common strings for monoprice config flow (#40592) --- homeassistant/components/monoprice/strings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/monoprice/strings.json b/homeassistant/components/monoprice/strings.json index c25fb901d76..008c182f41b 100644 --- a/homeassistant/components/monoprice/strings.json +++ b/homeassistant/components/monoprice/strings.json @@ -15,11 +15,11 @@ } }, "error": { - "cannot_connect": "Failed to connect, please try again", - "unknown": "Unexpected error" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "Device is already configured" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, "options": { @@ -37,4 +37,4 @@ } } } -} \ No newline at end of file +} From 6977425d042f5089374d25fc1dadac7e3bd91f9d Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Sat, 26 Sep 2020 17:27:18 +0200 Subject: [PATCH 368/514] Use common strings for Freebox config flow (#40590) --- homeassistant/components/freebox/strings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/freebox/strings.json b/homeassistant/components/freebox/strings.json index 0fdc4571a1d..2257e7bd908 100644 --- a/homeassistant/components/freebox/strings.json +++ b/homeassistant/components/freebox/strings.json @@ -15,11 +15,11 @@ }, "error": { "register_failed": "Failed to register, please try again", - "connection_failed": "Failed to connect, please try again", - "unknown": "Unknown error: please retry later" + "connection_failed": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "Host already configured" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } -} \ No newline at end of file +} From 2f4aa35ca64045844591c06a69e6f5b3a9c87836 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sat, 26 Sep 2020 12:46:02 -0300 Subject: [PATCH 369/514] Bump python-broadlink to 0.15.0 (#39228) * Rename DeviceOfflineError to NetworkTimeoutError * Bump python-broadlink to 0.15 --- homeassistant/components/broadlink/config_flow.py | 6 +++--- homeassistant/components/broadlink/device.py | 4 ++-- homeassistant/components/broadlink/manifest.json | 2 +- homeassistant/components/broadlink/remote.py | 6 +++--- homeassistant/components/broadlink/updater.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/broadlink/test_config_flow.py | 12 ++++++------ tests/components/broadlink/test_device.py | 14 +++++++------- 9 files changed, 26 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index 4dfc80c6fe9..9fe83e350cf 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -7,7 +7,7 @@ import broadlink as blk from broadlink.exceptions import ( AuthenticationError, BroadlinkException, - DeviceOfflineError, + NetworkTimeoutError, ) import voluptuous as vol @@ -139,7 +139,7 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(device.mac.hex()) return await self.async_step_reset(errors=errors) - except DeviceOfflineError as err: + except NetworkTimeoutError as err: errors["base"] = "cannot_connect" err_msg = str(err) @@ -207,7 +207,7 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: await self.hass.async_add_executor_job(device.set_lock, False) - except DeviceOfflineError as err: + except NetworkTimeoutError as err: errors["base"] = "cannot_connect" err_msg = str(err) diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index d05fdfd4df6..51d9b0a497f 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -9,7 +9,7 @@ from broadlink.exceptions import ( AuthorizationError, BroadlinkException, ConnectionClosedError, - DeviceOfflineError, + NetworkTimeoutError, ) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE @@ -82,7 +82,7 @@ class BroadlinkDevice: await self._async_handle_auth_error() return False - except (DeviceOfflineError, OSError) as err: + except (NetworkTimeoutError, OSError) as err: raise ConfigEntryNotReady from err except BroadlinkException as err: diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index 6a630044904..9c6e571ec86 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -2,7 +2,7 @@ "domain": "broadlink", "name": "Broadlink", "documentation": "https://www.home-assistant.io/integrations/broadlink", - "requirements": ["broadlink==0.14.1"], + "requirements": ["broadlink==0.15.0"], "codeowners": ["@danielhiversen", "@felipediel"], "config_flow": true } diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index 7c1ae7349da..32305331a21 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -9,7 +9,7 @@ import logging from broadlink.exceptions import ( AuthorizationError, BroadlinkException, - DeviceOfflineError, + NetworkTimeoutError, ReadError, StorageError, ) @@ -262,7 +262,7 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): try: await self._device.async_request(self._device.api.send_data, code) - except (AuthorizationError, DeviceOfflineError, OSError) as err: + except (AuthorizationError, NetworkTimeoutError, OSError) as err: _LOGGER.error("Failed to send '%s': %s", command, err) break @@ -295,7 +295,7 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): if toggle: code = [code, await self._async_learn_command(command)] - except (AuthorizationError, DeviceOfflineError, OSError) as err: + except (AuthorizationError, NetworkTimeoutError, OSError) as err: _LOGGER.error("Failed to learn '%s': %s", command, err) break diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py index 8b6f1316f52..d36a07a185b 100644 --- a/homeassistant/components/broadlink/updater.py +++ b/homeassistant/components/broadlink/updater.py @@ -9,7 +9,7 @@ from broadlink.exceptions import ( AuthorizationError, BroadlinkException, CommandNotSupportedError, - DeviceOfflineError, + NetworkTimeoutError, StorageError, ) @@ -113,7 +113,7 @@ class BroadlinkRMMini3UpdateManager(BroadlinkUpdateManager): ) devices = await self.device.hass.async_add_executor_job(hello) if not devices: - raise DeviceOfflineError("The device is offline") + raise NetworkTimeoutError("The device is offline") return {} diff --git a/requirements_all.txt b/requirements_all.txt index 771d31c80be..6406f32bad6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -383,7 +383,7 @@ boto3==1.9.252 bravia-tv==1.0.6 # homeassistant.components.broadlink -broadlink==0.14.1 +broadlink==0.15.0 # homeassistant.components.brother brother==0.1.17 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6a86f7538ba..455341a2483 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -204,7 +204,7 @@ bond-api==0.1.8 bravia-tv==1.0.6 # homeassistant.components.broadlink -broadlink==0.14.1 +broadlink==0.15.0 # homeassistant.components.brother brother==0.1.17 diff --git a/tests/components/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py index a7660b03da5..bc0eba069a8 100644 --- a/tests/components/broadlink/test_config_flow.py +++ b/tests/components/broadlink/test_config_flow.py @@ -249,11 +249,11 @@ async def test_flow_auth_authentication_error(hass): assert result["errors"] == {"base": "invalid_auth"} -async def test_flow_auth_device_offline(hass): - """Test we handle a device offline in the auth step.""" +async def test_flow_auth_network_timeout(hass): + """Test we handle a network timeout in the auth step.""" device = get_device("Living Room") mock_api = device.get_mock_api() - mock_api.auth.side_effect = blke.DeviceOfflineError() + mock_api.auth.side_effect = blke.NetworkTimeoutError() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -403,12 +403,12 @@ async def test_flow_unlock_works(hass): assert mock_api.set_lock.call_count == 1 -async def test_flow_unlock_device_offline(hass): - """Test we handle a device offline in the unlock step.""" +async def test_flow_unlock_network_timeout(hass): + """Test we handle a network timeout in the unlock step.""" device = get_device("Living Room") mock_api = device.get_mock_api() mock_api.is_locked = True - mock_api.set_lock.side_effect = blke.DeviceOfflineError + mock_api.set_lock.side_effect = blke.NetworkTimeoutError() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} diff --git a/tests/components/broadlink/test_device.py b/tests/components/broadlink/test_device.py index 1e68921e9bd..d267243aeb9 100644 --- a/tests/components/broadlink/test_device.py +++ b/tests/components/broadlink/test_device.py @@ -62,11 +62,11 @@ async def test_device_setup_authentication_error(hass): } -async def test_device_setup_device_offline(hass): - """Test we handle a device offline.""" +async def test_device_setup_network_timeout(hass): + """Test we handle a network timeout.""" device = get_device("Office") mock_api = device.get_mock_api() - mock_api.auth.side_effect = blke.DeviceOfflineError() + mock_api.auth.side_effect = blke.NetworkTimeoutError() with patch.object( hass.config_entries, "async_forward_entry_setup" @@ -119,11 +119,11 @@ async def test_device_setup_broadlink_exception(hass): assert mock_init.call_count == 0 -async def test_device_setup_update_device_offline(hass): - """Test we handle a device offline in the update step.""" +async def test_device_setup_update_network_timeout(hass): + """Test we handle a network timeout in the update step.""" device = get_device("Office") mock_api = device.get_mock_api() - mock_api.check_sensors.side_effect = blke.DeviceOfflineError() + mock_api.check_sensors.side_effect = blke.NetworkTimeoutError() with patch.object( hass.config_entries, "async_forward_entry_setup" @@ -308,7 +308,7 @@ async def test_device_unload_update_failed(hass): """Test we unload a device that failed the update step.""" device = get_device("Office") mock_api = device.get_mock_api() - mock_api.check_sensors.side_effect = blke.DeviceOfflineError() + mock_api.check_sensors.side_effect = blke.NetworkTimeoutError() with patch.object(hass.config_entries, "async_forward_entry_setup"): _, mock_entry = await device.setup_entry(hass, mock_api=mock_api) From 1d41f024cf1bb24af34c73c7d018a4808fe686c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Z=C3=A1hradn=C3=ADk?= Date: Sat, 26 Sep 2020 18:11:51 +0200 Subject: [PATCH 370/514] Add Modbus cover (#33642) * Add Modbus cover * Fix improper commands written for Modbus cover via coil * Make changes per review comments * Fix default hub not defined Since support for multiple hubs was added, the default hub option was not implemented correctly. Now I added necessary logic to make it work. First hub in a list will be used as a default hub. * Move Cover config under Modbus section * Revert setting up a default hub alias * Make hub mandatory for Cover * Add default scan interval * Read scan_interval from discovery info * Fix linter error * Use default scan interval from Cover platform * Handle polling for Modbus cover directly inside entity * Move covers under hub config * Fix for review comment * Call update() from Cover actuator methods * Fix time validation --- CODEOWNERS | 2 +- homeassistant/components/modbus/__init__.py | 75 +++++- homeassistant/components/modbus/const.py | 10 + homeassistant/components/modbus/cover.py | 244 ++++++++++++++++++ homeassistant/components/modbus/manifest.json | 2 +- 5 files changed, 323 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/modbus/cover.py diff --git a/CODEOWNERS b/CODEOWNERS index 05c3dcf5087..cfaae6bc496 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -262,7 +262,7 @@ homeassistant/components/min_max/* @fabaff homeassistant/components/minecraft_server/* @elmurato homeassistant/components/minio/* @tkislan homeassistant/components/mobile_app/* @robbiet480 -homeassistant/components/modbus/* @adamchengtkc @janiversen +homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik homeassistant/components/monoprice/* @etsinko @OnFreund homeassistant/components/moon/* @fabaff homeassistant/components/mpd/* @fabaff diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 0a7ea08543a..822000cb56a 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -6,29 +6,50 @@ from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpC from pymodbus.transaction import ModbusRtuFramer import voluptuous as vol +from homeassistant.components.cover import ( + DEVICE_CLASSES_SCHEMA as COVER_DEVICE_CLASSES_SCHEMA, +) from homeassistant.const import ( ATTR_STATE, + CONF_COVERS, CONF_DELAY, + CONF_DEVICE_CLASS, CONF_HOST, CONF_METHOD, CONF_NAME, CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SLAVE, CONF_TIMEOUT, CONF_TYPE, EVENT_HOMEASSISTANT_STOP, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform from .const import ( ATTR_ADDRESS, ATTR_HUB, ATTR_UNIT, ATTR_VALUE, + CALL_TYPE_COIL, + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, CONF_BAUDRATE, CONF_BYTESIZE, + CONF_INPUT_TYPE, CONF_PARITY, + CONF_REGISTER, + CONF_STATE_CLOSED, + CONF_STATE_CLOSING, + CONF_STATE_OPEN, + CONF_STATE_OPENING, + CONF_STATUS_REGISTER, + CONF_STATUS_REGISTER_TYPE, CONF_STOPBITS, DEFAULT_HUB, + DEFAULT_SCAN_INTERVAL, + DEFAULT_SLAVE, MODBUS_DOMAIN as DOMAIN, SERVICE_WRITE_COIL, SERVICE_WRITE_REGISTER, @@ -36,9 +57,33 @@ from .const import ( _LOGGER = logging.getLogger(__name__) - BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string}) +COVERS_SCHEMA = vol.All( + cv.has_at_least_one_key(CALL_TYPE_COIL, CONF_REGISTER), + vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): vol.All( + cv.time_period, lambda value: value.total_seconds() + ), + vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_SLAVE, default=DEFAULT_SLAVE): cv.positive_int, + vol.Optional(CONF_STATE_CLOSED, default=0): cv.positive_int, + vol.Optional(CONF_STATE_CLOSING, default=3): cv.positive_int, + vol.Optional(CONF_STATE_OPEN, default=1): cv.positive_int, + vol.Optional(CONF_STATE_OPENING, default=2): cv.positive_int, + vol.Optional(CONF_STATUS_REGISTER): cv.positive_int, + vol.Optional( + CONF_STATUS_REGISTER_TYPE, + default=CALL_TYPE_REGISTER_HOLDING, + ): vol.In([CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]), + vol.Exclusive(CALL_TYPE_COIL, CONF_INPUT_TYPE): cv.positive_int, + vol.Exclusive(CONF_REGISTER, CONF_INPUT_TYPE): cv.positive_int, + } + ), +) + SERIAL_SCHEMA = BASE_SCHEMA.extend( { vol.Required(CONF_BAUDRATE): cv.positive_int, @@ -49,6 +94,7 @@ SERIAL_SCHEMA = BASE_SCHEMA.extend( vol.Required(CONF_STOPBITS): vol.Any(1, 2), vol.Required(CONF_TYPE): "serial", vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout, + vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]), } ) @@ -59,14 +105,10 @@ ETHERNET_SCHEMA = BASE_SCHEMA.extend( vol.Required(CONF_TYPE): vol.Any("tcp", "udp", "rtuovertcp"), vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout, vol.Optional(CONF_DELAY, default=0): cv.positive_int, + vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]), } ) -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.All(cv.ensure_list, [vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA)])}, - extra=vol.ALLOW_EXTRA, -) - SERVICE_WRITE_REGISTER_SCHEMA = vol.Schema( { vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string, @@ -87,13 +129,30 @@ SERVICE_WRITE_COIL_SCHEMA = vol.Schema( } ) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + cv.ensure_list, + [ + vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA), + ], + ), + }, + extra=vol.ALLOW_EXTRA, +) + def setup(hass, config): """Set up Modbus component.""" hass.data[DOMAIN] = hub_collect = {} - for client_config in config[DOMAIN]: - hub_collect[client_config[CONF_NAME]] = ModbusHub(client_config) + for conf_hub in config[DOMAIN]: + hub_collect[conf_hub[CONF_NAME]] = ModbusHub(conf_hub) + + # load platforms + for component, conf_key in (("cover", CONF_COVERS),): + if conf_key in conf_hub: + load_platform(hass, component, DOMAIN, conf_hub, config) def stop_modbus(event): """Stop Modbus service.""" diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index c12c50cdc07..dc29dd626ae 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -46,6 +46,7 @@ ATTR_UNIT = "unit" ATTR_VALUE = "value" SERVICE_WRITE_COIL = "write_coil" SERVICE_WRITE_REGISTER = "write_register" +DEFAULT_SCAN_INTERVAL = 15 # seconds # binary_sensor.py CONF_INPUTS = "inputs" @@ -71,3 +72,12 @@ CONF_UNIT = "temperature_unit" CONF_MAX_TEMP = "max_temp" CONF_MIN_TEMP = "min_temp" CONF_STEP = "temp_step" + +# cover.py +CONF_STATE_OPEN = "state_open" +CONF_STATE_CLOSED = "state_closed" +CONF_STATE_OPENING = "state_opening" +CONF_STATE_CLOSING = "state_closing" +CONF_STATUS_REGISTER = "status_register" +CONF_STATUS_REGISTER_TYPE = "status_register_type" +DEFAULT_SLAVE = 1 diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py new file mode 100644 index 00000000000..a7c9c301ac5 --- /dev/null +++ b/homeassistant/components/modbus/cover.py @@ -0,0 +1,244 @@ +"""Support for Modbus covers.""" +from datetime import timedelta +import logging +from typing import Any, Dict, Optional + +from pymodbus.exceptions import ConnectionException, ModbusException +from pymodbus.pdu import ExceptionResponse + +from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN, CoverEntity +from homeassistant.const import ( + CONF_COVERS, + CONF_DEVICE_CLASS, + CONF_NAME, + CONF_SCAN_INTERVAL, + CONF_SLAVE, +) +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ( + ConfigType, + DiscoveryInfoType, + HomeAssistantType, +) + +from . import ModbusHub +from .const import ( + CALL_TYPE_COIL, + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, + CONF_REGISTER, + CONF_STATE_CLOSED, + CONF_STATE_CLOSING, + CONF_STATE_OPEN, + CONF_STATE_OPENING, + CONF_STATUS_REGISTER, + CONF_STATUS_REGISTER_TYPE, + MODBUS_DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform( + hass: HomeAssistantType, + config: ConfigType, + async_add_entities, + discovery_info: Optional[DiscoveryInfoType] = None, +): + """Read configuration and create Modbus cover.""" + if discovery_info is None: + return + + covers = [] + for cover in discovery_info[CONF_COVERS]: + hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + covers.append(ModbusCover(hub, cover)) + + async_add_entities(covers) + + +class ModbusCover(CoverEntity, RestoreEntity): + """Representation of a Modbus cover.""" + + def __init__( + self, + hub: ModbusHub, + config: Dict[str, Any], + ): + """Initialize the modbus cover.""" + self._hub: ModbusHub = hub + self._coil = config.get(CALL_TYPE_COIL) + self._device_class = config.get(CONF_DEVICE_CLASS) + self._name = config[CONF_NAME] + self._register = config.get(CONF_REGISTER) + self._slave = config[CONF_SLAVE] + self._state_closed = config[CONF_STATE_CLOSED] + self._state_closing = config[CONF_STATE_CLOSING] + self._state_open = config[CONF_STATE_OPEN] + self._state_opening = config[CONF_STATE_OPENING] + self._status_register = config.get(CONF_STATUS_REGISTER) + self._status_register_type = config[CONF_STATUS_REGISTER_TYPE] + self._scan_interval = timedelta(seconds=config[CONF_SCAN_INTERVAL]) + self._value = None + self._available = True + + # If we read cover status from coil, and not from optional status register, + # we interpret boolean value False as closed cover, and value True as open cover. + # Intermediate states are not supported in such a setup. + if self._coil is not None and self._status_register is None: + self._state_closed = False + self._state_open = True + self._state_closing = None + self._state_opening = None + + # If we read cover status from the main register (i.e., an optional + # status register is not specified), we need to make sure the register_type + # is set to "holding". + if self._register is not None and self._status_register is None: + self._status_register = self._register + self._status_register_type = CALL_TYPE_REGISTER_HOLDING + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + state = await self.async_get_last_state() + if not state: + return + self._value = state.state + + async_track_time_interval( + self.hass, lambda arg: self._update(), self._scan_interval + ) + + @property + def device_class(self) -> Optional[str]: + """Return the device class of the sensor.""" + return self._device_class + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return self._value == self._state_opening + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return self._value == self._state_closing + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + return self._value == self._state_closed + + @property + def should_poll(self): + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + + # Handle polling directly in this entity + return False + + def open_cover(self, **kwargs: Any) -> None: + """Open cover.""" + if self._coil is not None: + self._write_coil(True) + else: + self._write_register(self._state_open) + + self._update() + + def close_cover(self, **kwargs: Any) -> None: + """Close cover.""" + if self._coil is not None: + self._write_coil(False) + else: + self._write_register(self._state_closed) + + self._update() + + def _update(self): + """Update the state of the cover.""" + if self._coil is not None and self._status_register is None: + self._value = self._read_coil() + else: + self._value = self._read_status_register() + + self.schedule_update_ha_state() + + def _read_status_register(self) -> Optional[int]: + """Read status register using the Modbus hub slave.""" + try: + if self._status_register_type == CALL_TYPE_REGISTER_INPUT: + result = self._hub.read_input_registers( + self._slave, self._status_register, 1 + ) + else: + result = self._hub.read_holding_registers( + self._slave, self._status_register, 1 + ) + except ConnectionException: + self._available = False + return + + if isinstance(result, (ModbusException, ExceptionResponse)): + self._available = False + return + + value = int(result.registers[0]) + self._available = True + + return value + + def _write_register(self, value): + """Write holding register using the Modbus hub slave.""" + try: + self._hub.write_register(self._slave, self._register, value) + except ConnectionException: + self._available = False + return + + self._available = True + + def _read_coil(self) -> Optional[bool]: + """Read coil using the Modbus hub slave.""" + try: + result = self._hub.read_coils(self._slave, self._coil, 1) + except ConnectionException: + self._available = False + return + + if isinstance(result, (ModbusException, ExceptionResponse)): + self._available = False + return + + value = bool(result.bits[0]) + self._available = True + + return value + + def _write_coil(self, value): + """Write coil using the Modbus hub slave.""" + try: + self._hub.write_coil(self._slave, self._coil, value) + except ConnectionException: + self._available = False + return + + self._available = True diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index a9155c7b628..05e9c39c4b5 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -3,5 +3,5 @@ "name": "Modbus", "documentation": "https://www.home-assistant.io/integrations/modbus", "requirements": ["pymodbus==2.3.0"], - "codeowners": ["@adamchengtkc", "@janiversen"] + "codeowners": ["@adamchengtkc", "@janiversen", "@vzahradnik"] } From 35533407fe8c9900328ec9f9fec9345821dafb5b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 26 Sep 2020 11:36:47 -0500 Subject: [PATCH 371/514] Improve performance of counting and iterating states in templates (#40250) Co-authored-by: Anders Melchiorsen --- homeassistant/core.py | 18 ++++++++++++++++++ homeassistant/helpers/template.py | 20 +++++++++----------- tests/helpers/test_template.py | 11 +++++++++++ tests/test_core.py | 17 +++++++++++++++++ 4 files changed, 55 insertions(+), 11 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index fd34032112b..779d0f975a7 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -923,6 +923,24 @@ class StateMachine: if state.domain in domain_filter ] + @callback + def async_entity_ids_count( + self, domain_filter: Optional[Union[str, Iterable]] = None + ) -> int: + """Count the entity ids that are being tracked. + + This method must be run in the event loop. + """ + if domain_filter is None: + return len(self._states.keys()) + + if isinstance(domain_filter, str): + domain_filter = (domain_filter.lower(),) + + return len( + [None for state in self._states.values() if state.domain in domain_filter] + ) + def all(self, domain_filter: Optional[Union[str, Iterable]] = None) -> List[State]: """Create a list of all states.""" return run_callback_threadsafe( diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index bef6323d10c..aefdacbeeaa 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -9,7 +9,7 @@ import math from operator import attrgetter import random import re -from typing import Any, Iterable, List, Optional, Union +from typing import Any, Generator, Iterable, List, Optional, Union from urllib.parse import urlencode as urllib_urlencode import weakref @@ -425,12 +425,12 @@ class AllStates: def __iter__(self): """Return all states.""" self._collect_all() - return _state_iterator(self._hass, None) + return _state_generator(self._hass, None) def __len__(self) -> int: """Return number of states.""" self._collect_all() - return len(self._hass.states.async_entity_ids()) + return self._hass.states.async_entity_ids_count() def __call__(self, entity_id): """Return the states.""" @@ -465,12 +465,12 @@ class DomainStates: def __iter__(self): """Return the iteration over all the states.""" self._collect_domain() - return _state_iterator(self._hass, self._domain) + return _state_generator(self._hass, self._domain) def __len__(self) -> int: """Return number of states.""" self._collect_domain() - return len(self._hass.states.async_entity_ids(self._domain)) + return self._hass.states.async_entity_ids_count(self._domain) def __repr__(self) -> str: """Representation of Domain States.""" @@ -537,12 +537,10 @@ def _collect_state(hass: HomeAssistantType, entity_id: str) -> None: entity_collect.entities.add(entity_id) -def _state_iterator(hass: HomeAssistantType, domain: Optional[str]) -> Iterable: - """Create an state iterator for a domain or all states.""" - return iter( - TemplateState(hass, state) - for state in sorted(hass.states.async_all(domain), key=attrgetter("entity_id")) - ) +def _state_generator(hass: HomeAssistantType, domain: Optional[str]) -> Generator: + """State generator for a domain or all states.""" + for state in sorted(hass.states.async_all(domain), key=attrgetter("entity_id")): + yield TemplateState(hass, state) def _get_state(hass: HomeAssistantType, entity_id: str) -> Optional[TemplateState]: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index ca36d8612d4..63a1e9de7c2 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2420,3 +2420,14 @@ For loop example getting 3 entity values: assert "sensor0" in result assert "sensor1" in result assert "sun" in result + + +async def test_slice_states(hass): + """Test iterating states with a slice.""" + hass.states.async_set("sensor.test", "23") + + tpl = template.Template( + "{% for states in states | slice(1) -%}{% set state = states | first %}{{ state.entity_id }}{%- endfor %}", + hass, + ) + assert tpl.async_render() == "sensor.test" diff --git a/tests/test_core.py b/tests/test_core.py index f5de9c5f1a1..6c684ae1eac 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1477,3 +1477,20 @@ async def test_async_all(hass): assert { state.entity_id for state in hass.states.async_all(["light", "switch"]) } == {"light.bowl", "light.frog", "switch.link"} + + +async def test_async_entity_ids_count(hass): + """Test async_entity_ids_count.""" + + 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 hass.states.async_entity_ids_count() == 4 + assert hass.states.async_entity_ids_count("light") == 2 + + hass.states.async_set("light.cow", "on") + + assert hass.states.async_entity_ids_count() == 5 + assert hass.states.async_entity_ids_count("light") == 3 From 8837ed35cd6697b3e5ec4496a3400a14a70843b6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 26 Sep 2020 19:28:26 +0200 Subject: [PATCH 372/514] Use direct service calls in tests instead of automation common (#40623) * Use direct service calls in tests instead of automation common * Remove automation common test helpers --- tests/components/automation/common.py | 56 ------------- tests/components/automation/test_init.py | 81 ++++++++++++------- tests/components/geo_location/test_trigger.py | 10 ++- .../homeassistant/triggers/test_event.py | 20 +++-- .../triggers/test_numeric_state.py | 20 +++-- .../homeassistant/triggers/test_state.py | 18 +++-- .../triggers/test_time_pattern.py | 10 ++- tests/components/mqtt/test_trigger.py | 12 ++- tests/components/sun/test_trigger.py | 26 ++++-- tests/components/template/test_trigger.py | 18 +++-- tests/components/zone/test_trigger.py | 10 ++- 11 files changed, 156 insertions(+), 125 deletions(-) delete mode 100644 tests/components/automation/common.py diff --git a/tests/components/automation/common.py b/tests/components/automation/common.py deleted file mode 100644 index 26521f76d31..00000000000 --- a/tests/components/automation/common.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Collection of helper methods. - -All containing methods are legacy helpers that should not be used by new -components. Instead call the service directly. -""" -from homeassistant.components.automation import ( - CONF_SKIP_CONDITION, - DOMAIN, - SERVICE_TRIGGER, -) -from homeassistant.const import ( - ATTR_ENTITY_ID, - ENTITY_MATCH_ALL, - SERVICE_RELOAD, - SERVICE_TOGGLE, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, -) -from homeassistant.loader import bind_hass - - -@bind_hass -async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL): - """Turn on specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data) - - -@bind_hass -async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL): - """Turn off specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data) - - -@bind_hass -async def async_toggle(hass, entity_id=ENTITY_MATCH_ALL): - """Toggle specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - await hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data) - - -@bind_hass -async def async_trigger(hass, entity_id=ENTITY_MATCH_ALL, skip_condition=True): - """Trigger specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - data[CONF_SKIP_CONDITION] = skip_condition - await hass.services.async_call(DOMAIN, SERVICE_TRIGGER, data) - - -@bind_hass -async def async_reload(hass, context=None): - """Reload the automation from config.""" - await hass.services.async_call( - DOMAIN, SERVICE_RELOAD, blocking=True, context=context - ) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 9c38574945d..1cdcfc11dfb 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -10,12 +10,16 @@ from homeassistant.components.automation import ( DOMAIN, EVENT_AUTOMATION_RELOADED, EVENT_AUTOMATION_TRIGGERED, + SERVICE_TRIGGER, ) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, EVENT_HOMEASSISTANT_STARTED, + SERVICE_RELOAD, + SERVICE_TOGGLE, SERVICE_TURN_OFF, + SERVICE_TURN_ON, STATE_OFF, STATE_ON, ) @@ -26,7 +30,6 @@ import homeassistant.util.dt as dt_util from tests.async_mock import Mock, patch from tests.common import assert_setup_component, async_mock_service, mock_restore_cache -from tests.components.automation import common from tests.components.logbook.test_init import MockLazyEventPartialState @@ -402,45 +405,59 @@ async def test_services(hass, calls): await hass.async_block_till_done() assert len(calls) == 1 - await common.async_turn_off(hass, entity_id) - await hass.async_block_till_done() + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: entity_id, + }, + blocking=True, + ) assert not automation.is_on(hass, entity_id) hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 1 - await common.async_toggle(hass, entity_id) - await hass.async_block_till_done() + await hass.services.async_call( + automation.DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) assert automation.is_on(hass, entity_id) hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 2 - await common.async_toggle(hass, entity_id) - await hass.async_block_till_done() - + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TOGGLE, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) assert not automation.is_on(hass, entity_id) hass.bus.async_fire("test_event") await hass.async_block_till_done() assert len(calls) == 2 - await common.async_toggle(hass, entity_id) - await hass.async_block_till_done() - - await common.async_trigger(hass, entity_id) - await hass.async_block_till_done() + await hass.services.async_call( + automation.DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + await hass.services.async_call( + automation.DOMAIN, SERVICE_TRIGGER, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) assert len(calls) == 3 - await common.async_turn_off(hass, entity_id) - await hass.async_block_till_done() - await common.async_trigger(hass, entity_id) - await hass.async_block_till_done() + await hass.services.async_call( + automation.DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + await hass.services.async_call( + automation.DOMAIN, SERVICE_TRIGGER, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) assert len(calls) == 4 - await common.async_turn_on(hass, entity_id) - await hass.async_block_till_done() + await hass.services.async_call( + automation.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) assert automation.is_on(hass, entity_id) @@ -492,10 +509,18 @@ async def test_reload_config_service(hass, calls, hass_admin_user, hass_read_onl }, ): with pytest.raises(Unauthorized): - await common.async_reload(hass, Context(user_id=hass_read_only_user.id)) - await hass.async_block_till_done() - await common.async_reload(hass, Context(user_id=hass_admin_user.id)) - await hass.async_block_till_done() + await hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + context=Context(user_id=hass_read_only_user.id), + blocking=True, + ) + await hass.services.async_call( + automation.DOMAIN, + SERVICE_RELOAD, + context=Context(user_id=hass_admin_user.id), + blocking=True, + ) # De-flake ?! await hass.async_block_till_done() @@ -547,8 +572,7 @@ async def test_reload_config_when_invalid_config(hass, calls): autospec=True, return_value={automation.DOMAIN: "not valid"}, ): - await common.async_reload(hass) - await hass.async_block_till_done() + await hass.services.async_call(automation.DOMAIN, SERVICE_RELOAD, blocking=True) assert hass.states.get("automation.hello") is None @@ -585,8 +609,7 @@ async def test_reload_config_handles_load_fails(hass, calls): "homeassistant.config.load_yaml_config_file", side_effect=HomeAssistantError("bla"), ): - await common.async_reload(hass) - await hass.async_block_till_done() + await hass.services.async_call(automation.DOMAIN, SERVICE_RELOAD, blocking=True) assert hass.states.get("automation.hello") is not None @@ -646,7 +669,9 @@ async def test_automation_stops(hass, calls, service): autospec=True, return_value=config, ): - await common.async_reload(hass) + await hass.services.async_call( + automation.DOMAIN, SERVICE_RELOAD, blocking=True + ) hass.states.async_set(test_entity, "goodbye") await hass.async_block_till_done() diff --git a/tests/components/geo_location/test_trigger.py b/tests/components/geo_location/test_trigger.py index 99ace50e77d..8bf1c6abe15 100644 --- a/tests/components/geo_location/test_trigger.py +++ b/tests/components/geo_location/test_trigger.py @@ -2,11 +2,11 @@ import pytest from homeassistant.components import automation, zone +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.core import Context from homeassistant.setup import async_setup_component from tests.common import async_mock_service, mock_component -from tests.components.automation import common @pytest.fixture @@ -98,8 +98,12 @@ async def test_if_fires_on_zone_enter(hass, calls): ) await hass.async_block_till_done() - await common.async_turn_off(hass) - await hass.async_block_till_done() + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) hass.states.async_set( "geo_location.entity", diff --git a/tests/components/homeassistant/triggers/test_event.py b/tests/components/homeassistant/triggers/test_event.py index 84b7e725f0a..0e4c2674b1b 100644 --- a/tests/components/homeassistant/triggers/test_event.py +++ b/tests/components/homeassistant/triggers/test_event.py @@ -2,11 +2,11 @@ import pytest import homeassistant.components.automation as automation +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.core import Context from homeassistant.setup import async_setup_component from tests.common import async_mock_service, mock_component -from tests.components.automation import common @pytest.fixture @@ -38,11 +38,15 @@ async def test_if_fires_on_event(hass, calls): hass.bus.async_fire("test_event", context=context) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id - await common.async_turn_off(hass) - await hass.async_block_till_done() + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) hass.bus.async_fire("test_event") await hass.async_block_till_done() @@ -66,8 +70,12 @@ async def test_if_fires_on_event_extra_data(hass, calls): await hass.async_block_till_done() assert len(calls) == 1 - await common.async_turn_off(hass) - await hass.async_block_till_done() + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) hass.bus.async_fire("test_event") await hass.async_block_till_done() diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index 932dde91120..09a13f95603 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -8,6 +8,7 @@ import homeassistant.components.automation as automation from homeassistant.components.homeassistant.triggers import ( numeric_state as numeric_state_trigger, ) +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.core import Context from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -19,7 +20,6 @@ from tests.common import ( async_mock_service, mock_component, ) -from tests.components.automation import common @pytest.fixture @@ -59,8 +59,13 @@ async def test_if_fires_on_entity_change_below(hass, calls): # Set above 12 so the automation will fire again hass.states.async_set("test.entity", 12) - await common.async_turn_off(hass) - await hass.async_block_till_done() + + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) hass.states.async_set("test.entity", 9) await hass.async_block_till_done() assert len(calls) == 1 @@ -863,9 +868,12 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): hass.states.async_set("test.entity_1", 9) hass.states.async_set("test.entity_2", 9) await hass.async_block_till_done() - await common.async_turn_off(hass) - await hass.async_block_till_done() - + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) 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 990fc9cc956..688115dc400 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -5,6 +5,7 @@ import pytest import homeassistant.components.automation as automation from homeassistant.components.homeassistant.triggers import state as state_trigger +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.core import Context from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -16,7 +17,6 @@ from tests.common import ( async_mock_service, mock_component, ) -from tests.components.automation import common @pytest.fixture @@ -70,8 +70,12 @@ async def test_if_fires_on_entity_change(hass, calls): assert calls[0].context.parent_id == context.id assert calls[0].data["some"] == "state - test.entity - hello - world - None" - await common.async_turn_off(hass) - await hass.async_block_till_done() + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) hass.states.async_set("test.entity", "planet") await hass.async_block_till_done() assert len(calls) == 1 @@ -394,8 +398,12 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): hass.states.async_set("test.entity_1", "world") hass.states.async_set("test.entity_2", "world") await hass.async_block_till_done() - await common.async_turn_off(hass) - await hass.async_block_till_done() + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() diff --git a/tests/components/homeassistant/triggers/test_time_pattern.py b/tests/components/homeassistant/triggers/test_time_pattern.py index 3d32748c176..f428bcf29bc 100644 --- a/tests/components/homeassistant/triggers/test_time_pattern.py +++ b/tests/components/homeassistant/triggers/test_time_pattern.py @@ -6,12 +6,12 @@ import voluptuous as vol import homeassistant.components.automation as automation import homeassistant.components.homeassistant.triggers.time_pattern as time_pattern +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF 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 @pytest.fixture @@ -55,8 +55,12 @@ async def test_if_fires_when_hour_matches(hass, calls): await hass.async_block_till_done() assert len(calls) == 1 - await common.async_turn_off(hass) - await hass.async_block_till_done() + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) async_fire_time_changed(hass, now.replace(year=now.year + 1, hour=0)) await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py index f0dd76ff1b4..13e96c69adc 100644 --- a/tests/components/mqtt/test_trigger.py +++ b/tests/components/mqtt/test_trigger.py @@ -4,10 +4,10 @@ from unittest import mock import pytest import homeassistant.components.automation as automation +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.setup import async_setup_component from tests.common import async_fire_mqtt_message, async_mock_service, mock_component -from tests.components.automation import common @pytest.fixture @@ -44,11 +44,15 @@ async def test_if_fires_on_topic_match(hass, calls): async_fire_mqtt_message(hass, "test-topic", '{ "hello": "world" }') await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert 'mqtt - test-topic - { "hello": "world" } - world' == calls[0].data["some"] - await common.async_turn_off(hass) - await hass.async_block_till_done() + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) async_fire_mqtt_message(hass, "test-topic", "test_payload") await hass.async_block_till_done() assert len(calls) == 1 diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py index f730dae3cf1..2cb21051200 100644 --- a/tests/components/sun/test_trigger.py +++ b/tests/components/sun/test_trigger.py @@ -5,13 +5,19 @@ import pytest from homeassistant.components import sun import homeassistant.components.automation as automation -from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET +from homeassistant.const import ( + ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SUN_EVENT_SUNRISE, + SUN_EVENT_SUNSET, +) 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 ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE @@ -54,16 +60,24 @@ async def test_sunset_trigger(hass, calls, legacy_patchable_time): }, ) - await common.async_turn_off(hass) - await hass.async_block_till_done() + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) async_fire_time_changed(hass, trigger_time) await hass.async_block_till_done() assert len(calls) == 0 with patch("homeassistant.util.dt.utcnow", return_value=now): - await common.async_turn_on(hass) - await hass.async_block_till_done() + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) async_fire_time_changed(hass, trigger_time) await hass.async_block_till_done() diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py index 300173fdadf..4bda4dc23ca 100644 --- a/tests/components/template/test_trigger.py +++ b/tests/components/template/test_trigger.py @@ -6,6 +6,7 @@ import pytest import homeassistant.components.automation as automation from homeassistant.components.template import trigger as template_trigger +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.core import Context, callback from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -16,7 +17,6 @@ from tests.common import ( async_mock_service, mock_component, ) -from tests.components.automation import common @pytest.fixture @@ -52,8 +52,12 @@ async def test_if_fires_on_change_bool(hass, calls): await hass.async_block_till_done() assert len(calls) == 1 - await common.async_turn_off(hass) - await hass.async_block_till_done() + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) hass.states.async_set("test.entity", "planet") await hass.async_block_till_done() @@ -698,8 +702,12 @@ async def test_if_not_fires_when_turned_off_with_for(hass, calls): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=4)) await hass.async_block_till_done() assert len(calls) == 0 - await common.async_turn_off(hass) - await hass.async_block_till_done() + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=6)) await hass.async_block_till_done() diff --git a/tests/components/zone/test_trigger.py b/tests/components/zone/test_trigger.py index e80f70b10fe..0477f9bead7 100644 --- a/tests/components/zone/test_trigger.py +++ b/tests/components/zone/test_trigger.py @@ -2,11 +2,11 @@ import pytest from homeassistant.components import automation, zone +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.core import Context from homeassistant.setup import async_setup_component from tests.common import async_mock_service, mock_component -from tests.components.automation import common @pytest.fixture @@ -91,8 +91,12 @@ async def test_if_fires_on_zone_enter(hass, calls): ) await hass.async_block_till_done() - await common.async_turn_off(hass) - await hass.async_block_till_done() + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, + blocking=True, + ) hass.states.async_set( "test.entity", "hello", {"latitude": 32.880586, "longitude": -117.237564} From b8f837365c8d8e9da2d589bda4aa2d1be1884f03 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 26 Sep 2020 22:39:02 +0200 Subject: [PATCH 373/514] Bump aioshelly library to version 0.3.3 (#40415) --- .../components/shelly/config_flow.py | 4 ++ homeassistant/components/shelly/manifest.json | 2 +- homeassistant/components/shelly/strings.json | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/shelly/test_config_flow.py | 45 ++++++++++++++----- 6 files changed, 42 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 0ebf70d2f00..b13c4090a10 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -62,6 +62,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): info = await self._async_get_info(host) except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" + except aioshelly.FirmwareUnsupported: + return self.async_abort(reason="unsupported_firmware") except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -133,6 +135,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.info = info = await self._async_get_info(zeroconf_info["host"]) except HTTP_CONNECT_ERRORS: return self.async_abort(reason="cannot_connect") + except aioshelly.FirmwareUnsupported: + return self.async_abort(reason="unsupported_firmware") await self.async_set_unique_id(info["mac"]) self._abort_if_unique_id_configured({CONF_HOST: zeroconf_info["host"]}) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index eebf53dd69c..1757bb28d9d 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.2"], + "requirements": ["aioshelly==0.3.3"], "zeroconf": [{"type": "_http._tcp.local.", "name":"shelly*"}], "codeowners": ["@balloob", "@bieniu"] } diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 16dc331e452..1a7c8c78189 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -24,7 +24,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "unsupported_firmware": "The device is using an unsupported firmware version." } } } diff --git a/requirements_all.txt b/requirements_all.txt index 6406f32bad6..a6b7a549620 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.2 +aioshelly==0.3.3 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 455341a2483..4025059a6b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -134,7 +134,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.shelly -aioshelly==0.3.2 +aioshelly==0.3.3 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 366b52ca4e7..03eb907d09b 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -2,6 +2,7 @@ import asyncio import aiohttp +import aioshelly import pytest from homeassistant import config_entries, setup @@ -113,10 +114,7 @@ async def test_form_errors_get_info(hass, error): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "aioshelly.get_info", - side_effect=exc, - ): + with patch("aioshelly.get_info", side_effect=exc): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.1.1.1"}, @@ -138,10 +136,7 @@ async def test_form_errors_test_connection(hass, error): with patch( "aioshelly.get_info", return_value={"mac": "test-mac", "auth": False} - ), patch( - "aioshelly.Device.create", - new=AsyncMock(side_effect=exc), - ): + ), patch("aioshelly.Device.create", new=AsyncMock(side_effect=exc)): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.1.1.1"}, @@ -179,6 +174,22 @@ async def test_form_already_configured(hass): assert entry.data["host"] == "1.1.1.1" +async def test_form_firmware_unsupported(hass): + """Test we abort if device firmware is unsupported.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("aioshelly.get_info", side_effect=aioshelly.FirmwareUnsupported): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "unsupported_firmware" + + @pytest.mark.parametrize( "error", [ @@ -315,12 +326,22 @@ async def test_zeroconf_already_configured(hass): assert entry.data["host"] == "1.1.1.1" +async def test_zeroconf_firmware_unsupported(hass): + """Test we abort if device firmware is unsupported.""" + with patch("aioshelly.get_info", side_effect=aioshelly.FirmwareUnsupported): + 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"] == "unsupported_firmware" + + async def test_zeroconf_cannot_connect(hass): """Test we get the form.""" - with patch( - "aioshelly.get_info", - side_effect=asyncio.TimeoutError, - ): + 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"}, From 3261a904da4e3dd2254096370969d964e18b7cbc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 26 Sep 2020 16:29:49 -0500 Subject: [PATCH 374/514] Reduce the number of template re-renders when we are only counting states (#40272) --- homeassistant/core.py | 4 +-- homeassistant/helpers/event.py | 21 +++++++++--- homeassistant/helpers/template.py | 54 ++++++++++++++++++++++++------- tests/helpers/test_template.py | 32 +++++++++++++++--- 4 files changed, 90 insertions(+), 21 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 779d0f975a7..bfb88ab6bfd 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -912,7 +912,7 @@ class StateMachine: This method must be run in the event loop. """ if domain_filter is None: - return list(self._states.keys()) + return list(self._states) if isinstance(domain_filter, str): domain_filter = (domain_filter.lower(),) @@ -932,7 +932,7 @@ class StateMachine: This method must be run in the event loop. """ if domain_filter is None: - return len(self._states.keys()) + return len(self._states) if isinstance(domain_filter, str): domain_filter = (domain_filter.lower(),) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index f2f8b5ac974..5ae1a2bf23a 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -602,7 +602,10 @@ class _TrackTemplateResultInfo: template = track_template_.template # Tracking all states - if self._info[template].all_states: + if ( + self._info[template].all_states + or self._info[template].all_states_lifecycle + ): return True # Previous call had an exception @@ -719,6 +722,9 @@ class _TrackTemplateResultInfo: @callback def _refresh(self, event: Optional[Event]) -> None: entity_id = event and event.data.get(ATTR_ENTITY_ID) + lifecycle_event = event and ( + event.data.get("new_state") is None or event.data.get("old_state") is None + ) updates = [] info_changed = False @@ -726,13 +732,18 @@ class _TrackTemplateResultInfo: template = track_template_.template if ( entity_id - and len(self._last_info) > 1 - and not self._last_info[template].filter_lifecycle(entity_id) + and not self._last_info[template].filter(entity_id) + and ( + not lifecycle_event + or not self._last_info[template].filter_lifecycle(entity_id) + ) ): continue _LOGGER.debug( - "Template update %s triggered by event: %s", template.template, event + "Template update %s triggered by event: %s", + template.template, + event, ) self._info[template] = template.async_render_to_info( @@ -1229,4 +1240,6 @@ def _entities_domains_from_info(render_infos: Iterable[RenderInfo]) -> Tuple[Set entities.update(render_info.entities) if render_info.domains: domains.update(render_info.domains) + if render_info.domains_lifecycle: + domains.update(render_info.domains_lifecycle) return entities, domains diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index aefdacbeeaa..f5d8bc6a6ae 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -164,6 +164,10 @@ def _true(arg: Any) -> bool: return True +def _false(arg: Any) -> bool: + return False + + class RenderInfo: """Holds information about a template render.""" @@ -172,23 +176,30 @@ class RenderInfo: self.template = template # Will be set sensibly once frozen. self.filter_lifecycle = _true + self.filter = _true self._result = None self.is_static = False self.exception = None self.all_states = False + self.all_states_lifecycle = False self.domains = set() + self.domains_lifecycle = set() self.entities = set() - def filter(self, entity_id: str) -> bool: - """Template should re-render if the state changes.""" - return entity_id in self.entities + def __repr__(self) -> str: + """Representation of RenderInfo.""" + return f"" - def _filter_lifecycle(self, entity_id: str) -> bool: - """Template should re-render if the state changes.""" + def _filter_domains_and_entities(self, entity_id: str) -> bool: + """Template should re-render if the entity state changes when we match specific domains or entities.""" return ( split_entity_id(entity_id)[0] in self.domains or entity_id in self.entities ) + def _filter_lifecycle_domains(self, entity_id: str) -> bool: + """Template should re-render if the entity is added or removed with domains watched.""" + return split_entity_id(entity_id)[0] in self.domains_lifecycle + def result(self) -> str: """Results of the template computation.""" if self.exception is not None: @@ -199,19 +210,30 @@ class RenderInfo: self.is_static = True self.entities = frozenset(self.entities) self.domains = frozenset(self.domains) + self.domains_lifecycle = frozenset(self.domains_lifecycle) self.all_states = False def _freeze(self) -> None: self.entities = frozenset(self.entities) self.domains = frozenset(self.domains) + self.domains_lifecycle = frozenset(self.domains_lifecycle) - if self.all_states or self.exception: + if self.exception: return - if not self.domains: - self.filter_lifecycle = self.filter + if not self.all_states_lifecycle: + if self.domains_lifecycle: + self.filter_lifecycle = self._filter_lifecycle_domains + else: + self.filter_lifecycle = _false + + if self.all_states: + return + + if self.entities or self.domains: + self.filter = self._filter_domains_and_entities else: - self.filter_lifecycle = self._filter_lifecycle + self.filter = _false class Template: @@ -422,6 +444,11 @@ class AllStates: if render_info is not None: render_info.all_states = True + def _collect_all_lifecycle(self) -> None: + render_info = self._hass.data.get(_RENDER_INFO) + if render_info is not None: + render_info.all_states_lifecycle = True + def __iter__(self): """Return all states.""" self._collect_all() @@ -429,7 +456,7 @@ class AllStates: def __len__(self) -> int: """Return number of states.""" - self._collect_all() + self._collect_all_lifecycle() return self._hass.states.async_entity_ids_count() def __call__(self, entity_id): @@ -462,6 +489,11 @@ class DomainStates: if entity_collect is not None: entity_collect.domains.add(self._domain) + def _collect_domain_lifecycle(self) -> None: + entity_collect = self._hass.data.get(_RENDER_INFO) + if entity_collect is not None: + entity_collect.domains_lifecycle.add(self._domain) + def __iter__(self): """Return the iteration over all the states.""" self._collect_domain() @@ -469,7 +501,7 @@ class DomainStates: def __len__(self) -> int: """Return number of states.""" - self._collect_domain() + self._collect_domain_lifecycle() return self._hass.states.async_entity_ids_count(self._domain) def __repr__(self) -> str: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 63a1e9de7c2..7cfdd4241b7 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -54,16 +54,17 @@ def assert_result_info(info, result, entities=None, domains=None, all_states=Fal """Check result info.""" assert info.result() == result assert info.all_states == all_states - assert info.filter_lifecycle("invalid_entity_name.somewhere") == all_states + assert info.filter("invalid_entity_name.somewhere") == all_states if entities is not None: assert info.entities == frozenset(entities) assert all([info.filter(entity) for entity in entities]) - assert not info.filter("invalid_entity_name.somewhere") + if not all_states: + assert not info.filter("invalid_entity_name.somewhere") else: assert not info.entities if domains is not None: assert info.domains == frozenset(domains) - assert all([info.filter_lifecycle(domain + ".entity") for domain in domains]) + assert all([info.filter(domain + ".entity") for domain in domains]) else: assert not hasattr(info, "_domains") @@ -1958,7 +1959,8 @@ def test_generate_select(hass): tmp = template.Template(template_str, hass) info = tmp.async_render_to_info() - assert_result_info(info, "", [], ["sensor"]) + assert_result_info(info, "", [], []) + assert info.domains_lifecycle == {"sensor"} hass.states.async_set("sensor.test_sensor", "off", {"attr": "value"}) hass.states.async_set("sensor.test_sensor_on", "on") @@ -1970,6 +1972,7 @@ def test_generate_select(hass): ["sensor.test_sensor", "sensor.test_sensor_on"], ["sensor"], ) + assert info.domains_lifecycle == {"sensor"} async def test_async_render_to_info_in_conditional(hass): @@ -2431,3 +2434,24 @@ async def test_slice_states(hass): hass, ) assert tpl.async_render() == "sensor.test" + + +async def test_lifecycle(hass): + """Test that we limit template render info for lifecycle events.""" + hass.states.async_set("sun.sun", "above", {"elevation": 50, "next_rising": "later"}) + for i in range(2): + hass.states.async_set(f"sensor.sensor{i}", "on") + + tmp = template.Template("{{ states | count }}", hass) + + info = tmp.async_render_to_info() + assert info.all_states is False + assert info.all_states_lifecycle is True + assert info.entities == set() + assert info.domains == set() + assert info.domains_lifecycle == set() + + assert info.filter("sun.sun") is False + assert info.filter("sensor.sensor1") is False + assert info.filter_lifecycle("sensor.new") is True + assert info.filter_lifecycle("sensor.removed") is True From 57b75598320f710d904ed544caab841d9b31415a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 26 Sep 2020 17:03:32 -0500 Subject: [PATCH 375/514] Ensure all jinja2 errors are trapped and displayed in the developer tools (#40624) Co-authored-by: Paulus Schoutsen --- .../components/websocket_api/commands.py | 30 ++++++------ .../components/websocket_api/const.py | 1 + homeassistant/helpers/event.py | 13 ++++-- homeassistant/helpers/template.py | 2 +- .../components/websocket_api/test_commands.py | 46 +++++++++++++++++-- tests/helpers/test_event.py | 19 ++++++++ 6 files changed, 89 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 8995f075f32..d80c7934dd4 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -17,6 +17,7 @@ from homeassistant.exceptions import ( from homeassistant.helpers import config_validation as cv, entity from homeassistant.helpers.event import TrackTemplate, async_track_template_result from homeassistant.helpers.service import async_get_all_descriptions +from homeassistant.helpers.template import Template from homeassistant.loader import IntegrationNotFound, async_get_integration from . import const, decorators, messages @@ -242,16 +243,15 @@ def handle_ping(hass, connection, msg): @decorators.websocket_command( { vol.Required("type"): "render_template", - vol.Required("template"): cv.template, + vol.Required("template"): str, vol.Optional("entity_ids"): cv.entity_ids, vol.Optional("variables"): dict, } ) def handle_render_template(hass, connection, msg): """Handle render_template command.""" - template = msg["template"] - template.hass = hass - + template_str = msg["template"] + template = Template(template_str, hass) variables = msg.get("variables") info = None @@ -261,13 +261,8 @@ def handle_render_template(hass, connection, msg): track_template_result = updates.pop() result = track_template_result.result if isinstance(result, TemplateError): - _LOGGER.error( - "TemplateError('%s') " "while processing template '%s'", - result, - track_template_result.template, - ) - - result = None + connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(result)) + return connection.send_message( messages.event_message( @@ -275,9 +270,16 @@ def handle_render_template(hass, connection, msg): ) ) - info = async_track_template_result( - hass, [TrackTemplate(template, variables)], _template_listener - ) + try: + info = async_track_template_result( + hass, + [TrackTemplate(template, variables)], + _template_listener, + raise_on_template_error=True, + ) + except TemplateError as ex: + connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex)) + return connection.subscriptions[msg["id"]] = info.async_remove diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index f01a2880b9d..5f2cfb2257d 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -29,6 +29,7 @@ ERR_UNKNOWN_COMMAND = "unknown_command" ERR_UNKNOWN_ERROR = "unknown_error" ERR_UNAUTHORIZED = "unauthorized" ERR_TIMEOUT = "timeout" +ERR_TEMPLATE_ERROR = "template_error" TYPE_RESULT = "result" diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 5ae1a2bf23a..d7b1f171a51 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -565,7 +565,7 @@ class _TrackTemplateResultInfo: self._last_domains: Set = set() self._last_entities: Set = set() - def async_setup(self) -> None: + def async_setup(self, raise_on_template_error: bool) -> None: """Activation of template tracking.""" for track_template_ in self._track_templates: template = track_template_.template @@ -573,6 +573,8 @@ class _TrackTemplateResultInfo: self._info[template] = template.async_render_to_info(variables) if self._info[template].exception: + if raise_on_template_error: + raise self._info[template].exception _LOGGER.error( "Error while processing template: %s", track_template_.template, @@ -812,6 +814,7 @@ def async_track_template_result( hass: HomeAssistant, track_templates: Iterable[TrackTemplate], action: TrackTemplateResultListener, + raise_on_template_error: bool = False, ) -> _TrackTemplateResultInfo: """Add a listener that fires when a the result of a template changes. @@ -833,9 +836,13 @@ def async_track_template_result( Home assistant object. track_templates An iterable of TrackTemplate. - action Callable to call with results. + raise_on_template_error + When set to True, if there is an exception + processing the template during setup, the system + will raise the exception instead of setting up + tracking. Returns ------- @@ -843,7 +850,7 @@ def async_track_template_result( """ tracker = _TrackTemplateResultInfo(hass, track_templates, action) - tracker.async_setup() + tracker.async_setup(raise_on_template_error) return tracker diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index f5d8bc6a6ae..5564024a92b 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -266,7 +266,7 @@ class Template: try: self._compiled_code = self._env.compile(self.template) - except jinja2.exceptions.TemplateSyntaxError as err: + except jinja2.TemplateError as err: raise TemplateError(err) from err def extract_entities( diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 1b9eea86018..ea6f2f42bdc 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -484,21 +484,59 @@ async def test_render_template_with_error( ) msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR + + assert "TemplateError" not in caplog.text + + +async def test_render_template_with_delayed_error( + hass, websocket_client, hass_admin_user, caplog +): + """Test a template with an error that only happens after a state change.""" + hass.states.async_set("sensor.test", "on") + await hass.async_block_till_done() + + template_str = """ +{% if states.sensor.test.state %} + on +{% else %} + {{ explode + 1 }} +{% endif %} + """ + + await websocket_client.send_json( + {"id": 5, "type": "render_template", "template": template_str} + ) + await hass.async_block_till_done() + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 assert msg["type"] == const.TYPE_RESULT assert msg["success"] + hass.states.async_remove("sensor.test") + await hass.async_block_till_done() + msg = await websocket_client.receive_json() assert msg["id"] == 5 assert msg["type"] == "event" event = msg["event"] assert event == { - "result": None, - "listeners": {"all": True, "domains": [], "entities": []}, + "result": "on", + "listeners": {"all": False, "domains": [], "entities": ["sensor.test"]}, } - assert "my_unknown_var" in caplog.text - assert "TemplateError" in caplog.text + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR + + assert "TemplateError" not in caplog.text async def test_render_template_returns_with_match_all( diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 479984b97f1..8bdf9cb891c 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -1460,6 +1460,25 @@ async def test_async_track_template_result_multiple_templates_mixing_domain(hass ] +async def test_async_track_template_result_raise_on_template_error(hass): + """Test that we raise as soon as we encounter a failed template.""" + + with pytest.raises(TemplateError): + async_track_template_result( + hass, + [ + TrackTemplate( + Template( + "{{ states.switch | function_that_does_not_exist | list }}" + ), + None, + ), + ], + ha.callback(lambda event, updates: None), + raise_on_template_error=True, + ) + + async def test_track_same_state_simple_no_trigger(hass): """Test track_same_change with no trigger.""" callback_runs = [] From 2b00d28af9d7a67cca255bbafbb88fd0b3b2eaf9 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 27 Sep 2020 00:06:49 +0000 Subject: [PATCH 376/514] [ci skip] Translation update --- .../accuweather/translations/de.json | 13 ++++ .../alarmdecoder/translations/de.json | 7 ++ .../components/awair/translations/de.json | 8 +++ .../components/bond/translations/de.json | 3 +- .../components/broadlink/translations/de.json | 16 +++++ .../components/broadlink/translations/pl.json | 2 +- .../components/bsblan/translations/nl.json | 1 + .../components/canary/translations/de.json | 11 ++++ .../components/canary/translations/et.json | 14 ++++ .../components/control4/translations/de.json | 13 ++++ .../components/control4/translations/nl.json | 11 ++++ .../components/denonavr/translations/de.json | 11 ++++ .../components/dexcom/translations/de.json | 3 +- .../components/dexcom/translations/nl.json | 11 ++++ .../components/doorbird/translations/nl.json | 3 +- .../components/enocean/translations/de.json | 3 +- .../flick_electric/translations/de.json | 3 +- .../flick_electric/translations/nl.json | 3 +- .../components/flo/translations/de.json | 12 ++++ .../components/flo/translations/nl.json | 11 ++++ .../components/freebox/translations/ca.json | 6 +- .../components/freebox/translations/en.json | 6 +- .../components/freebox/translations/ru.json | 6 +- .../components/hlk_sw16/translations/de.json | 12 ++++ .../components/hlk_sw16/translations/nl.json | 11 ++++ .../huawei_lte/translations/de.json | 2 + .../components/hue/translations/de.json | 3 + .../hvv_departures/translations/de.json | 1 + .../hvv_departures/translations/nl.json | 11 ++++ .../components/insteon/translations/de.json | 66 +++++++++++++++++++ .../components/insteon/translations/nl.json | 22 +++++++ .../components/isy994/translations/nl.json | 9 ++- .../components/kodi/translations/de.json | 30 +++++++++ .../components/kodi/translations/nl.json | 5 ++ .../components/metoffice/translations/de.json | 12 ++++ .../components/mill/translations/nl.json | 11 ++++ .../components/monoprice/translations/ca.json | 2 +- .../components/monoprice/translations/en.json | 2 +- .../components/monoprice/translations/ru.json | 4 +- .../components/mqtt/translations/ca.json | 2 +- .../components/mqtt/translations/de.json | 5 +- .../components/mqtt/translations/nl.json | 9 +++ .../nightscout/translations/de.json | 12 ++++ .../components/nzbget/translations/de.json | 13 +++- .../components/nzbget/translations/nl.json | 3 +- .../components/omnilogic/translations/ca.json | 30 +++++++++ .../components/omnilogic/translations/de.json | 13 ++++ .../components/omnilogic/translations/nl.json | 12 ++++ .../omnilogic/translations/zh-Hant.json | 30 +++++++++ .../openweathermap/translations/de.json | 3 +- .../ovo_energy/translations/de.json | 12 ++++ .../ovo_energy/translations/nl.json | 11 ++++ .../components/plex/translations/de.json | 1 + .../components/plugwise/translations/de.json | 1 + .../components/plugwise/translations/et.json | 3 +- .../plum_lightpad/translations/de.json | 12 ++++ .../components/poolsense/translations/de.json | 1 + .../progettihwsw/translations/de.json | 32 +++++++++ .../components/risco/translations/de.json | 24 +++++++ .../components/roon/translations/de.json | 3 + .../components/sentry/translations/de.json | 3 + .../components/sharkiq/translations/de.json | 18 +++++ .../components/sharkiq/translations/nl.json | 5 ++ .../components/shelly/translations/ca.json | 3 +- .../components/shelly/translations/de.json | 16 +++++ .../components/shelly/translations/en.json | 3 +- .../components/shelly/translations/nl.json | 5 ++ .../simplisafe/translations/de.json | 5 ++ .../components/smappee/translations/de.json | 12 ++++ .../smart_meter_texas/translations/de.json | 13 ++++ .../smart_meter_texas/translations/nl.json | 11 ++++ .../components/sms/translations/de.json | 11 ++++ .../components/somfy/translations/ca.json | 5 +- .../components/somfy/translations/en.json | 5 +- .../components/somfy/translations/ru.json | 3 +- .../components/spider/translations/de.json | 12 ++++ .../components/spider/translations/nl.json | 11 ++++ .../squeezebox/translations/de.json | 9 ++- .../squeezebox/translations/nl.json | 11 ++++ .../components/syncthru/translations/de.json | 1 + .../transmission/translations/de.json | 1 + .../components/wilight/translations/de.json | 10 +++ .../components/wolflink/translations/de.json | 17 +++++ .../components/wolflink/translations/nl.json | 11 ++++ .../wolflink/translations/sensor.de.json | 8 +++ .../xiaomi_aqara/translations/de.json | 10 +++ .../xiaomi_miio/translations/de.json | 1 + .../components/yeelight/translations/de.json | 9 ++- .../zoneminder/translations/de.json | 14 ++++ .../zoneminder/translations/et.json | 1 + .../zoneminder/translations/nl.json | 1 + 91 files changed, 821 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/accuweather/translations/de.json create mode 100644 homeassistant/components/broadlink/translations/de.json create mode 100644 homeassistant/components/control4/translations/de.json create mode 100644 homeassistant/components/control4/translations/nl.json create mode 100644 homeassistant/components/denonavr/translations/de.json create mode 100644 homeassistant/components/dexcom/translations/nl.json create mode 100644 homeassistant/components/flo/translations/de.json create mode 100644 homeassistant/components/flo/translations/nl.json create mode 100644 homeassistant/components/hlk_sw16/translations/de.json create mode 100644 homeassistant/components/hlk_sw16/translations/nl.json create mode 100644 homeassistant/components/hvv_departures/translations/nl.json create mode 100644 homeassistant/components/insteon/translations/de.json create mode 100644 homeassistant/components/kodi/translations/de.json create mode 100644 homeassistant/components/metoffice/translations/de.json create mode 100644 homeassistant/components/mill/translations/nl.json create mode 100644 homeassistant/components/nightscout/translations/de.json create mode 100644 homeassistant/components/omnilogic/translations/ca.json create mode 100644 homeassistant/components/omnilogic/translations/de.json create mode 100644 homeassistant/components/omnilogic/translations/nl.json create mode 100644 homeassistant/components/omnilogic/translations/zh-Hant.json create mode 100644 homeassistant/components/ovo_energy/translations/de.json create mode 100644 homeassistant/components/ovo_energy/translations/nl.json create mode 100644 homeassistant/components/plum_lightpad/translations/de.json create mode 100644 homeassistant/components/progettihwsw/translations/de.json create mode 100644 homeassistant/components/risco/translations/de.json create mode 100644 homeassistant/components/roon/translations/de.json create mode 100644 homeassistant/components/sharkiq/translations/de.json create mode 100644 homeassistant/components/smappee/translations/de.json create mode 100644 homeassistant/components/smart_meter_texas/translations/de.json create mode 100644 homeassistant/components/smart_meter_texas/translations/nl.json create mode 100644 homeassistant/components/sms/translations/de.json create mode 100644 homeassistant/components/spider/translations/de.json create mode 100644 homeassistant/components/spider/translations/nl.json create mode 100644 homeassistant/components/squeezebox/translations/nl.json create mode 100644 homeassistant/components/wilight/translations/de.json create mode 100644 homeassistant/components/wolflink/translations/de.json create mode 100644 homeassistant/components/wolflink/translations/nl.json create mode 100644 homeassistant/components/wolflink/translations/sensor.de.json create mode 100644 homeassistant/components/xiaomi_aqara/translations/de.json create mode 100644 homeassistant/components/zoneminder/translations/de.json diff --git a/homeassistant/components/accuweather/translations/de.json b/homeassistant/components/accuweather/translations/de.json new file mode 100644 index 00000000000..320c834e920 --- /dev/null +++ b/homeassistant/components/accuweather/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad" + }, + "title": "AccuWeather" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/de.json b/homeassistant/components/alarmdecoder/translations/de.json index cae97cea6a2..69318f87b11 100644 --- a/homeassistant/components/alarmdecoder/translations/de.json +++ b/homeassistant/components/alarmdecoder/translations/de.json @@ -4,6 +4,12 @@ "service_unavailable": "Verbindung konnte nicht hergestellt werden" }, "step": { + "protocol": { + "data": { + "host": "Host", + "port": "Port" + } + }, "user": { "data": { "protocol": "Protokoll" @@ -27,6 +33,7 @@ "zone_details": { "data": { "zone_name": "Zonenname", + "zone_relayaddr": "Relais-Adresse", "zone_type": "Zonentyp" } }, diff --git a/homeassistant/components/awair/translations/de.json b/homeassistant/components/awair/translations/de.json index d0eb8e91b3e..fcdcd0190e3 100644 --- a/homeassistant/components/awair/translations/de.json +++ b/homeassistant/components/awair/translations/de.json @@ -5,7 +5,15 @@ }, "step": { "reauth": { + "data": { + "email": "E-Mail" + }, "description": "Bitte geben Sie Ihr Awair-Entwicklerzugriffstoken erneut ein." + }, + "user": { + "data": { + "email": "E-Mail" + } } } } diff --git a/homeassistant/components/bond/translations/de.json b/homeassistant/components/bond/translations/de.json index d10ea1e71b0..393232025dd 100644 --- a/homeassistant/components/bond/translations/de.json +++ b/homeassistant/components/bond/translations/de.json @@ -7,7 +7,8 @@ "step": { "user": { "data": { - "access_token": "Zugriffstoken" + "access_token": "Zugriffstoken", + "host": "Host" } } } diff --git a/homeassistant/components/broadlink/translations/de.json b/homeassistant/components/broadlink/translations/de.json new file mode 100644 index 00000000000..b0d7a55c787 --- /dev/null +++ b/homeassistant/components/broadlink/translations/de.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "finish": { + "data": { + "name": "Name" + } + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/pl.json b/homeassistant/components/broadlink/translations/pl.json index a168d020d02..64afc759689 100644 --- a/homeassistant/components/broadlink/translations/pl.json +++ b/homeassistant/components/broadlink/translations/pl.json @@ -24,7 +24,7 @@ "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).", + "description": "Twoje urz\u0105dzenie jest zablokowane. Post\u0119puj zgodnie z instrukcjami, aby je odblokowa\u0107: \nOpcja 1 (preferowana):\n1. W aplikacji Broadlink wejd\u017a w swoje urz\u0105dzenie\n2. Kliknij \"\u2022 \u2022 \u2022\" \n3. Wy\u0142\u0105cz opcje \"Lock Device\"\n\nOpcja 2:\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": { diff --git a/homeassistant/components/bsblan/translations/nl.json b/homeassistant/components/bsblan/translations/nl.json index c1909b19508..c92ccbebb62 100644 --- a/homeassistant/components/bsblan/translations/nl.json +++ b/homeassistant/components/bsblan/translations/nl.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "host": "Host", "port": "Poort" } } diff --git a/homeassistant/components/canary/translations/de.json b/homeassistant/components/canary/translations/de.json index c495accb16f..159f961c3a6 100644 --- a/homeassistant/components/canary/translations/de.json +++ b/homeassistant/components/canary/translations/de.json @@ -1,4 +1,15 @@ { + "config": { + "flow_title": "Canary: {name}", + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/canary/translations/et.json b/homeassistant/components/canary/translations/et.json index 8e17e25ee4c..cc6601e6cc5 100644 --- a/homeassistant/components/canary/translations/et.json +++ b/homeassistant/components/canary/translations/et.json @@ -1,4 +1,18 @@ { + "config": { + "error": { + "cannot_connect": "\u00dchendus nurjus" + }, + "flow_title": "Canary {name}", + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/control4/translations/de.json b/homeassistant/components/control4/translations/de.json new file mode 100644 index 00000000000..1653a11c3ed --- /dev/null +++ b/homeassistant/components/control4/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "IP-Addresse", + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/control4/translations/nl.json b/homeassistant/components/control4/translations/nl.json new file mode 100644 index 00000000000..4d00f0bfc74 --- /dev/null +++ b/homeassistant/components/control4/translations/nl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/de.json b/homeassistant/components/denonavr/translations/de.json new file mode 100644 index 00000000000..4fd05d6c578 --- /dev/null +++ b/homeassistant/components/denonavr/translations/de.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "IP-Adresse" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/de.json b/homeassistant/components/dexcom/translations/de.json index af843097539..31ded6b7f9e 100644 --- a/homeassistant/components/dexcom/translations/de.json +++ b/homeassistant/components/dexcom/translations/de.json @@ -10,7 +10,8 @@ "step": { "user": { "data": { - "password": "Passwort" + "password": "Passwort", + "username": "Benutzername" } } } diff --git a/homeassistant/components/dexcom/translations/nl.json b/homeassistant/components/dexcom/translations/nl.json new file mode 100644 index 00000000000..4d00f0bfc74 --- /dev/null +++ b/homeassistant/components/dexcom/translations/nl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/nl.json b/homeassistant/components/doorbird/translations/nl.json index 85180df8b4a..2bf97d687ab 100644 --- a/homeassistant/components/doorbird/translations/nl.json +++ b/homeassistant/components/doorbird/translations/nl.json @@ -13,7 +13,8 @@ "user": { "data": { "host": "Host (IP-adres)", - "name": "Apparaatnaam" + "name": "Apparaatnaam", + "username": "Gebruikersnaam" }, "title": "Maak verbinding met de DoorBird" } diff --git a/homeassistant/components/enocean/translations/de.json b/homeassistant/components/enocean/translations/de.json index 9664031c000..eb98e1fb2b9 100644 --- a/homeassistant/components/enocean/translations/de.json +++ b/homeassistant/components/enocean/translations/de.json @@ -4,5 +4,6 @@ "single_instance_allowed": "Schon konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "flow_title": "ENOcean-Einrichtung" - } + }, + "title": "EnOcean" } \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/de.json b/homeassistant/components/flick_electric/translations/de.json index b69e8de8f7c..d63283fe36b 100644 --- a/homeassistant/components/flick_electric/translations/de.json +++ b/homeassistant/components/flick_electric/translations/de.json @@ -16,5 +16,6 @@ } } } - } + }, + "title": "Flick Electric" } \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/nl.json b/homeassistant/components/flick_electric/translations/nl.json index 5f7433d97db..c4901d328c3 100644 --- a/homeassistant/components/flick_electric/translations/nl.json +++ b/homeassistant/components/flick_electric/translations/nl.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "Wachtwoord" + "password": "Wachtwoord", + "username": "Gebruikersnaam" } } } diff --git a/homeassistant/components/flo/translations/de.json b/homeassistant/components/flo/translations/de.json new file mode 100644 index 00000000000..6f398062876 --- /dev/null +++ b/homeassistant/components/flo/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/nl.json b/homeassistant/components/flo/translations/nl.json new file mode 100644 index 00000000000..4d00f0bfc74 --- /dev/null +++ b/homeassistant/components/flo/translations/nl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/ca.json b/homeassistant/components/freebox/translations/ca.json index 264e0ed3038..d5be94be363 100644 --- a/homeassistant/components/freebox/translations/ca.json +++ b/homeassistant/components/freebox/translations/ca.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "L'amfitri\u00f3 ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { - "connection_failed": "No s'ha pogut connectar, torna-ho a provar", + "connection_failed": "Ha fallat la connexi\u00f3", "register_failed": "No s'ha pogut registrar, torna-ho a provar", - "unknown": "Error desconegut: torna-ho a provar m\u00e9s tard" + "unknown": "Error inesperat" }, "step": { "link": { diff --git a/homeassistant/components/freebox/translations/en.json b/homeassistant/components/freebox/translations/en.json index 15e18a8982b..a0b5c98e72f 100644 --- a/homeassistant/components/freebox/translations/en.json +++ b/homeassistant/components/freebox/translations/en.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Host already configured" + "already_configured": "Device is already configured" }, "error": { - "connection_failed": "Failed to connect, please try again", + "connection_failed": "Failed to connect", "register_failed": "Failed to register, please try again", - "unknown": "Unknown error: please retry later" + "unknown": "Unexpected error" }, "step": { "link": { diff --git a/homeassistant/components/freebox/translations/ru.json b/homeassistant/components/freebox/translations/ru.json index 1bef863e15f..377c0e8002f 100644 --- a/homeassistant/components/freebox/translations/ru.json +++ b/homeassistant/components/freebox/translations/ru.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "\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." + "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": { - "connection_failed": "\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.", + "connection_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "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.", - "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." + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { "link": { diff --git a/homeassistant/components/hlk_sw16/translations/de.json b/homeassistant/components/hlk_sw16/translations/de.json new file mode 100644 index 00000000000..6f398062876 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/nl.json b/homeassistant/components/hlk_sw16/translations/nl.json new file mode 100644 index 00000000000..4d00f0bfc74 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/nl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/de.json b/homeassistant/components/huawei_lte/translations/de.json index 15fd57a3d33..a0fa9914c47 100644 --- a/homeassistant/components/huawei_lte/translations/de.json +++ b/homeassistant/components/huawei_lte/translations/de.json @@ -16,10 +16,12 @@ "response_error": "Unbekannter Fehler vom Ger\u00e4t", "unknown_connection_error": "Unbekannter Fehler beim Herstellen der Verbindung zum Ger\u00e4t" }, + "flow_title": "Huawei LTE: {name}", "step": { "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/hue/translations/de.json b/homeassistant/components/hue/translations/de.json index c9c8c96f4d5..0defb33ae5e 100644 --- a/homeassistant/components/hue/translations/de.json +++ b/homeassistant/components/hue/translations/de.json @@ -26,6 +26,9 @@ "title": "Hub verbinden" }, "manual": { + "data": { + "host": "Host" + }, "title": "Manuelles Konfigurieren einer Hue Bridge" } } diff --git a/homeassistant/components/hvv_departures/translations/de.json b/homeassistant/components/hvv_departures/translations/de.json index e0255ec4637..b383e57bd93 100644 --- a/homeassistant/components/hvv_departures/translations/de.json +++ b/homeassistant/components/hvv_departures/translations/de.json @@ -23,6 +23,7 @@ }, "user": { "data": { + "host": "Host", "password": "Passwort", "username": "Benutzername" }, diff --git a/homeassistant/components/hvv_departures/translations/nl.json b/homeassistant/components/hvv_departures/translations/nl.json new file mode 100644 index 00000000000..4d00f0bfc74 --- /dev/null +++ b/homeassistant/components/hvv_departures/translations/nl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/de.json b/homeassistant/components/insteon/translations/de.json new file mode 100644 index 00000000000..dfefff8c559 --- /dev/null +++ b/homeassistant/components/insteon/translations/de.json @@ -0,0 +1,66 @@ +{ + "config": { + "step": { + "hub2": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + }, + "hubv1": { + "data": { + "host": "IP-Adresse", + "port": "Port" + } + }, + "hubv2": { + "data": { + "host": "IP-Adresse", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + } + }, + "init": { + "title": "Insteon" + }, + "plm": { + "title": "Insteon PLM" + }, + "user": { + "title": "Insteon" + } + } + }, + "options": { + "step": { + "add_override": { + "title": "Insteon" + }, + "add_x10": { + "data": { + "platform": "Plattform" + }, + "title": "Insteon" + }, + "change_hub_config": { + "data": { + "host": "IP-Adresse", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + }, + "title": "Insteon" + }, + "init": { + "title": "Insteon" + }, + "remove_override": { + "title": "Insteon" + }, + "remove_x10": { + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/nl.json b/homeassistant/components/insteon/translations/nl.json index 7a93eb9f843..538755dd013 100644 --- a/homeassistant/components/insteon/translations/nl.json +++ b/homeassistant/components/insteon/translations/nl.json @@ -1,6 +1,16 @@ { "config": { "step": { + "hub2": { + "data": { + "username": "Gebruikersnaam" + } + }, + "hubv2": { + "data": { + "username": "Gebruikersnaam" + } + }, "user": { "data": { "modem_type": "Modemtype." @@ -9,5 +19,17 @@ "title": "Insteon" } } + }, + "options": { + "step": { + "change_hub_config": { + "data": { + "host": "IP-adres", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/nl.json b/homeassistant/components/isy994/translations/nl.json index a39fc58dc25..b252dde3257 100644 --- a/homeassistant/components/isy994/translations/nl.json +++ b/homeassistant/components/isy994/translations/nl.json @@ -1,5 +1,12 @@ { "config": { - "flow_title": "Universele apparaten ISY994 {name} ({host})" + "flow_title": "Universele apparaten ISY994 {name} ({host})", + "step": { + "user": { + "data": { + "username": "Gebruikersnaam" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/de.json b/homeassistant/components/kodi/translations/de.json new file mode 100644 index 00000000000..ac98f2016b4 --- /dev/null +++ b/homeassistant/components/kodi/translations/de.json @@ -0,0 +1,30 @@ +{ + "config": { + "flow_title": "Kodi: {name}", + "step": { + "credentials": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + }, + "host": { + "data": { + "host": "Host", + "port": "Port" + } + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + } + }, + "ws_port": { + "data": { + "ws_port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/nl.json b/homeassistant/components/kodi/translations/nl.json index f6b02d0f84c..235d5a50be6 100644 --- a/homeassistant/components/kodi/translations/nl.json +++ b/homeassistant/components/kodi/translations/nl.json @@ -4,6 +4,11 @@ "unknown": "Onverwachte fout" }, "step": { + "credentials": { + "data": { + "username": "Gebruikersnaam" + } + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/metoffice/translations/de.json b/homeassistant/components/metoffice/translations/de.json new file mode 100644 index 00000000000..55896dc4901 --- /dev/null +++ b/homeassistant/components/metoffice/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/nl.json b/homeassistant/components/mill/translations/nl.json new file mode 100644 index 00000000000..4d00f0bfc74 --- /dev/null +++ b/homeassistant/components/mill/translations/nl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/ca.json b/homeassistant/components/monoprice/translations/ca.json index 6af5204b91e..1e4c623c215 100644 --- a/homeassistant/components/monoprice/translations/ca.json +++ b/homeassistant/components/monoprice/translations/ca.json @@ -4,7 +4,7 @@ "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { - "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", + "cannot_connect": "Ha fallat la connexi\u00f3", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/monoprice/translations/en.json b/homeassistant/components/monoprice/translations/en.json index 9e9f3a4d2cf..08438f8a985 100644 --- a/homeassistant/components/monoprice/translations/en.json +++ b/homeassistant/components/monoprice/translations/en.json @@ -4,7 +4,7 @@ "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "Failed to connect, please try again", + "cannot_connect": "Failed to connect", "unknown": "Unexpected error" }, "step": { diff --git a/homeassistant/components/monoprice/translations/ru.json b/homeassistant/components/monoprice/translations/ru.json index 5b891db80a3..4fb5eb892a1 100644 --- a/homeassistant/components/monoprice/translations/ru.json +++ b/homeassistant/components/monoprice/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": "\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.", + "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/mqtt/translations/ca.json b/homeassistant/components/mqtt/translations/ca.json index e94be2d1a6d..7709c784b9f 100644 --- a/homeassistant/components/mqtt/translations/ca.json +++ b/homeassistant/components/mqtt/translations/ca.json @@ -72,7 +72,7 @@ "birth_retain": "Retenci\u00f3 missatge de naixement", "birth_topic": "Topic missatge de naixement", "discovery": "Activar descobriment", - "will_enable": "Activa el missatge de naixement", + "will_enable": "Activa el missatge d'\u00faltima voluntat", "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/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json index 7256fe2f956..7f34e10fa89 100644 --- a/homeassistant/components/mqtt/translations/de.json +++ b/homeassistant/components/mqtt/translations/de.json @@ -52,7 +52,10 @@ "step": { "broker": { "data": { - "password": "Passwort" + "broker": "Broker", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" } } } diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index 36a51c46802..2b8119aca4d 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -37,5 +37,14 @@ "turn_off": "Uitschakelen", "turn_on": "Inschakelen" } + }, + "options": { + "step": { + "broker": { + "data": { + "username": "Gebruikersnaam" + } + } + } } } \ 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..a7ad0fe1d27 --- /dev/null +++ b/homeassistant/components/nightscout/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "flow_title": "Nightscout", + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/de.json b/homeassistant/components/nzbget/translations/de.json index b89b2d83550..6aa89d2e62e 100644 --- a/homeassistant/components/nzbget/translations/de.json +++ b/homeassistant/components/nzbget/translations/de.json @@ -1,6 +1,17 @@ { "config": { - "flow_title": "NZBGet: {name}" + "flow_title": "NZBGet: {name}", + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + } + } + } }, "options": { "step": { diff --git a/homeassistant/components/nzbget/translations/nl.json b/homeassistant/components/nzbget/translations/nl.json index 900bac61bc5..472952ad8b1 100644 --- a/homeassistant/components/nzbget/translations/nl.json +++ b/homeassistant/components/nzbget/translations/nl.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "name": "Naam" + "name": "Naam", + "username": "Gebruikersnaam" } } } diff --git a/homeassistant/components/omnilogic/translations/ca.json b/homeassistant/components/omnilogic/translations/ca.json new file mode 100644 index 00000000000..53c8755dd36 --- /dev/null +++ b/homeassistant/components/omnilogic/translations/ca.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "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" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "polling_interval": "Interval d'escaneig (segons)" + } + } + } + }, + "title": "Omnilogic" +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/de.json b/homeassistant/components/omnilogic/translations/de.json new file mode 100644 index 00000000000..c4002834589 --- /dev/null +++ b/homeassistant/components/omnilogic/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + }, + "title": "Omnilogic" +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/nl.json b/homeassistant/components/omnilogic/translations/nl.json new file mode 100644 index 00000000000..2f7e9cfbd12 --- /dev/null +++ b/homeassistant/components/omnilogic/translations/nl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Benutzername" + } + } + } + }, + "title": "Omnilogic" +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/zh-Hant.json b/homeassistant/components/omnilogic/translations/zh-Hant.json new file mode 100644 index 00000000000..335e26ced8c --- /dev/null +++ b/homeassistant/components/omnilogic/translations/zh-Hant.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + }, + "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" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "polling_interval": "\u66f4\u65b0\u9593\u8ddd\uff08\u79d2\uff09" + } + } + } + }, + "title": "Omnilogic" +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/de.json b/homeassistant/components/openweathermap/translations/de.json index 7103abba03c..6582b2046b8 100644 --- a/homeassistant/components/openweathermap/translations/de.json +++ b/homeassistant/components/openweathermap/translations/de.json @@ -10,7 +10,8 @@ "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad", "mode": "Modus" - } + }, + "title": "OpenWeatherMap" } } }, diff --git a/homeassistant/components/ovo_energy/translations/de.json b/homeassistant/components/ovo_energy/translations/de.json new file mode 100644 index 00000000000..6f398062876 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/nl.json b/homeassistant/components/ovo_energy/translations/nl.json new file mode 100644 index 00000000000..4d00f0bfc74 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/nl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/translations/de.json b/homeassistant/components/plex/translations/de.json index b14e3a3c574..961ad4b3ed6 100644 --- a/homeassistant/components/plex/translations/de.json +++ b/homeassistant/components/plex/translations/de.json @@ -14,6 +14,7 @@ "not_found": "Plex-Server nicht gefunden", "ssl_error": "SSL-Zertifikatsproblem" }, + "flow_title": "{name} ({host})", "step": { "manual_setup": { "data": { diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index 7ee92b1ead5..19d3678b5e8 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -4,6 +4,7 @@ "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", "unknown": "Unerwarteter Fehler" }, + "flow_title": "Smile: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/et.json b/homeassistant/components/plugwise/translations/et.json index 4b7c8bf1d8d..a1a5d42db59 100644 --- a/homeassistant/components/plugwise/translations/et.json +++ b/homeassistant/components/plugwise/translations/et.json @@ -4,7 +4,8 @@ "init": { "data": { "scan_interval": "P\u00e4ringute intervall (sekundites)" - } + }, + "description": "Kohanda Plugwise s\u00e4tteid" } } } diff --git a/homeassistant/components/plum_lightpad/translations/de.json b/homeassistant/components/plum_lightpad/translations/de.json new file mode 100644 index 00000000000..f55df964f86 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "E-Mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/de.json b/homeassistant/components/poolsense/translations/de.json index d54c09e3cef..8fc660e8128 100644 --- a/homeassistant/components/poolsense/translations/de.json +++ b/homeassistant/components/poolsense/translations/de.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "email": "E-Mail", "password": "Passwort" } } diff --git a/homeassistant/components/progettihwsw/translations/de.json b/homeassistant/components/progettihwsw/translations/de.json new file mode 100644 index 00000000000..f772a8586d0 --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/de.json @@ -0,0 +1,32 @@ +{ + "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", + "relay_9": "Relais 9" + } + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/de.json b/homeassistant/components/risco/translations/de.json new file mode 100644 index 00000000000..b0bae40bf4e --- /dev/null +++ b/homeassistant/components/risco/translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + }, + "options": { + "step": { + "risco_to_ha": { + "data": { + "A": "Gruppe A", + "B": "Gruppe B", + "C": "Gruppe C", + "D": "Gruppe D" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/de.json b/homeassistant/components/roon/translations/de.json new file mode 100644 index 00000000000..b73dc4d6444 --- /dev/null +++ b/homeassistant/components/roon/translations/de.json @@ -0,0 +1,3 @@ +{ + "title": "Roon" +} \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/de.json b/homeassistant/components/sentry/translations/de.json index 5d6e27bd737..6e6d640cd45 100644 --- a/homeassistant/components/sentry/translations/de.json +++ b/homeassistant/components/sentry/translations/de.json @@ -9,6 +9,9 @@ }, "step": { "user": { + "data": { + "dsn": "DSN" + }, "description": "Gib deine Sentry-DSN ein", "title": "Sentry" } diff --git a/homeassistant/components/sharkiq/translations/de.json b/homeassistant/components/sharkiq/translations/de.json new file mode 100644 index 00000000000..5a1d4f2f185 --- /dev/null +++ b/homeassistant/components/sharkiq/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "reauth": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + }, + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/nl.json b/homeassistant/components/sharkiq/translations/nl.json index ec17130cee2..03605190c3c 100644 --- a/homeassistant/components/sharkiq/translations/nl.json +++ b/homeassistant/components/sharkiq/translations/nl.json @@ -6,6 +6,11 @@ "password": "Paswoord", "username": "Gebruikersnaam" } + }, + "user": { + "data": { + "username": "Gebruikersnaam" + } } } } diff --git a/homeassistant/components/shelly/translations/ca.json b/homeassistant/components/shelly/translations/ca.json index 7e8c3873c11..2747ce5b277 100644 --- a/homeassistant/components/shelly/translations/ca.json +++ b/homeassistant/components/shelly/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", + "unsupported_firmware": "El dispositiu utilitza una versi\u00f3 de programari no compatible." }, "error": { "auth_not_supported": "Actualment els dispositius Shelly amb autenticaci\u00f3 no son compatibles.", diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json index bc1354cc0c3..aad0d1fa47d 100644 --- a/homeassistant/components/shelly/translations/de.json +++ b/homeassistant/components/shelly/translations/de.json @@ -1,3 +1,19 @@ { + "config": { + "flow_title": "Shelly: {name}", + "step": { + "credentials": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + }, + "user": { + "data": { + "host": "Host" + } + } + } + }, "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 546007af0d1..736e190497a 100644 --- a/homeassistant/components/shelly/translations/en.json +++ b/homeassistant/components/shelly/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "unsupported_firmware": "The device is using an unsupported firmware version." }, "error": { "auth_not_supported": "Shelly devices requiring authentication are not currently supported.", diff --git a/homeassistant/components/shelly/translations/nl.json b/homeassistant/components/shelly/translations/nl.json index b5dc06eb140..92a172ff081 100644 --- a/homeassistant/components/shelly/translations/nl.json +++ b/homeassistant/components/shelly/translations/nl.json @@ -13,6 +13,11 @@ "confirm_discovery": { "description": "Wilt u het {model} bij {host} opzetten?" }, + "credentials": { + "data": { + "username": "Benutzername" + } + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index 42fc575f650..6b71d78673c 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -8,6 +8,11 @@ "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + } + }, "user": { "data": { "code": "Code (wird in der Benutzeroberfl\u00e4che von Home Assistant verwendet)", diff --git a/homeassistant/components/smappee/translations/de.json b/homeassistant/components/smappee/translations/de.json new file mode 100644 index 00000000000..0e77c8fbd7a --- /dev/null +++ b/homeassistant/components/smappee/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "flow_title": "Smappee: {name}", + "step": { + "environment": { + "data": { + "environment": "Umgebung" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/de.json b/homeassistant/components/smart_meter_texas/translations/de.json new file mode 100644 index 00000000000..936e9817d92 --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + }, + "title": "Smart Meter Texas" +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/nl.json b/homeassistant/components/smart_meter_texas/translations/nl.json new file mode 100644 index 00000000000..a40ab60cd1e --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/nl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sms/translations/de.json b/homeassistant/components/sms/translations/de.json new file mode 100644 index 00000000000..273daf6ef0a --- /dev/null +++ b/homeassistant/components/sms/translations/de.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "device": "Ger\u00e4t" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/ca.json b/homeassistant/components/somfy/translations/ca.json index 489ab7a7f9f..40af5096345 100644 --- a/homeassistant/components/somfy/translations/ca.json +++ b/homeassistant/components/somfy/translations/ca.json @@ -4,10 +4,11 @@ "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte amb Somfy.", "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "missing_configuration": "El component Somfy no est\u00e0 configurat. Mira'n la documentaci\u00f3.", - "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})" + "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "create_entry": { - "default": "Autenticaci\u00f3 exitosa amb Somfy." + "default": "Autenticaci\u00f3 exitosa" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/somfy/translations/en.json b/homeassistant/components/somfy/translations/en.json index 2a2bb689860..18910be2595 100644 --- a/homeassistant/components/somfy/translations/en.json +++ b/homeassistant/components/somfy/translations/en.json @@ -4,10 +4,11 @@ "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.", - "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})" + "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", + "single_instance_allowed": "Already configured. Only a single configuration possible." }, "create_entry": { - "default": "Successfully authenticated with Somfy." + "default": "Successfully authenticated" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/somfy/translations/ru.json b/homeassistant/components/somfy/translations/ru.json index 46c1e080480..89659e4e80b 100644 --- a/homeassistant/components/somfy/translations/ru.json +++ b/homeassistant/components/somfy/translations/ru.json @@ -4,7 +4,8 @@ "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Somfy \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.", - "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435." + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", + "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/spider/translations/de.json b/homeassistant/components/spider/translations/de.json new file mode 100644 index 00000000000..6f398062876 --- /dev/null +++ b/homeassistant/components/spider/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spider/translations/nl.json b/homeassistant/components/spider/translations/nl.json new file mode 100644 index 00000000000..4d00f0bfc74 --- /dev/null +++ b/homeassistant/components/spider/translations/nl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/de.json b/homeassistant/components/squeezebox/translations/de.json index 4d024c59d26..8671822cdf8 100644 --- a/homeassistant/components/squeezebox/translations/de.json +++ b/homeassistant/components/squeezebox/translations/de.json @@ -4,9 +4,16 @@ "edit": { "data": { "password": "Passwort", + "port": "Port", "username": "Benutzername" } + }, + "user": { + "data": { + "host": "Host" + } } } - } + }, + "title": "Logitech Squeezebox" } \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/nl.json b/homeassistant/components/squeezebox/translations/nl.json new file mode 100644 index 00000000000..bb140f4ca89 --- /dev/null +++ b/homeassistant/components/squeezebox/translations/nl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "edit": { + "data": { + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/de.json b/homeassistant/components/syncthru/translations/de.json index 76eaf5f21f3..8e568131e62 100644 --- a/homeassistant/components/syncthru/translations/de.json +++ b/homeassistant/components/syncthru/translations/de.json @@ -8,6 +8,7 @@ "syncthru_not_supported": "Ger\u00e4t unterst\u00fctzt kein SyncThru", "unknown_state": "Druckerstatus unbekannt, \u00fcberpr\u00fcfe URL und Netzwerkverbindung" }, + "flow_title": "Samsung SyncThru Drucker: {name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/transmission/translations/de.json b/homeassistant/components/transmission/translations/de.json index 66d6caf1b17..a09fbba4e85 100644 --- a/homeassistant/components/transmission/translations/de.json +++ b/homeassistant/components/transmission/translations/de.json @@ -25,6 +25,7 @@ "step": { "init": { "data": { + "limit": "Limit", "order": "Reihenfolge", "scan_interval": "Aktualisierungsfrequenz" }, diff --git a/homeassistant/components/wilight/translations/de.json b/homeassistant/components/wilight/translations/de.json new file mode 100644 index 00000000000..07d00495af7 --- /dev/null +++ b/homeassistant/components/wilight/translations/de.json @@ -0,0 +1,10 @@ +{ + "config": { + "flow_title": "WiLight: {name}", + "step": { + "confirm": { + "title": "WiLight" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/de.json b/homeassistant/components/wolflink/translations/de.json new file mode 100644 index 00000000000..cb7e571d1e6 --- /dev/null +++ b/homeassistant/components/wolflink/translations/de.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "device": { + "data": { + "device_name": "Ger\u00e4t" + } + }, + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/nl.json b/homeassistant/components/wolflink/translations/nl.json new file mode 100644 index 00000000000..4d00f0bfc74 --- /dev/null +++ b/homeassistant/components/wolflink/translations/nl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.de.json b/homeassistant/components/wolflink/translations/sensor.de.json new file mode 100644 index 00000000000..ef60c1c1ae1 --- /dev/null +++ b/homeassistant/components/wolflink/translations/sensor.de.json @@ -0,0 +1,8 @@ +{ + "state": { + "wolflink__state": { + "test": "Test", + "tpw": "TPW" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/de.json b/homeassistant/components/xiaomi_aqara/translations/de.json new file mode 100644 index 00000000000..75aa3d537e8 --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/de.json @@ -0,0 +1,10 @@ +{ + "config": { + "flow_title": "Xiaomi Aqara Gateway: {name}", + "step": { + "user": { + "title": "Xiaomi Aqara Gateway" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json index 6ec92566ade..d52715249b9 100644 --- a/homeassistant/components/xiaomi_miio/translations/de.json +++ b/homeassistant/components/xiaomi_miio/translations/de.json @@ -8,6 +8,7 @@ "connect_error": "Verbindung fehlgeschlagen", "no_device_selected": "Kein Ger\u00e4t ausgew\u00e4hlt, bitte w\u00e4hlen Sie ein Ger\u00e4t aus." }, + "flow_title": "Xiaomi Miio: {name}", "step": { "gateway": { "data": { diff --git a/homeassistant/components/yeelight/translations/de.json b/homeassistant/components/yeelight/translations/de.json index 29fbc0dfe33..6930fca0a5b 100644 --- a/homeassistant/components/yeelight/translations/de.json +++ b/homeassistant/components/yeelight/translations/de.json @@ -5,6 +5,12 @@ "data": { "device": "Ger\u00e4te" } + }, + "user": { + "data": { + "host": "Host", + "ip_address": "IP-Addresse" + } } } }, @@ -16,5 +22,6 @@ } } } - } + }, + "title": "Yeelight" } \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/de.json b/homeassistant/components/zoneminder/translations/de.json new file mode 100644 index 00000000000..1362dcbd62d --- /dev/null +++ b/homeassistant/components/zoneminder/translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "flow_title": "ZoneMinder", + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/et.json b/homeassistant/components/zoneminder/translations/et.json index 1dcaa5626b0..1446846940a 100644 --- a/homeassistant/components/zoneminder/translations/et.json +++ b/homeassistant/components/zoneminder/translations/et.json @@ -15,6 +15,7 @@ "step": { "user": { "data": { + "host": "Host ja port (n\u00e4iteks 10.10.0.4:8010)", "password": "Salas\u00f5na", "path": "ZM aadress", "path_zms": "ZMS-i aadress", diff --git a/homeassistant/components/zoneminder/translations/nl.json b/homeassistant/components/zoneminder/translations/nl.json index 03c7559dc44..a9ad121c32e 100644 --- a/homeassistant/components/zoneminder/translations/nl.json +++ b/homeassistant/components/zoneminder/translations/nl.json @@ -4,6 +4,7 @@ "auth_fail": "Gebruikersnaam of wachtwoord is onjuist.", "connection_error": "Kan geen verbinding maken met een ZoneMinder-server." }, + "flow_title": "ZoneMinder", "step": { "user": { "data": { From a19b43a304dc60d6ece0ad7f5366f56be9217bc4 Mon Sep 17 00:00:00 2001 From: Julius Mittenzwei Date: Sun, 27 Sep 2020 07:07:59 +0200 Subject: [PATCH 377/514] Add support for homekit windows (#40635) Co-authored-by: J. Nick Koston --- .../components/homekit/accessories.py | 11 +++- homeassistant/components/homekit/const.py | 1 + .../components/homekit/type_covers.py | 60 +++++++++++++----- .../homekit/test_get_accessories.py | 6 ++ tests/components/homekit/test_type_covers.py | 62 +++++++++++++------ 5 files changed, 106 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 3912d8e9056..6dc2e2364b6 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -9,7 +9,11 @@ from pyhap.accessory_driver import AccessoryDriver from pyhap.const import CATEGORY_OTHER from homeassistant.components import cover, vacuum -from homeassistant.components.cover import DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE +from homeassistant.components.cover import ( + DEVICE_CLASS_GARAGE, + DEVICE_CLASS_GATE, + DEVICE_CLASS_WINDOW, +) from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.const import ( ATTR_BATTERY_CHARGING, @@ -155,6 +159,11 @@ def get_accessory(hass, driver, state, aid, config): cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE ): a_type = "GarageDoorOpener" + elif ( + device_class == DEVICE_CLASS_WINDOW + and features & cover.SUPPORT_SET_POSITION + ): + a_type = "Window" elif features & cover.SUPPORT_SET_POSITION: a_type = "WindowCovering" elif features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index d8eec057191..9a2bc37a5a9 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -136,6 +136,7 @@ SERV_TELEVISION_SPEAKER = "TelevisionSpeaker" SERV_TEMPERATURE_SENSOR = "TemperatureSensor" SERV_THERMOSTAT = "Thermostat" SERV_VALVE = "Valve" +SERV_WINDOW = "Window" SERV_WINDOW_COVERING = "WindowCovering" # #### Characteristics #### diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 1e18ad82b94..d8d8da5a974 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -1,7 +1,11 @@ """Class to hold all cover accessories.""" import logging -from pyhap.const import CATEGORY_GARAGE_DOOR_OPENER, CATEGORY_WINDOW_COVERING +from pyhap.const import ( + CATEGORY_GARAGE_DOOR_OPENER, + CATEGORY_WINDOW, + CATEGORY_WINDOW_COVERING, +) from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, @@ -46,6 +50,7 @@ from .const import ( HK_POSITION_GOING_TO_MIN, HK_POSITION_STOPPED, SERV_GARAGE_DOOR_OPENER, + SERV_WINDOW, SERV_WINDOW_COVERING, ) @@ -128,16 +133,16 @@ class GarageDoorOpener(HomeAccessory): self.char_current_state.set_value(current_door_state) -class WindowCoveringBase(HomeAccessory): +class OpeningDeviceBase(HomeAccessory): """Generate a base Window accessory for a cover entity. This class is used for WindowCoveringBasic and WindowCovering """ - def __init__(self, *args, category): - """Initialize a WindowCoveringBase accessory object.""" - super().__init__(*args, category=CATEGORY_WINDOW_COVERING) + def __init__(self, *args, category, service): + """Initialize a OpeningDeviceBase accessory object.""" + super().__init__(*args, category=category) state = self.hass.states.get(self.entity_id) self.features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -151,7 +156,7 @@ class WindowCoveringBase(HomeAccessory): if self._supports_tilt: self.chars.extend([CHAR_TARGET_TILT_ANGLE, CHAR_CURRENT_TILT_ANGLE]) - self.serv_cover = self.add_preload_service(SERV_WINDOW_COVERING, self.chars) + self.serv_cover = self.add_preload_service(service, self.chars) if self._supports_stop: self.char_hold_position = self.serv_cover.configure_char( @@ -211,16 +216,15 @@ class WindowCoveringBase(HomeAccessory): self._homekit_target_tilt = None -@TYPES.register("WindowCovering") -class WindowCovering(WindowCoveringBase, HomeAccessory): - """Generate a Window accessory for a cover entity. +class OpeningDevice(OpeningDeviceBase, HomeAccessory): + """Generate a Window/WindowOpening accessory for a cover entity. The cover entity must support: set_cover_position. """ - def __init__(self, *args): + def __init__(self, *args, category, service): """Initialize a WindowCovering accessory object.""" - super().__init__(*args, category=CATEGORY_WINDOW_COVERING) + super().__init__(*args, category=category, service=service) state = self.hass.states.get(self.entity_id) self._homekit_target = None @@ -278,8 +282,34 @@ class WindowCovering(WindowCoveringBase, HomeAccessory): super().async_update_state(new_state) +@TYPES.register("Window") +class Window(OpeningDevice): + """Generate a Window accessory for a cover entity with DEVICE_CLASS_WINDOW. + + The entity must support: set_cover_position. + """ + + def __init__(self, *args): + """Initialize a Window accessory object.""" + super().__init__(*args, category=CATEGORY_WINDOW, service=SERV_WINDOW) + + +@TYPES.register("WindowCovering") +class WindowCovering(OpeningDevice): + """Generate a WindowCovering accessory for a cover entity. + + The entity must support: set_cover_position. + """ + + def __init__(self, *args): + """Initialize a WindowCovering accessory object.""" + super().__init__( + *args, category=CATEGORY_WINDOW_COVERING, service=SERV_WINDOW_COVERING + ) + + @TYPES.register("WindowCoveringBasic") -class WindowCoveringBasic(WindowCoveringBase, HomeAccessory): +class WindowCoveringBasic(OpeningDeviceBase, HomeAccessory): """Generate a Window accessory for a cover entity. The cover entity must support: open_cover, close_cover, @@ -287,8 +317,10 @@ class WindowCoveringBasic(WindowCoveringBase, HomeAccessory): """ def __init__(self, *args): - """Initialize a WindowCovering accessory object.""" - super().__init__(*args, category=CATEGORY_WINDOW_COVERING) + """Initialize a WindowCoveringBasic accessory object.""" + super().__init__( + *args, category=CATEGORY_WINDOW_COVERING, service=SERV_WINDOW_COVERING + ) state = self.hass.states.get(self.entity_id) self.char_current_position = self.serv_cover.configure_char( CHAR_CURRENT_POSITION, value=0 diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index a7468955d36..ea91733fdab 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -120,6 +120,12 @@ def test_types(type_name, entity_id, state, attrs, config): ATTR_SUPPORTED_FEATURES: cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE, }, ), + ( + "Window", + "cover.set_position", + "open", + {ATTR_DEVICE_CLASS: "window", ATTR_SUPPORTED_FEATURES: 4}, + ), ("WindowCovering", "cover.set_position", "open", {ATTR_SUPPORTED_FEATURES: 4}), ( "WindowCoveringBasic", diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 3eed6d05816..cd193b61646 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -48,10 +48,13 @@ def cls(): "homeassistant.components.homekit.type_covers", fromlist=["GarageDoorOpener", "WindowCovering", "WindowCoveringBasic"], ) - patcher_tuple = namedtuple("Cls", ["window", "window_basic", "garage"]) + patcher_tuple = namedtuple( + "Cls", ["window", "windowcovering", "windowcovering_basic", "garage"] + ) yield patcher_tuple( - window=_import.WindowCovering, - window_basic=_import.WindowCoveringBasic, + window=_import.Window, + windowcovering=_import.WindowCovering, + windowcovering_basic=_import.WindowCoveringBasic, garage=_import.GarageDoorOpener, ) patcher.stop() @@ -136,13 +139,13 @@ async def test_garage_door_open_close(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] is None -async def test_window_set_cover_position(hass, hk_driver, cls, events): +async def test_windowcovering_set_cover_position(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = "cover.window" hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = cls.window(hass, hk_driver, "Cover", entity_id, 2, None) + acc = cls.windowcovering(hass, hk_driver, "Cover", entity_id, 2, None) await acc.run_handler() await hass.async_block_till_done() @@ -206,7 +209,24 @@ async def test_window_set_cover_position(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == 75 -async def test_window_cover_set_tilt(hass, hk_driver, cls, events): +async def test_window_instantiate(hass, hk_driver, cls, events): + """Test if Window accessory is instantiated correctly.""" + entity_id = "cover.window" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = cls.window(hass, hk_driver, "Window", entity_id, 2, None) + await acc.run_handler() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 13 # Window + + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 + + +async def test_windowcovering_cover_set_tilt(hass, hk_driver, cls, events): """Test if accessory and HA update slat tilt accordingly.""" entity_id = "cover.window" @@ -214,7 +234,7 @@ async def test_window_cover_set_tilt(hass, hk_driver, cls, events): entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_TILT_POSITION} ) await hass.async_block_till_done() - acc = cls.window(hass, hk_driver, "Cover", entity_id, 2, None) + acc = cls.windowcovering(hass, hk_driver, "Cover", entity_id, 2, None) await acc.run_handler() await hass.async_block_till_done() @@ -273,12 +293,12 @@ async def test_window_cover_set_tilt(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == 75 -async def test_window_open_close(hass, hk_driver, cls, events): +async def test_windowcovering_open_close(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = "cover.window" hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: 0}) - acc = cls.window_basic(hass, hk_driver, "Cover", entity_id, 2, None) + acc = cls.windowcovering_basic(hass, hk_driver, "Cover", entity_id, 2, None) await acc.run_handler() await hass.async_block_till_done() @@ -354,14 +374,14 @@ async def test_window_open_close(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] is None -async def test_window_open_close_stop(hass, hk_driver, cls, events): +async def test_windowcovering_open_close_stop(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = "cover.window" hass.states.async_set( entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP} ) - acc = cls.window_basic(hass, hk_driver, "Cover", entity_id, 2, None) + acc = cls.windowcovering_basic(hass, hk_driver, "Cover", entity_id, 2, None) await acc.run_handler() await hass.async_block_till_done() @@ -401,7 +421,9 @@ async def test_window_open_close_stop(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] is None -async def test_window_open_close_with_position_and_stop(hass, hk_driver, cls, events): +async def test_windowcovering_open_close_with_position_and_stop( + hass, hk_driver, cls, events +): """Test if accessory and HA are updated accordingly.""" entity_id = "cover.stop_window" @@ -410,7 +432,7 @@ async def test_window_open_close_with_position_and_stop(hass, hk_driver, cls, ev STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP | SUPPORT_SET_POSITION}, ) - acc = cls.window(hass, hk_driver, "Cover", entity_id, 2, None) + acc = cls.windowcovering(hass, hk_driver, "Cover", entity_id, 2, None) await acc.run_handler() await hass.async_block_till_done() @@ -430,7 +452,7 @@ async def test_window_open_close_with_position_and_stop(hass, hk_driver, cls, ev assert events[-1].data[ATTR_VALUE] is None -async def test_window_basic_restore(hass, hk_driver, cls, events): +async def test_windowcovering_basic_restore(hass, hk_driver, cls, events): """Test setting up an entity from state in the event registry.""" hass.state = CoreState.not_running @@ -455,20 +477,22 @@ async def test_window_basic_restore(hass, hk_driver, cls, events): hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) await hass.async_block_till_done() - acc = cls.window_basic(hass, hk_driver, "Cover", "cover.simple", 2, None) + acc = cls.windowcovering_basic(hass, hk_driver, "Cover", "cover.simple", 2, None) assert acc.category == 14 assert acc.char_current_position is not None assert acc.char_target_position is not None assert acc.char_position_state is not None - acc = cls.window_basic(hass, hk_driver, "Cover", "cover.all_info_set", 2, None) + acc = cls.windowcovering_basic( + hass, hk_driver, "Cover", "cover.all_info_set", 2, None + ) assert acc.category == 14 assert acc.char_current_position is not None assert acc.char_target_position is not None assert acc.char_position_state is not None -async def test_window_restore(hass, hk_driver, cls, events): +async def test_windowcovering_restore(hass, hk_driver, cls, events): """Test setting up an entity from state in the event registry.""" hass.state = CoreState.not_running @@ -493,13 +517,13 @@ async def test_window_restore(hass, hk_driver, cls, events): hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) await hass.async_block_till_done() - acc = cls.window(hass, hk_driver, "Cover", "cover.simple", 2, None) + acc = cls.windowcovering(hass, hk_driver, "Cover", "cover.simple", 2, None) assert acc.category == 14 assert acc.char_current_position is not None assert acc.char_target_position is not None assert acc.char_position_state is not None - acc = cls.window(hass, hk_driver, "Cover", "cover.all_info_set", 2, None) + acc = cls.windowcovering(hass, hk_driver, "Cover", "cover.all_info_set", 2, None) assert acc.category == 14 assert acc.char_current_position is not None assert acc.char_target_position is not None From 88957528376a1e6596fb286f237ac886523e4345 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 27 Sep 2020 10:16:45 +0200 Subject: [PATCH 378/514] Handle Shelly channel names (if available) (#40119) Co-authored-by: Paulus Schoutsen Co-authored-by: Franck Nijhof --- homeassistant/components/shelly/entity.py | 37 ++++++++++++++++++----- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 96281e2aee1..96c35eecbf1 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -20,6 +20,33 @@ def temperature_unit(block_info: dict) -> str: return TEMP_CELSIUS +def shelly_naming(self, block, entity_type: str): + """Naming for switch and sensors.""" + + channels = 0 + if "num_outputs" in self.wrapper.device.shelly: + channels = self.wrapper.device.shelly["num_outputs"] + if ( + self.wrapper.model in ["SHSW-21", "SHSW-25"] + and self.wrapper.device.settings["mode"] == "roller" + ): + channels = 1 + + entity_name = self.wrapper.name + if channels > 1 and block.type != "device": + entity_name = self.wrapper.device.settings["relays"][int(block.channel)]["name"] + if not entity_name: + entity_name = f"{self.wrapper.name} channel {int(block.channel)+1}" + + if entity_type == "switch": + return entity_name + + if entity_type == "sensor": + return f"{entity_name} {self.description.name}" + + raise ValueError + + async def async_setup_entry_attribute_entities( hass, config_entry, async_add_entities, sensors, sensor_class ): @@ -75,7 +102,7 @@ class ShellyBlockEntity(entity.Entity): """Initialize Shelly entity.""" self.wrapper = wrapper self.block = block - self._name = f"{self.wrapper.name} {self.block.description.replace('_', ' ')}" + self._name = shelly_naming(self, block, "switch") @property def name(self): @@ -142,13 +169,7 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): 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.channel)) - name_parts.append(self.description.name) - - self._name = " ".join(name_parts) + self._name = shelly_naming(self, block, "sensor") @property def unique_id(self): From 66a8edb11e4c680644e203a5432e4e160d929d0d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 27 Sep 2020 11:02:45 +0200 Subject: [PATCH 379/514] deCONZ fix comments from #40265 (#40640) * Use set not list * Events are not entities * Don't await unload_events * Remove checks of entities content in tests * List to set comprehension * Why is it so hard to remember that sets arent parenthesis... --- .../components/deconz/deconz_device.py | 16 +++++++------- .../components/deconz/deconz_event.py | 21 +++++++------------ homeassistant/components/deconz/gateway.py | 2 +- homeassistant/components/deconz/light.py | 2 +- homeassistant/components/deconz/sensor.py | 2 +- tests/components/deconz/test_binary_sensor.py | 8 ------- tests/components/deconz/test_climate.py | 8 ------- tests/components/deconz/test_cover.py | 3 --- tests/components/deconz/test_deconz_event.py | 4 +--- tests/components/deconz/test_light.py | 6 ------ tests/components/deconz/test_sensor.py | 12 ----------- tests/components/deconz/test_switch.py | 3 --- 12 files changed, 20 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 56dc00dab58..68d96aecf35 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -10,17 +10,10 @@ from .const import DOMAIN as DECONZ_DOMAIN class DeconzBase: """Common base for deconz entities and events.""" - TYPE = "" - def __init__(self, device, gateway): """Set up device and add update callback to get data from websocket.""" self._device = device self.gateway = gateway - self.gateway.entities[self.TYPE].add(self.unique_id) - - async def async_will_remove_from_hass(self) -> None: - """Remove unique id.""" - self.gateway.entities[self.TYPE].remove(self.unique_id) @property def unique_id(self): @@ -57,6 +50,13 @@ class DeconzBase: class DeconzDevice(DeconzBase, Entity): """Representation of a deCONZ device.""" + TYPE = "" + + def __init__(self, device, gateway): + """Set up device and add update callback to get data from websocket.""" + super().__init__(device, gateway) + self.gateway.entities[self.TYPE].add(self.unique_id) + @property def entity_registry_enabled_default(self): """Return if the entity should be enabled when first added to the entity registry. @@ -83,7 +83,7 @@ class DeconzDevice(DeconzBase, Entity): self._device.remove_callback(self.async_update_callback) if self.entity_id in self.gateway.deconz_ids: del self.gateway.deconz_ids[self.entity_id] - await super().async_will_remove_from_hass() + self.gateway.entities[self.TYPE].remove(self.unique_id) @callback def async_update_callback(self, force_update=False, ignore_update=False): diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 4b7027b8b36..0192c55add9 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -11,12 +11,9 @@ from .deconz_device import DeconzBase CONF_DECONZ_EVENT = "deconz_event" -EVENT = "Event" - async def async_setup_events(gateway) -> None: """Set up the deCONZ events.""" - gateway.entities[EVENT] = set() @callback def async_add_sensor(sensors): @@ -26,10 +23,9 @@ async def async_setup_events(gateway) -> None: if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): continue - if ( - sensor.type not in Switch.ZHATYPE - or sensor.uniqueid in gateway.entities[EVENT] - ): + if sensor.type not in Switch.ZHATYPE or sensor.uniqueid in { + event.unique_id for event in gateway.events + }: continue new_event = DeconzEvent(sensor, gateway) @@ -47,10 +43,11 @@ async def async_setup_events(gateway) -> None: ) -async def async_unload_events(gateway) -> None: +@callback +def async_unload_events(gateway) -> None: """Unload all deCONZ events.""" for event in gateway.events: - await event.async_will_remove_from_hass() + event.async_will_remove_from_hass() gateway.events.clear() @@ -62,8 +59,6 @@ class DeconzEvent(DeconzBase): instead of a sensor entity in hass. """ - TYPE = EVENT - def __init__(self, device, gateway): """Register callback that will be used for signals.""" super().__init__(device, gateway) @@ -79,10 +74,10 @@ class DeconzEvent(DeconzBase): """Return Event device.""" return self._device - async def async_will_remove_from_hass(self) -> None: + @callback + def async_will_remove_from_hass(self) -> None: """Disconnect event object when removed.""" self._device.remove_callback(self.async_update_callback) - await super().async_will_remove_from_hass() @callback def async_update_callback(self, force_update=False, ignore_update=False): diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index ceed473a383..998c8cd4812 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -232,7 +232,7 @@ class DeconzGateway: unsub_dispatcher() self.listeners = [] - await async_unload_events(self) + async_unload_events(self) self.deconz_ids = {} return True diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 135a01cd0df..3f11cef31da 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -75,7 +75,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if not group.lights: continue - known_groups = list(gateway.entities[DOMAIN]) + known_groups = set(gateway.entities[DOMAIN]) new_group = DeconzGroup(group, gateway) if new_group.unique_id not in known_groups: entities.append(new_group) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index d7d7b777b41..d7210b31c43 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -96,7 +96,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if sensor.battery is not None: battery_handler.remove_tracker(sensor) - known_batteries = list(gateway.entities[DOMAIN]) + known_batteries = set(gateway.entities[DOMAIN]) new_battery = DeconzBattery(sensor, gateway) if new_battery.unique_id not in known_batteries: entities.append(new_battery) diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 1ced3b8ed2d..30f7251e067 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -67,7 +67,6 @@ async def test_no_binary_sensors(hass): """Test that no sensors in deconz results in no sensor entities.""" gateway = await setup_deconz_integration(hass) assert len(gateway.deconz_ids) == 0 - assert len(gateway.entities[binary_sensor.DOMAIN]) == 0 assert len(hass.states.async_all()) == 0 @@ -81,7 +80,6 @@ async def test_binary_sensors(hass): assert "binary_sensor.clip_presence_sensor" not in gateway.deconz_ids assert "binary_sensor.vibration_sensor" in gateway.deconz_ids assert len(hass.states.async_all()) == 3 - assert len(gateway.entities[binary_sensor.DOMAIN]) == 2 presence_sensor = hass.states.get("binary_sensor.presence_sensor") assert presence_sensor.state == "off" @@ -113,7 +111,6 @@ async def test_binary_sensors(hass): await gateway.async_reset() assert len(hass.states.async_all()) == 0 - assert len(gateway.entities[binary_sensor.DOMAIN]) == 0 async def test_allow_clip_sensor(hass): @@ -130,7 +127,6 @@ async def test_allow_clip_sensor(hass): assert "binary_sensor.clip_presence_sensor" in gateway.deconz_ids assert "binary_sensor.vibration_sensor" in gateway.deconz_ids assert len(hass.states.async_all()) == 4 - assert len(gateway.entities[binary_sensor.DOMAIN]) == 3 presence_sensor = hass.states.get("binary_sensor.presence_sensor") assert presence_sensor.state == "off" @@ -154,7 +150,6 @@ async def test_allow_clip_sensor(hass): assert "binary_sensor.clip_presence_sensor" not in gateway.deconz_ids assert "binary_sensor.vibration_sensor" in gateway.deconz_ids assert len(hass.states.async_all()) == 3 - assert len(gateway.entities[binary_sensor.DOMAIN]) == 2 hass.config_entries.async_update_entry( gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True} @@ -166,14 +161,12 @@ async def test_allow_clip_sensor(hass): assert "binary_sensor.clip_presence_sensor" in gateway.deconz_ids assert "binary_sensor.vibration_sensor" in gateway.deconz_ids assert len(hass.states.async_all()) == 4 - assert len(gateway.entities[binary_sensor.DOMAIN]) == 3 async def test_add_new_binary_sensor(hass): """Test that adding a new binary sensor works.""" gateway = await setup_deconz_integration(hass) assert len(gateway.deconz_ids) == 0 - assert len(gateway.entities[binary_sensor.DOMAIN]) == 0 state_added_event = { "t": "event", @@ -189,4 +182,3 @@ async def test_add_new_binary_sensor(hass): presence_sensor = hass.states.get("binary_sensor.presence_sensor") assert presence_sensor.state == "off" - assert len(gateway.entities[binary_sensor.DOMAIN]) == 1 diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 361e182fe98..ddc89295cba 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -59,7 +59,6 @@ async def test_no_sensors(hass): gateway = await setup_deconz_integration(hass) assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 - assert len(gateway.entities[climate.DOMAIN]) == 0 async def test_climate_devices(hass): @@ -73,7 +72,6 @@ async def test_climate_devices(hass): assert "climate.presence_sensor" not in gateway.deconz_ids assert "climate.clip_thermostat" not in gateway.deconz_ids assert len(hass.states.async_all()) == 3 - assert len(gateway.entities[climate.DOMAIN]) == 1 thermostat = hass.states.get("climate.thermostat") assert thermostat.state == "auto" @@ -183,7 +181,6 @@ async def test_climate_devices(hass): await gateway.async_reset() assert len(hass.states.async_all()) == 0 - assert len(gateway.entities[climate.DOMAIN]) == 0 async def test_clip_climate_device(hass): @@ -201,7 +198,6 @@ async def test_clip_climate_device(hass): assert "climate.presence_sensor" not in gateway.deconz_ids assert "climate.clip_thermostat" in gateway.deconz_ids assert len(hass.states.async_all()) == 4 - assert len(gateway.entities[climate.DOMAIN]) == 2 thermostat = hass.states.get("climate.thermostat") assert thermostat.state == "auto" @@ -229,7 +225,6 @@ async def test_clip_climate_device(hass): assert "climate.presence_sensor" not in gateway.deconz_ids assert "climate.clip_thermostat" not in gateway.deconz_ids assert len(hass.states.async_all()) == 3 - assert len(gateway.entities[climate.DOMAIN]) == 1 hass.config_entries.async_update_entry( gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True} @@ -242,7 +237,6 @@ async def test_clip_climate_device(hass): assert "climate.presence_sensor" not in gateway.deconz_ids assert "climate.clip_thermostat" in gateway.deconz_ids assert len(hass.states.async_all()) == 4 - assert len(gateway.entities[climate.DOMAIN]) == 2 async def test_verify_state_update(hass): @@ -274,7 +268,6 @@ async def test_add_new_climate_device(hass): """Test that adding a new climate device works.""" gateway = await setup_deconz_integration(hass) assert len(gateway.deconz_ids) == 0 - assert len(gateway.entities[climate.DOMAIN]) == 0 state_added_event = { "t": "event", @@ -290,4 +283,3 @@ async def test_add_new_climate_device(hass): thermostat = hass.states.get("climate.thermostat") assert thermostat.state == "auto" - assert len(gateway.entities[climate.DOMAIN]) == 1 diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 28a6f150fc4..095ae7e4bc5 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -67,7 +67,6 @@ async def test_no_covers(hass): """Test that no cover entities are created.""" gateway = await setup_deconz_integration(hass) assert len(gateway.deconz_ids) == 0 - assert len(gateway.entities[cover.DOMAIN]) == 0 assert len(hass.states.async_all()) == 0 @@ -82,7 +81,6 @@ async def test_cover(hass): assert "cover.deconz_old_brightness_cover" in gateway.deconz_ids assert "cover.window_covering_controller" in gateway.deconz_ids assert len(hass.states.async_all()) == 5 - assert len(gateway.entities[cover.DOMAIN]) == 4 level_controllable_cover = hass.states.get("cover.level_controllable_cover") assert level_controllable_cover.state == "open" @@ -160,4 +158,3 @@ async def test_cover(hass): await gateway.async_reset() assert len(hass.states.async_all()) == 0 - assert len(gateway.entities[cover.DOMAIN]) == 0 diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 717c783d0ed..525821e389f 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -1,7 +1,7 @@ """Test deCONZ remote events.""" from copy import deepcopy -from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT, EVENT +from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -62,7 +62,6 @@ async def test_deconz_events(hass): assert "sensor.switch_2_battery_level" in gateway.deconz_ids assert len(hass.states.async_all()) == 3 assert len(gateway.events) == 5 - assert len(gateway.entities[EVENT]) == 5 switch_1 = hass.states.get("sensor.switch_1") assert switch_1 is None @@ -128,4 +127,3 @@ async def test_deconz_events(hass): assert len(hass.states.async_all()) == 0 assert len(gateway.events) == 0 - assert len(gateway.entities[EVENT]) == 0 diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index dac1b071e93..4e9f6b3d512 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -95,7 +95,6 @@ async def test_no_lights_or_groups(hass): gateway = await setup_deconz_integration(hass) assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 - assert len(gateway.entities[light.DOMAIN]) == 0 async def test_lights_and_groups(hass): @@ -112,7 +111,6 @@ async def test_lights_and_groups(hass): assert "light.on_off_light" in gateway.deconz_ids assert len(hass.states.async_all()) == 6 - assert len(gateway.entities[light.DOMAIN]) == 5 rgb_light = hass.states.get("light.rgb_light") assert rgb_light.state == "on" @@ -258,7 +256,6 @@ async def test_lights_and_groups(hass): await gateway.async_reset() assert len(hass.states.async_all()) == 0 - assert len(gateway.entities[light.DOMAIN]) == 0 async def test_disable_light_groups(hass): @@ -278,7 +275,6 @@ async def test_disable_light_groups(hass): assert "light.on_off_switch" not in gateway.deconz_ids # 3 entities assert len(hass.states.async_all()) == 5 - assert len(gateway.entities[light.DOMAIN]) == 4 rgb_light = hass.states.get("light.rgb_light") assert rgb_light is not None @@ -304,7 +300,6 @@ async def test_disable_light_groups(hass): assert "light.on_off_switch" not in gateway.deconz_ids # 3 entities assert len(hass.states.async_all()) == 6 - assert len(gateway.entities[light.DOMAIN]) == 5 hass.config_entries.async_update_entry( gateway.config_entry, options={deconz.gateway.CONF_ALLOW_DECONZ_GROUPS: False} @@ -318,4 +313,3 @@ async def test_disable_light_groups(hass): assert "light.on_off_switch" not in gateway.deconz_ids # 3 entities assert len(hass.states.async_all()) == 5 - assert len(gateway.entities[light.DOMAIN]) == 4 diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 8c7829586e4..9d87c7b91cb 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -2,7 +2,6 @@ from copy import deepcopy from homeassistant.components import deconz -from homeassistant.components.deconz.deconz_event import EVENT import homeassistant.components.sensor as sensor from homeassistant.const import ( DEVICE_CLASS_BATTERY, @@ -97,7 +96,6 @@ async def test_no_sensors(hass): gateway = await setup_deconz_integration(hass) assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 - assert len(gateway.entities[sensor.DOMAIN]) == 0 async def test_sensors(hass): @@ -116,7 +114,6 @@ async def test_sensors(hass): assert "sensor.consumption_sensor" in gateway.deconz_ids assert "sensor.clip_light_level_sensor" not in gateway.deconz_ids assert len(hass.states.async_all()) == 5 - assert len(gateway.entities[sensor.DOMAIN]) == 5 light_level_sensor = hass.states.get("sensor.light_level_sensor") assert light_level_sensor.state == "999.8" @@ -177,8 +174,6 @@ async def test_sensors(hass): await gateway.async_reset() assert len(hass.states.async_all()) == 0 - # Daylight sensor from deCONZ is added to set but is disabled by default - assert len(gateway.entities[sensor.DOMAIN]) == 1 async def test_allow_clip_sensors(hass): @@ -201,7 +196,6 @@ async def test_allow_clip_sensors(hass): assert "sensor.consumption_sensor" in gateway.deconz_ids assert "sensor.clip_light_level_sensor" in gateway.deconz_ids assert len(hass.states.async_all()) == 6 - assert len(gateway.entities[sensor.DOMAIN]) == 6 light_level_sensor = hass.states.get("sensor.light_level_sensor") assert light_level_sensor.state == "999.8" @@ -249,7 +243,6 @@ async def test_allow_clip_sensors(hass): assert "sensor.consumption_sensor" in gateway.deconz_ids assert "sensor.clip_light_level_sensor" not in gateway.deconz_ids assert len(hass.states.async_all()) == 5 - assert len(gateway.entities[sensor.DOMAIN]) == 5 hass.config_entries.async_update_entry( gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True} @@ -267,7 +260,6 @@ async def test_allow_clip_sensors(hass): assert "sensor.consumption_sensor" in gateway.deconz_ids assert "sensor.clip_light_level_sensor" in gateway.deconz_ids assert len(hass.states.async_all()) == 6 - assert len(gateway.entities[sensor.DOMAIN]) == 6 async def test_add_new_sensor(hass): @@ -300,8 +292,6 @@ async def test_add_battery_later(hass): assert len(gateway.deconz_ids) == 0 assert len(gateway.events) == 1 assert len(remote._callbacks) == 2 - assert len(gateway.entities[sensor.DOMAIN]) == 0 - assert len(gateway.entities[EVENT]) == 1 remote.update({"config": {"battery": 50}}) await hass.async_block_till_done() @@ -312,5 +302,3 @@ async def test_add_battery_later(hass): battery_sensor = hass.states.get("sensor.switch_1_battery_level") assert battery_sensor is not None - assert len(gateway.entities[sensor.DOMAIN]) == 1 - assert len(gateway.entities[EVENT]) == 1 diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 7b1e0e6121c..b441868859b 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -64,7 +64,6 @@ async def test_no_switches(hass): gateway = await setup_deconz_integration(hass) assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 - assert len(gateway.entities[switch.DOMAIN]) == 0 async def test_switches(hass): @@ -78,7 +77,6 @@ async def test_switches(hass): assert "switch.unsupported_switch" not in gateway.deconz_ids assert "switch.on_off_relay" in gateway.deconz_ids assert len(hass.states.async_all()) == 5 - assert len(gateway.entities[switch.DOMAIN]) == 4 on_off_switch = hass.states.get("switch.on_off_switch") assert on_off_switch.state == "on" @@ -175,4 +173,3 @@ async def test_switches(hass): await gateway.async_reset() assert len(hass.states.async_all()) == 0 - assert len(gateway.entities[switch.DOMAIN]) == 0 From b65583084b7510b299213fc926399b459c56eba3 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Sun, 27 Sep 2020 11:40:56 +0200 Subject: [PATCH 380/514] Fix solaredge service data KeyError (#40653) --- homeassistant/components/solaredge/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 2b085d1ba40..0cd498b4e3b 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -149,7 +149,7 @@ class SolarEdgeOverviewSensor(SolarEdgeSensor): def update(self): """Get the latest data from the sensor and update the state.""" self.data_service.update() - self._state = self.data_service.data[self._json_key] + self._state = self.data_service.data.get(self._json_key) class SolarEdgeDetailsSensor(SolarEdgeSensor): @@ -192,8 +192,8 @@ class SolarEdgeInventorySensor(SolarEdgeSensor): def update(self): """Get the latest inventory data and update state and attributes.""" self.data_service.update() - self._state = self.data_service.data[self._json_key] - self._attributes = self.data_service.attributes[self._json_key] + self._state = self.data_service.data.get(self._json_key) + self._attributes = self.data_service.attributes.get(self._json_key) class SolarEdgeEnergyDetailsSensor(SolarEdgeSensor): From 5274b03dc2ce43650a54e3ea0f956f079f7dda66 Mon Sep 17 00:00:00 2001 From: Bashir <37988348+Brahmah@users.noreply.github.com> Date: Sun, 27 Sep 2020 20:51:41 +1000 Subject: [PATCH 381/514] Resolve Frontend Services.yaml Minor Oversight (#40659) The Default Values Suggest That The User enters 'light' as a parameter for the theme name. 'Light' is, however, not a valid option on a fresh install of HA unless a user manually downloads a theme with the name 'light'. --- homeassistant/components/frontend/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/services.yaml b/homeassistant/components/frontend/services.yaml index 31eb4d5d1ca..cc0d6bde216 100644 --- a/homeassistant/components/frontend/services.yaml +++ b/homeassistant/components/frontend/services.yaml @@ -5,7 +5,7 @@ set_theme: fields: name: description: Name of a predefined theme, 'default' or 'none'. - example: "light" + example: "default" mode: description: The mode the theme is for, either 'dark' or 'light' (default). example: "dark" From 0e6d54ea60341e9db339fdce1b7267d5f3ea52a7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 27 Sep 2020 15:39:45 +0200 Subject: [PATCH 382/514] Rewrite core event tests to pytest tests (#40664) --- tests/test_core.py | 72 ++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 6c684ae1eac..33cb0a37e23 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -268,49 +268,47 @@ async def test_add_job_with_none(hass): hass.async_add_job(None, "test_arg") -class TestEvent(unittest.TestCase): - """A Test Event class.""" +def test_event_eq(): + """Test events.""" + now = dt_util.utcnow() + data = {"some": "attr"} + context = ha.Context() + event1, event2 = [ + ha.Event("some_type", data, time_fired=now, context=context) for _ in range(2) + ] - def test_eq(self): - """Test events.""" - now = dt_util.utcnow() - data = {"some": "attr"} - context = ha.Context() - event1, event2 = [ - ha.Event("some_type", data, time_fired=now, context=context) - for _ in range(2) - ] + assert event1 == event2 - assert event1 == event2 - def test_repr(self): - """Test that repr method works.""" - assert str(ha.Event("TestEvent")) == "" +def test_event_repr(): + """Test that Event repr method works.""" + assert str(ha.Event("TestEvent")) == "" - assert ( - str(ha.Event("TestEvent", {"beer": "nice"}, ha.EventOrigin.remote)) - == "" - ) + assert ( + str(ha.Event("TestEvent", {"beer": "nice"}, ha.EventOrigin.remote)) + == "" + ) - def test_as_dict(self): - """Test as dictionary.""" - event_type = "some_type" - now = dt_util.utcnow() - data = {"some": "attr"} - event = ha.Event(event_type, data, ha.EventOrigin.local, now) - expected = { - "event_type": event_type, - "data": data, - "origin": "LOCAL", - "time_fired": now, - "context": { - "id": event.context.id, - "parent_id": None, - "user_id": event.context.user_id, - }, - } - assert expected == event.as_dict() +def test_event_as_dict(): + """Test as Event as dictionary.""" + event_type = "some_type" + now = dt_util.utcnow() + data = {"some": "attr"} + + event = ha.Event(event_type, data, ha.EventOrigin.local, now) + expected = { + "event_type": event_type, + "data": data, + "origin": "LOCAL", + "time_fired": now, + "context": { + "id": event.context.id, + "parent_id": None, + "user_id": event.context.user_id, + }, + } + assert event.as_dict() == expected class TestEventBus(unittest.TestCase): From 6028953eca4b7c8aa45fa16522c99ec9cde9cbcb Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Sun, 27 Sep 2020 17:12:36 +0200 Subject: [PATCH 383/514] Bump python-velbus to 2.0.46 (#40663) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 42e8a3307dc..368c4865bab 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["python-velbus==2.0.45"], + "requirements": ["python-velbus==2.0.46"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"] } diff --git a/requirements_all.txt b/requirements_all.txt index a6b7a549620..b0c0eaf54b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1785,7 +1785,7 @@ python-telnet-vlc==1.0.4 python-twitch-client==0.6.0 # homeassistant.components.velbus -python-velbus==2.0.45 +python-velbus==2.0.46 # homeassistant.components.vlc python-vlc==1.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4025059a6b7..d9df60ef4ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -848,7 +848,7 @@ python-tado==0.8.1 python-twitch-client==0.6.0 # homeassistant.components.velbus -python-velbus==2.0.45 +python-velbus==2.0.46 # homeassistant.components.awair python_awair==0.1.1 From 3fba4274f5b5949b35cf95a22d1e2bc573e67799 Mon Sep 17 00:00:00 2001 From: Marcio Granzotto Rodrigues Date: Sun, 27 Sep 2020 12:22:28 -0300 Subject: [PATCH 384/514] Add authentication support to Nightscout (#40602) * Add API Key to the Nightscout integration config * Add tests for nightscout config changes * Apply suggestions from code review Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> Co-authored-by: Chris Talkington * Update homeassistant/components/nightscout/__init__.py * Run translations script to fix en.json Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> Co-authored-by: Chris Talkington --- .../components/nightscout/__init__.py | 5 ++-- .../components/nightscout/config_flow.py | 13 +++++--- .../components/nightscout/manifest.json | 2 +- .../components/nightscout/strings.json | 8 +++-- .../nightscout/translations/en.json | 14 +++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nightscout/__init__.py | 5 ++++ .../components/nightscout/test_config_flow.py | 30 +++++++++++++++++-- 9 files changed, 63 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py index 88939cbe790..1b67963bcc3 100644 --- a/homeassistant/components/nightscout/__init__.py +++ b/homeassistant/components/nightscout/__init__.py @@ -7,7 +7,7 @@ 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.const import CONF_API_KEY, CONF_URL from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -30,8 +30,9 @@ 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_key = entry.data.get(CONF_API_KEY) session = async_get_clientsession(hass) - api = NightscoutAPI(server_url, session=session) + api = NightscoutAPI(server_url, session=session, api_secret=api_key) 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 bd33bc8dcb4..3000d652e46 100644 --- a/homeassistant/components/nightscout/config_flow.py +++ b/homeassistant/components/nightscout/config_flow.py @@ -2,27 +2,32 @@ from asyncio import TimeoutError as AsyncIOTimeoutError import logging -from aiohttp import ClientError +from aiohttp import ClientError, ClientResponseError from py_nightscout import Api as NightscoutAPI import voluptuous as vol from homeassistant import config_entries, exceptions -from homeassistant.const import CONF_URL +from homeassistant.const import CONF_API_KEY, CONF_URL from .const import DOMAIN # pylint:disable=unused-import from .utils import hash_from_url _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema({vol.Required(CONF_URL): str}) +DATA_SCHEMA = vol.Schema({vol.Required(CONF_URL): str, vol.Optional(CONF_API_KEY): str}) async def _validate_input(data): """Validate the user input allows us to connect.""" url = data[CONF_URL] + api_key = data.get(CONF_API_KEY) try: - api = NightscoutAPI(url) + api = NightscoutAPI(url, api_secret=api_key) status = await api.get_server_status() + if status.settings.get("authDefaultRoles") == "status-only": + await api.get_sgvs() + except ClientResponseError as error: + raise InputValidationError("invalid_auth") from error except (ClientError, AsyncIOTimeoutError, OSError) as error: raise InputValidationError("cannot_connect") from error diff --git a/homeassistant/components/nightscout/manifest.json b/homeassistant/components/nightscout/manifest.json index b3e9b3a0d55..ecc44258e90 100644 --- a/homeassistant/components/nightscout/manifest.json +++ b/homeassistant/components/nightscout/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nightscout", "requirements": [ - "py-nightscout==1.2.1" + "py-nightscout==1.2.2" ], "codeowners": [ "@marciogranzotto" diff --git a/homeassistant/components/nightscout/strings.json b/homeassistant/components/nightscout/strings.json index a6e100ae8f2..2240bcec02b 100644 --- a/homeassistant/components/nightscout/strings.json +++ b/homeassistant/components/nightscout/strings.json @@ -3,12 +3,16 @@ "flow_title": "Nightscout", "step": { "user": { + "title": "Enter your Nightscout server information.", + "description": "- URL: the address of your nightscout instance. I.e.: https://myhomeassistant.duckdns.org:5423\n- API Key (Optional): Only use if your instance is protected (auth_default_roles != readable).", "data": { - "url": "URL" + "url": "[%key:common::config_flow::data::url%]", + "api_key": "[%key:common::config_flow::data::api_key%]" } } }, "error": { + "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%]" }, @@ -16,4 +20,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/nightscout/translations/en.json b/homeassistant/components/nightscout/translations/en.json index b7947c84997..ffe5cce81a6 100644 --- a/homeassistant/components/nightscout/translations/en.json +++ b/homeassistant/components/nightscout/translations/en.json @@ -1,18 +1,22 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, "error": { - "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": "Nightscout", "step": { "user": { "data": { - "url": "URL" - } + "api_key": "[%key:common::config_flow::data::api_key%]", + "url": "[%key:common::config_flow::data::url%]" + }, + "description": "- URL: the address of your nightscout instance. I.e.: https://myhomeassistant.duckdns.org:5423\n- API Key (Optional): Only use if your instance is protected (auth_default_roles != readable).", + "title": "Enter your Nightscout server information." } } } diff --git a/requirements_all.txt b/requirements_all.txt index b0c0eaf54b9..76c59fcf791 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1183,7 +1183,7 @@ py-cpuinfo==7.0.0 py-melissa-climate==2.1.4 # homeassistant.components.nightscout -py-nightscout==1.2.1 +py-nightscout==1.2.2 # homeassistant.components.schluter py-schluter==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d9df60ef4ba..9e9899e039e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -573,7 +573,7 @@ py-canary==0.5.0 py-melissa-climate==2.1.4 # homeassistant.components.nightscout -py-nightscout==1.2.1 +py-nightscout==1.2.2 # homeassistant.components.seventeentrack py17track==2.2.2 diff --git a/tests/components/nightscout/__init__.py b/tests/components/nightscout/__init__.py index 52064d1a92b..c7f15068d1d 100644 --- a/tests/components/nightscout/__init__.py +++ b/tests/components/nightscout/__init__.py @@ -22,6 +22,11 @@ SERVER_STATUS = ServerStatus.new_from_json_dict( '{"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}' ) ) +SERVER_STATUS_STATUS_ONLY = ServerStatus.new_from_json_dict( + json.loads( + '{"status":"ok","name":"nightscout","version":"14.0.4","serverTime":"2020-09-25T21:03:59.315Z","serverTimeEpoch":1601067839315,"apiEnabled":true,"careportalEnabled":true,"boluscalcEnabled":true,"settings":{"units":"mg/dl","timeFormat":12,"nightMode":false,"editMode":true,"showRawbg":"never","customTitle":"Nightscout","theme":"default","alarmUrgentHigh":true,"alarmUrgentHighMins":[30,60,90,120],"alarmHigh":true,"alarmHighMins":[30,60,90,120],"alarmLow":true,"alarmLowMins":[15,30,45,60],"alarmUrgentLow":true,"alarmUrgentLowMins":[15,30,45],"alarmUrgentMins":[30,60,90,120],"alarmWarnMins":[30,60,90,120],"alarmTimeagoWarn":true,"alarmTimeagoWarnMins":15,"alarmTimeagoUrgent":true,"alarmTimeagoUrgentMins":30,"alarmPumpBatteryLow":false,"language":"en","scaleY":"log","showPlugins":"dbsize delta direction upbat","showForecast":"ar2","focusHours":3,"heartbeat":60,"baseURL":"","authDefaultRoles":"status-only","thresholds":{"bgHigh":260,"bgTargetTop":180,"bgTargetBottom":80,"bgLow":55},"insecureUseHttp":true,"secureHstsHeader":false,"secureHstsHeaderIncludeSubdomains":false,"secureHstsHeaderPreload":false,"secureCsp":false,"deNormalizeDates":false,"showClockDelta":false,"showClockLastTime":false,"bolusRenderOver":1,"frameUrl1":"","frameUrl2":"","frameUrl3":"","frameUrl4":"","frameUrl5":"","frameUrl6":"","frameUrl7":"","frameUrl8":"","frameName1":"","frameName2":"","frameName3":"","frameName4":"","frameName5":"","frameName6":"","frameName7":"","frameName8":"","DEFAULT_FEATURES":["bgnow","delta","direction","timeago","devicestatus","upbat","errorcodes","profile","dbsize"],"alarmTypes":["predict"],"enable":["careportal","boluscalc","food","bwp","cage","sage","iage","iob","cob","basal","ar2","rawbg","pushover","bgi","pump","openaps","treatmentnotify","bgnow","delta","direction","timeago","devicestatus","upbat","errorcodes","profile","dbsize","ar2"]},"extendedSettings":{"devicestatus":{"advanced":true,"days":1}},"authorized":null}' + ) +) async def init_integration(hass) -> MockConfigEntry: diff --git a/tests/components/nightscout/test_config_flow.py b/tests/components/nightscout/test_config_flow.py index a5f3315fbb1..71983f1b29d 100644 --- a/tests/components/nightscout/test_config_flow.py +++ b/tests/components/nightscout/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Nightscout config flow.""" -from aiohttp import ClientConnectionError +from aiohttp import ClientConnectionError, ClientResponseError from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.nightscout.const import DOMAIN @@ -8,7 +8,11 @@ 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 +from tests.components.nightscout import ( + GLUCOSE_READINGS, + SERVER_STATUS, + SERVER_STATUS_STATUS_ONLY, +) CONFIG = {CONF_URL: "https://some.url:1234"} @@ -55,6 +59,28 @@ async def test_user_form_cannot_connect(hass): assert result2["errors"] == {"base": "cannot_connect"} +async def test_user_form_api_key_required(hass): + """Test we handle an unauthorized 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", + return_value=SERVER_STATUS_STATUS_ONLY, + ), patch( + "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", + side_effect=ClientResponseError(None, None, status=401), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_URL: "https://some.url:1234"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + async def test_user_form_unexpected_exception(hass): """Test we handle unexpected exception.""" result = await hass.config_entries.flow.async_init( From c63cd63c1bf3f7926be7d857a28f04e21055f33c Mon Sep 17 00:00:00 2001 From: Melvin Date: Sun, 27 Sep 2020 17:49:30 +0200 Subject: [PATCH 385/514] Use common strings for oauth config flows (#40608) --- homeassistant/components/almond/strings.json | 2 +- homeassistant/components/home_connect/strings.json | 2 +- homeassistant/components/smappee/strings.json | 2 +- homeassistant/components/somfy/strings.json | 2 +- homeassistant/components/spotify/strings.json | 2 +- homeassistant/components/withings/strings.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/almond/strings.json b/homeassistant/components/almond/strings.json index e8244798e81..9c85eeb92c1 100644 --- a/homeassistant/components/almond/strings.json +++ b/homeassistant/components/almond/strings.json @@ -1,7 +1,7 @@ { "config": { "step": { - "pick_implementation": { "title": "Pick Authentication Method" }, + "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" }, "hassio_confirm": { "title": "Almond via Hass.io add-on", "description": "Do you want to configure Home Assistant to connect to Almond provided by the Hass.io add-on: {addon}?" diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 798fe2930a0..2c624f8b0a3 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "pick_implementation": { - "title": "Pick Authentication Method" + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" } }, "abort": { diff --git a/homeassistant/components/smappee/strings.json b/homeassistant/components/smappee/strings.json index 1bec8fda0cc..63cf4254e54 100644 --- a/homeassistant/components/smappee/strings.json +++ b/homeassistant/components/smappee/strings.json @@ -19,7 +19,7 @@ "title": "Discovered Smappee device" }, "pick_implementation": { - "title": "Pick Authentication Method" + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" } }, "abort": { diff --git a/homeassistant/components/somfy/strings.json b/homeassistant/components/somfy/strings.json index 2f54456091c..384b9c3c5e5 100644 --- a/homeassistant/components/somfy/strings.json +++ b/homeassistant/components/somfy/strings.json @@ -1,7 +1,7 @@ { "config": { "step": { - "pick_implementation": { "title": "Pick Authentication Method" } + "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" } }, "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index 8e3fa6fc679..5a7e56013cc 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -1,7 +1,7 @@ { "config": { "step": { - "pick_implementation": { "title": "Pick Authentication Method" }, + "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" }, "reauth_confirm": { "title": "Re-authenticate with Spotify", "description": "The Spotify integration needs to re-authenticate with Spotify for account: {account}" diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index c9d2d7ca22c..05f6d15ca11 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -7,7 +7,7 @@ "description": "Provide a unique profile name for this data. Typically this is the name of the profile you selected in the previous step.", "data": { "profile": "Profile Name" } }, - "pick_implementation": { "title": "Pick Authentication Method" }, + "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" }, "reauth": { "title": "Re-authenticate Profile", "description": "The \"{profile}\" profile needs to be re-authenticated in order to continue receiving Withings data." From 4ca7b8569bb1eed6b7590464fe7612c8ec376c33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 27 Sep 2020 19:02:44 +0300 Subject: [PATCH 386/514] Remove pre-0.102 Huawei LTE setup noop warnings (#40654) --- homeassistant/components/huawei_lte/device_tracker.py | 9 --------- homeassistant/components/huawei_lte/notify.py | 4 ---- homeassistant/components/huawei_lte/sensor.py | 8 -------- 3 files changed, 21 deletions(-) diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 54e8f318cf6..80578fce7d9 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -153,12 +153,3 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): self._device_state_attributes = { _better_snakecase(k): v for k, v in host.items() if k != "HostName" } - - -def get_scanner(*args, **kwargs): # pylint: disable=useless-return - """Old no longer used way to set up Huawei LTE device tracker.""" - _LOGGER.warning( - "Loading and configuring as a platform is no longer supported or " - "required, convert to enabling/disabling available entities" - ) - return None diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index 91cc8864eb0..375ced911c8 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -19,10 +19,6 @@ _LOGGER = logging.getLogger(__name__) async def async_get_service(hass, config, discovery_info=None): """Get the notification service.""" if discovery_info is None: - _LOGGER.warning( - "Loading as a platform is no longer supported, convert to use " - "config entries or the huawei_lte component" - ) return None router = hass.data[DOMAIN].routers[discovery_info[CONF_URL]] diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index ccdeb47ee88..7fe1a7c1571 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -420,11 +420,3 @@ class HuaweiLteSensor(HuaweiLteBaseEntity): formatter = format_default self._state, self._unit = formatter(value) - - -async def async_setup_platform(*args, **kwargs): - """Old no longer used way to set up Huawei LTE sensors.""" - _LOGGER.warning( - "Loading and configuring as a platform is no longer supported or " - "required, convert to enabling/disabling available entities" - ) From 67a7b28c84b6d8de3ab53a123b2207a96d76caca Mon Sep 17 00:00:00 2001 From: tkdrob Date: Sun, 27 Sep 2020 13:44:21 -0400 Subject: [PATCH 387/514] Add Integration for Goal Zero Yeti Power Stations (#39231) * Add Integration for Goal Zero Yeti Power Stations * Goal Zero Yeti integration with config flow * Remove unused entities * Remove entry from requirements_test_all * Pylint fix * Apply suggestions from code review Co-authored-by: Franck Nijhof * Add tests for goalzero integration * Fix UNIT_PERCENTAGE to PERCENTAGE * isort PERCENTAGE * Add tests * Add en translation * Fix tests * bump goalzero to 0.1.1 * fix await * bump goalzero to 0.1.2 * Update tests/components/goalzero/__init__.py Co-authored-by: J. Nick Koston * apply recommended changes * isort * bump goalzero to 0.1.4 * apply recommended changes * apply recommended changes Co-authored-by: Franck Nijhof Co-authored-by: J. Nick Koston --- .coveragerc | 2 + CODEOWNERS | 1 + homeassistant/components/goalzero/__init__.py | 140 ++++++++++++++++++ .../components/goalzero/binary_sensor.py | 62 ++++++++ .../components/goalzero/config_flow.py | 75 ++++++++++ homeassistant/components/goalzero/const.py | 28 ++++ .../components/goalzero/manifest.json | 8 + .../components/goalzero/strings.json | 22 +++ .../components/goalzero/translations/en.json | 22 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/goalzero/__init__.py | 35 +++++ tests/components/goalzero/test_config_flow.py | 120 +++++++++++++++ 14 files changed, 522 insertions(+) create mode 100644 homeassistant/components/goalzero/__init__.py create mode 100644 homeassistant/components/goalzero/binary_sensor.py create mode 100644 homeassistant/components/goalzero/config_flow.py create mode 100644 homeassistant/components/goalzero/const.py create mode 100644 homeassistant/components/goalzero/manifest.json create mode 100644 homeassistant/components/goalzero/strings.json create mode 100644 homeassistant/components/goalzero/translations/en.json create mode 100644 tests/components/goalzero/__init__.py create mode 100644 tests/components/goalzero/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 6d4c8a66762..a7438497aff 100644 --- a/.coveragerc +++ b/.coveragerc @@ -320,6 +320,8 @@ omit = homeassistant/components/glances/sensor.py homeassistant/components/gntp/notify.py homeassistant/components/goalfeed/* + homeassistant/components/goalzero/__init__.py + homeassistant/components/goalzero/binary_sensor.py homeassistant/components/google/* homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py diff --git a/CODEOWNERS b/CODEOWNERS index cfaae6bc496..c1064f4c60b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -157,6 +157,7 @@ homeassistant/components/geonetnz_volcano/* @exxamalte homeassistant/components/gios/* @bieniu homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff @engrbm87 +homeassistant/components/goalzero/* @tkdrob homeassistant/components/gogogate2/* @vangorra homeassistant/components/google_assistant/* @home-assistant/cloud homeassistant/components/google_cloud/* @lufton diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py new file mode 100644 index 00000000000..892ee46982d --- /dev/null +++ b/homeassistant/components/goalzero/__init__.py @@ -0,0 +1,140 @@ +"""The Goal Zero Yeti integration.""" +import asyncio +import logging + +from goalzero import Yeti, exceptions +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.core import HomeAssistant +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.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ( + DATA_KEY_API, + DATA_KEY_COORDINATOR, + DEFAULT_NAME, + DOMAIN, + MIN_TIME_BETWEEN_UPDATES, +) + +_LOGGER = logging.getLogger(__name__) + +GOALZERO_SCHEMA = vol.Schema( + vol.All( + { + vol.Required(CONF_HOST): cv.matches_regex( + r"\A(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2 \ + [0-4][0-9]|[01]?[0-9][0-9]?)\Z" + ), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + }, + ) +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [GOALZERO_SCHEMA]))}, + extra=vol.ALLOW_EXTRA, +) + + +PLATFORMS = ["binary_sensor"] + + +async def async_setup(hass: HomeAssistant, config): + """Set up the Goal Zero Yeti component.""" + + hass.data[DOMAIN] = {} + + return True + + +async def async_setup_entry(hass, entry): + """Set up Goal Zero Yeti from a config entry.""" + name = entry.data[CONF_NAME] + host = entry.data[CONF_HOST] + + _LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) + + session = async_get_clientsession(hass) + api = Yeti(host, hass.loop, session) + try: + await api.get_state() + except exceptions.ConnectError as ex: + _LOGGER.warning("Failed to connect: %s", ex) + raise ConfigEntryNotReady from ex + + async def async_update_data(): + """Fetch data from API endpoint.""" + try: + await api.get_state() + except exceptions.ConnectError as err: + _LOGGER.warning("Failed to update data from Yeti") + raise UpdateFailed(f"Failed to communicating with API: {err}") from err + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=name, + update_method=async_update_data, + update_interval=MIN_TIME_BETWEEN_UPDATES, + ) + hass.data[DOMAIN][entry.entry_id] = { + DATA_KEY_API: api, + DATA_KEY_COORDINATOR: coordinator, + } + + 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.""" + 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 YetiEntity(CoordinatorEntity): + """Representation of a Goal Zero Yeti entity.""" + + def __init__(self, _api, coordinator, name, sensor_name, server_unique_id): + """Initialize a Goal Zero Yeti entity.""" + super().__init__(coordinator) + self.api = _api + self._name = name + self._server_unique_id = server_unique_id + self._device_class = None + + @property + def device_info(self): + """Return the device information of the entity.""" + return { + "identifiers": {(DOMAIN, self._server_unique_id)}, + "name": self._name, + "manufacturer": "Goal Zero", + } + + @property + def device_class(self): + """Return the class of this device.""" + return self._device_class diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py new file mode 100644 index 00000000000..25b370c459f --- /dev/null +++ b/homeassistant/components/goalzero/binary_sensor.py @@ -0,0 +1,62 @@ +"""Support for Goal Zero Yeti Sensors.""" +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.const import CONF_NAME + +from . import YetiEntity +from .const import BINARY_SENSOR_DICT, DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Goal Zero Yeti sensor.""" + name = entry.data[CONF_NAME] + goalzero_data = hass.data[DOMAIN][entry.entry_id] + sensors = [ + YetiBinarySensor( + goalzero_data[DATA_KEY_API], + goalzero_data[DATA_KEY_COORDINATOR], + name, + sensor_name, + entry.entry_id, + ) + for sensor_name in BINARY_SENSOR_DICT + ] + async_add_entities(sensors, True) + + +class YetiBinarySensor(YetiEntity, BinarySensorEntity): + """Representation of a Goal Zero Yeti sensor.""" + + def __init__(self, api, coordinator, name, sensor_name, server_unique_id): + """Initialize a Goal Zero Yeti sensor.""" + super().__init__(api, coordinator, name, sensor_name, server_unique_id) + + self._condition = sensor_name + + variable_info = BINARY_SENSOR_DICT[sensor_name] + self._condition_name = variable_info[0] + self._icon = variable_info[2] + self.api = api + self._device_class = variable_info[1] + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._name} {self._condition_name}" + + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return f"{self._server_unique_id}/{self._condition_name}" + + @property + def is_on(self): + """Return if the service is on.""" + if self.api.data: + return self.api.data[self._condition] == 1 + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon diff --git a/homeassistant/components/goalzero/config_flow.py b/homeassistant/components/goalzero/config_flow.py new file mode 100644 index 00000000000..31c1a51efeb --- /dev/null +++ b/homeassistant/components/goalzero/config_flow.py @@ -0,0 +1,75 @@ +"""Config flow for Goal Zero Yeti integration.""" +import logging + +from goalzero import Yeti, exceptions +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DEFAULT_NAME, DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({"host": str, "name": str}) + + +class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Goal Zero Yeti.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + errors = {} + + if user_input is not None: + host = user_input[CONF_HOST] + name = user_input[CONF_NAME] + + if await self._async_endpoint_existed(host): + return self.async_abort(reason="already_configured") + + try: + await self._async_try_connect(host) + return self.async_create_entry( + title=name, + data={CONF_HOST: host, CONF_NAME: name}, + ) + except exceptions.ConnectError: + errors["base"] = "cannot_connect" + _LOGGER.exception("Error connecting to device at %s", host) + except exceptions.InvalidHost: + errors["base"] = "invalid_host" + _LOGGER.exception("Invalid data received from device at %s", host) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + user_input = user_input or {} + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_HOST, default=user_input.get(CONF_HOST) or "" + ): str, + vol.Optional( + CONF_NAME, default=user_input.get(CONF_NAME) or DEFAULT_NAME + ): str, + } + ), + errors=errors, + ) + + async def _async_endpoint_existed(self, endpoint): + for entry in self._async_current_entries(): + if endpoint == entry.data.get(CONF_HOST): + return endpoint + + async def _async_try_connect(self, host): + session = async_get_clientsession(self.hass) + api = Yeti(host, self.hass.loop, session) + await api.get_state() diff --git a/homeassistant/components/goalzero/const.py b/homeassistant/components/goalzero/const.py new file mode 100644 index 00000000000..3afa1e537c1 --- /dev/null +++ b/homeassistant/components/goalzero/const.py @@ -0,0 +1,28 @@ +"""Constants for the Goal Zero Yeti integration.""" +from datetime import timedelta + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_POWER, +) + +DATA_KEY_COORDINATOR = "coordinator" +DOMAIN = "goalzero" +DEFAULT_NAME = "Yeti" +DATA_KEY_API = "api" + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) + +BINARY_SENSOR_DICT = { + "v12PortStatus": ["12V Port Status", DEVICE_CLASS_POWER, None], + "usbPortStatus": ["USB Port Status", DEVICE_CLASS_POWER, None], + "acPortStatus": ["AC Port Status", DEVICE_CLASS_POWER, None], + "backlight": ["Backlight", None, "mdi:clock-digital"], + "app_online": [ + "App Online", + DEVICE_CLASS_CONNECTIVITY, + None, + ], + "isCharging": ["Charging", DEVICE_CLASS_BATTERY_CHARGING, None], +} diff --git a/homeassistant/components/goalzero/manifest.json b/homeassistant/components/goalzero/manifest.json new file mode 100644 index 00000000000..803b8f7eaae --- /dev/null +++ b/homeassistant/components/goalzero/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "goalzero", + "name": "Goal Zero Yeti", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/goalzero", + "requirements": ["goalzero==0.1.4"], + "codeowners": ["@tkdrob"] +} diff --git a/homeassistant/components/goalzero/strings.json b/homeassistant/components/goalzero/strings.json new file mode 100644 index 00000000000..e7a134c01ec --- /dev/null +++ b/homeassistant/components/goalzero/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "title": "Goal Zero Yeti", + "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wifi network. Then get the host ip from your router. DHCP must be set up in your router settings for the device to ensure the host ip does not change. Refer to your router's user manual.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "name": "Name" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_host": "This is not the Yeti you are looking for", + "unknown": "Unknown Error" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + } + } +} diff --git a/homeassistant/components/goalzero/translations/en.json b/homeassistant/components/goalzero/translations/en.json new file mode 100644 index 00000000000..412bef4c1d9 --- /dev/null +++ b/homeassistant/components/goalzero/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Already Configured" + }, + "error": { + "cannot_connect": "Error connecting to host", + "invalid_host": "This is not a Yeti", + "unknown": "Unknown Error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name" + }, + "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wifi network. Then get the host ip from your router. DHCP must be set up in your router settings for the device to ensure the host ip does not change. Refer to your router's user manual.", + "title": "Goal Zero Yeti" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 55e6bf2eafe..bfd3c340e6d 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -68,6 +68,7 @@ FLOWS = [ "geonetnz_volcano", "gios", "glances", + "goalzero", "gogogate2", "gpslogger", "griddy", diff --git a/requirements_all.txt b/requirements_all.txt index 76c59fcf791..f8fb22385f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -668,6 +668,9 @@ glances_api==0.2.0 # homeassistant.components.gntp gntp==1.0.3 +# homeassistant.components.goalzero +goalzero==0.1.4 + # homeassistant.components.gogogate2 gogogate2-api==2.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e9899e039e..c7b33625fc1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -336,6 +336,9 @@ gios==0.1.4 # homeassistant.components.glances glances_api==0.2.0 +# homeassistant.components.goalzero +goalzero==0.1.4 + # homeassistant.components.gogogate2 gogogate2-api==2.0.3 diff --git a/tests/components/goalzero/__init__.py b/tests/components/goalzero/__init__.py new file mode 100644 index 00000000000..0ba2a912766 --- /dev/null +++ b/tests/components/goalzero/__init__.py @@ -0,0 +1,35 @@ +"""Tests for the Goal Zero Yeti integration.""" + +from homeassistant.const import CONF_HOST, CONF_NAME + +from tests.async_mock import AsyncMock, patch + +HOST = "1.2.3.4" +NAME = "Yeti" + +CONF_DATA = { + CONF_HOST: HOST, + CONF_NAME: NAME, +} + +CONF_CONFIG_FLOW = { + CONF_HOST: HOST, + CONF_NAME: NAME, +} + + +async def _create_mocked_yeti(raise_exception=False): + mocked_yeti = AsyncMock() + mocked_yeti.get_state = AsyncMock() + return mocked_yeti + + +def _patch_init_yeti(mocked_yeti): + return patch("homeassistant.components.goalzero.Yeti", return_value=mocked_yeti) + + +def _patch_config_flow_yeti(mocked_yeti): + return patch( + "homeassistant.components.goalzero.config_flow.Yeti", + return_value=mocked_yeti, + ) diff --git a/tests/components/goalzero/test_config_flow.py b/tests/components/goalzero/test_config_flow.py new file mode 100644 index 00000000000..5a367c452c6 --- /dev/null +++ b/tests/components/goalzero/test_config_flow.py @@ -0,0 +1,120 @@ +"""Test Goal Zero Yeti config flow.""" +from goalzero import exceptions + +from homeassistant.components.goalzero.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import ( + CONF_CONFIG_FLOW, + CONF_DATA, + CONF_HOST, + CONF_NAME, + NAME, + _create_mocked_yeti, + _patch_config_flow_yeti, +) + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +def _flow_next(hass, flow_id): + return next( + flow + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == flow_id + ) + + +def _patch_setup(): + return patch( + "homeassistant.components.goalzero.async_setup_entry", + return_value=True, + ) + + +async def test_flow_user(hass): + """Test user initialized flow.""" + mocked_yeti = await _create_mocked_yeti() + with _patch_config_flow_yeti(mocked_yeti), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + _flow_next(hass, result["flow_id"]) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_CONFIG_FLOW, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"] == CONF_DATA + + +async def test_flow_user_already_configured(hass): + """Test user initialized flow with duplicate server.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.2.3.4", CONF_NAME: "Yeti"}, + ) + + entry.add_to_hass(hass) + + service_info = { + "host": "1.2.3.4", + "name": "Yeti", + } + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=service_info + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_user_cannot_connect(hass): + """Test user initialized flow with unreachable server.""" + mocked_yeti = await _create_mocked_yeti(True) + with _patch_config_flow_yeti(mocked_yeti) as yetimock: + yetimock.side_effect = exceptions.ConnectError + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_flow_user_invalid_host(hass): + """Test user initialized flow with invalid server.""" + mocked_yeti = await _create_mocked_yeti(True) + with _patch_config_flow_yeti(mocked_yeti) as yetimock: + yetimock.side_effect = exceptions.InvalidHost + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_host"} + + +async def test_flow_user_unknown_error(hass): + """Test user initialized flow with unreachable server.""" + mocked_yeti = await _create_mocked_yeti(True) + with _patch_config_flow_yeti(mocked_yeti) as yetimock: + yetimock.side_effect = Exception + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "unknown"} From abcfbf790ba0317bbd320aef6529bbb6cd65856d Mon Sep 17 00:00:00 2001 From: stephan192 Date: Sun, 27 Sep 2020 19:46:41 +0200 Subject: [PATCH 388/514] Bump dwdwfsapi to v1.0.3 (#40644) --- homeassistant/components/dwd_weather_warnings/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dwd_weather_warnings/manifest.json b/homeassistant/components/dwd_weather_warnings/manifest.json index e67fbb08e29..d034ec3d9df 100644 --- a/homeassistant/components/dwd_weather_warnings/manifest.json +++ b/homeassistant/components/dwd_weather_warnings/manifest.json @@ -3,5 +3,5 @@ "name": "Deutsche Wetter Dienst (DWD) Weather Warnings", "documentation": "https://www.home-assistant.io/integrations/dwd_weather_warnings", "codeowners": ["@runningman84", "@stephan192", "@Hummel95"], - "requirements": ["dwdwfsapi==1.0.2"] + "requirements": ["dwdwfsapi==1.0.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index f8fb22385f6..0879d069f00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -511,7 +511,7 @@ dovado==0.4.1 dsmr_parser==0.18 # homeassistant.components.dwd_weather_warnings -dwdwfsapi==1.0.2 +dwdwfsapi==1.0.3 # homeassistant.components.dweet dweepy==0.3.0 From 52b745b8a9d477eb2a5583840dcc19dabc2b0fdf Mon Sep 17 00:00:00 2001 From: Chris <1828125+digitallyserviced@users.noreply.github.com> Date: Sun, 27 Sep 2020 14:40:57 -0400 Subject: [PATCH 389/514] set ID3 tags as TextFrame types (#40666) --- homeassistant/components/tts/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 0f758d4a2eb..e5f762af647 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -11,6 +11,7 @@ from typing import Dict, Optional from aiohttp import web import mutagen +from mutagen.id3 import TextFrame as ID3Text import voluptuous as vol from homeassistant.components.http import HomeAssistantView @@ -467,9 +468,9 @@ class SpeechManager: try: tts_file = mutagen.File(data_bytes, easy=True) if tts_file is not None: - tts_file["artist"] = artist - tts_file["album"] = album - tts_file["title"] = message + tts_file["artist"] = ID3Text(encoding=3, text=artist) + tts_file["album"] = ID3Text(encoding=3, text=album) + tts_file["title"] = ID3Text(encoding=3, text=message) tts_file.save(data_bytes) except mutagen.MutagenError as err: _LOGGER.error("ID3 tag error: %s", err) From ef751c0961911fe06e40dc156020ae3ea472b924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Kr=C3=B3l?= Date: Sun, 27 Sep 2020 21:02:29 +0200 Subject: [PATCH 390/514] Update wolf_smartset to 0.1.6 (#40668) * Bump wolf_smartset version. Support not fully fetched data * Fix black and pylint errors Co-authored-by: Chris Talkington * Remove dot from exception message. Co-authored-by: Chris Talkington Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> Co-authored-by: Chris Talkington --- homeassistant/components/wolflink/__init__.py | 6 +++--- .../components/wolflink/manifest.json | 2 +- homeassistant/components/wolflink/sensor.py | 18 ++++++++---------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py index 9a272c502a0..611fa7da315 100644 --- a/homeassistant/components/wolflink/__init__.py +++ b/homeassistant/components/wolflink/__init__.py @@ -2,7 +2,7 @@ from datetime import timedelta import logging -from httpcore import ConnectError +from httpcore import ConnectError, ConnectTimeout from wolf_smartset.token_auth import InvalidAuth from wolf_smartset.wolf_client import WolfClient @@ -99,7 +99,7 @@ async def fetch_parameters(client: WolfClient, gateway_id: int, device_id: int): try: 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: + except (ConnectError, ConnectTimeout) as exception: raise UpdateFailed(f"Error communicating with API: {exception}") from exception except InvalidAuth as exception: - raise UpdateFailed("Invalid authentication during update.") from exception + raise UpdateFailed("Invalid authentication during update") from exception diff --git a/homeassistant/components/wolflink/manifest.json b/homeassistant/components/wolflink/manifest.json index c188c090369..633318f2f62 100644 --- a/homeassistant/components/wolflink/manifest.json +++ b/homeassistant/components/wolflink/manifest.json @@ -3,6 +3,6 @@ "name": "Wolf SmartSet Service", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wolflink", - "requirements": ["wolf_smartset==0.1.4"], + "requirements": ["wolf_smartset==0.1.6"], "codeowners": ["@adamkrol93"] } diff --git a/homeassistant/components/wolflink/sensor.py b/homeassistant/components/wolflink/sensor.py index 97f48e27988..1cae006824b 100644 --- a/homeassistant/components/wolflink/sensor.py +++ b/homeassistant/components/wolflink/sensor.py @@ -11,13 +11,6 @@ from wolf_smartset.models import ( Temperature, ) -from homeassistant.components.wolflink.const import ( - COORDINATOR, - DEVICE_ID, - DOMAIN, - PARAMETERS, - STATES, -) from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, @@ -27,6 +20,8 @@ from homeassistant.const import ( ) from homeassistant.helpers.update_coordinator import CoordinatorEntity +from .const import COORDINATOR, DEVICE_ID, DOMAIN, PARAMETERS, STATES + _LOGGER = logging.getLogger(__name__) @@ -63,6 +58,7 @@ class WolfLinkSensor(CoordinatorEntity): super().__init__(coordinator) self.wolf_object = wolf_object self.device_id = device_id + self._state = None @property def name(self): @@ -71,8 +67,10 @@ class WolfLinkSensor(CoordinatorEntity): @property def state(self): - """Return the state.""" - return self.coordinator.data[self.wolf_object.value_id] + """Return the state. Wolf Client is returning only changed values so we need to store old value here.""" + if self.wolf_object.value_id in self.coordinator.data: + self._state = self.coordinator.data[self.wolf_object.value_id] + return self._state @property def device_state_attributes(self): @@ -151,7 +149,7 @@ class WolfLinkState(WolfLinkSensor): @property def state(self): """Return the state converting with supported values.""" - state = self.coordinator.data[self.wolf_object.value_id] + state = super().state resolved_state = [ item for item in self.wolf_object.items if item.value == int(state) ] diff --git a/requirements_all.txt b/requirements_all.txt index 0879d069f00..0c9c287be2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2262,7 +2262,7 @@ withings-api==2.1.6 wled==0.4.4 # homeassistant.components.wolflink -wolf_smartset==0.1.4 +wolf_smartset==0.1.6 # homeassistant.components.xbee xbee-helper==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7b33625fc1..3fe14f2404e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1055,7 +1055,7 @@ withings-api==2.1.6 wled==0.4.4 # homeassistant.components.wolflink -wolf_smartset==0.1.4 +wolf_smartset==0.1.6 # homeassistant.components.bluesound # homeassistant.components.rest From e320c3b73584d7a527e47990da82a5502f828df8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 27 Sep 2020 21:51:37 +0200 Subject: [PATCH 391/514] Pin gRPC to 1.31.0 to workaround amrv7 issues (#40678) --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2658e755890..e90d015ff1c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,6 +39,10 @@ urllib3>=1.24.3 # Constrain httplib2 to protect against CVE-2020-11078 httplib2>=0.18.0 +# gRPC 1.32+ currently causes issues on ARMv7, see: +# https://github.com/home-assistant/core/issues/40148 +grpcio==1.31.0 + # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 62b1a22cc73..c3e489c1ebb 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -69,6 +69,10 @@ urllib3>=1.24.3 # Constrain httplib2 to protect against CVE-2020-11078 httplib2>=0.18.0 +# gRPC 1.32+ currently causes issues on ARMv7, see: +# https://github.com/home-assistant/core/issues/40148 +grpcio==1.31.0 + # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 From 9a32e285749518bd2ec3dcb53c5942e75fe723dc Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 28 Sep 2020 04:38:14 +0800 Subject: [PATCH 392/514] Create master playlist for cast (#40483) Co-authored-by: Jason Hunter --- homeassistant/components/stream/fmp4utils.py | 111 ++++++++++++++++ homeassistant/components/stream/hls.py | 126 +++++++++++-------- homeassistant/components/stream/recorder.py | 2 +- 3 files changed, 188 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index 00603807215..dc929e531c1 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -36,3 +36,114 @@ def get_m4s(segment: io.BytesIO, sequence: int) -> bytes: mfra_location = next(find_box(segment, b"mfra")) segment.seek(moof_location) return segment.read(mfra_location - moof_location) + + +def get_codec_string(segment: io.BytesIO) -> str: + """Get RFC 6381 codec string.""" + codecs = [] + + # Find moov + moov_location = next(find_box(segment, b"moov")) + + # Find tracks + for trak_location in find_box(segment, b"trak", moov_location): + # Drill down to media info + mdia_location = next(find_box(segment, b"mdia", trak_location)) + minf_location = next(find_box(segment, b"minf", mdia_location)) + stbl_location = next(find_box(segment, b"stbl", minf_location)) + stsd_location = next(find_box(segment, b"stsd", stbl_location)) + + # Get stsd box + segment.seek(stsd_location) + stsd_length = int.from_bytes(segment.read(4), byteorder="big") + segment.seek(stsd_location) + stsd_box = segment.read(stsd_length) + + # Base Codec + codec = stsd_box[20:24].decode("utf-8") + + # Handle H264 + if ( + codec in ("avc1", "avc2", "avc3", "avc4") + and stsd_length > 110 + and stsd_box[106:110] == b"avcC" + ): + profile = stsd_box[111:112].hex() + compatibility = stsd_box[112:113].hex() + level = stsd_box[113:114].hex() + codec += "." + profile + compatibility + level + + # Handle H265 + elif ( + codec in ("hev1", "hvc1") + and stsd_length > 110 + and stsd_box[106:110] == b"hvcC" + ): + tmp_byte = int.from_bytes(stsd_box[111:112], byteorder="big") + + # Profile Space + codec += "." + profile_space_map = {0: "", 1: "A", 2: "B", 3: "C"} + profile_space = tmp_byte >> 6 + codec += profile_space_map[profile_space] + general_profile_idc = tmp_byte & 31 + codec += str(general_profile_idc) + + # Compatibility + codec += "." + general_profile_compatibility = int.from_bytes( + stsd_box[112:116], byteorder="big" + ) + reverse = 0 + for i in range(0, 32): + reverse |= general_profile_compatibility & 1 + if i == 31: + break + reverse <<= 1 + general_profile_compatibility >>= 1 + codec += hex(reverse)[2:] + + # Tier Flag + if (tmp_byte & 32) >> 5 == 0: + codec += ".L" + else: + codec += ".H" + codec += str(int.from_bytes(stsd_box[122:123], byteorder="big")) + + # Constraint String + has_byte = False + constraint_string = "" + for i in range(121, 115, -1): + gci = int.from_bytes(stsd_box[i : i + 1], byteorder="big") + if gci or has_byte: + constraint_string = "." + hex(gci)[2:] + constraint_string + has_byte = True + codec += constraint_string + + # Handle Audio + elif codec == "mp4a": + oti = None + dsi = None + + # Parse ES Descriptors + oti_loc = stsd_box.find(b"\x04\x80\x80\x80") + if oti_loc > 0: + oti = stsd_box[oti_loc + 5 : oti_loc + 6].hex() + codec += f".{oti}" + + dsi_loc = stsd_box.find(b"\x05\x80\x80\x80") + if dsi_loc > 0: + dsi_length = int.from_bytes( + stsd_box[dsi_loc + 4 : dsi_loc + 5], byteorder="big" + ) + dsi_data = stsd_box[dsi_loc + 5 : dsi_loc + 5 + dsi_length] + dsi0 = int.from_bytes(dsi_data[0:1], byteorder="big") + dsi = (dsi0 & 248) >> 3 + if dsi == 31 and len(dsi_data) >= 2: + dsi1 = int.from_bytes(dsi_data[1:2], byteorder="big") + dsi = 32 + ((dsi0 & 7) << 3) + ((dsi1 & 224) >> 5) + codec += f".{dsi}" + + codecs.append(codec) + + return ",".join(codecs) diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 816d1231c4c..09729f79ada 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -1,4 +1,5 @@ """Provide functionality to stream HLS.""" +import io from typing import Callable from aiohttp import web @@ -7,7 +8,7 @@ from homeassistant.core import callback from .const import FORMAT_CONTENT_TYPE from .core import PROVIDERS, StreamOutput, StreamView -from .fmp4utils import get_init, get_m4s +from .fmp4utils import get_codec_string, get_init, get_m4s @callback @@ -16,7 +17,43 @@ def async_setup_hls(hass): hass.http.register_view(HlsPlaylistView()) hass.http.register_view(HlsSegmentView()) hass.http.register_view(HlsInitView()) - return "/api/hls/{}/playlist.m3u8" + hass.http.register_view(HlsMasterPlaylistView()) + return "/api/hls/{}/master_playlist.m3u8" + + +class HlsMasterPlaylistView(StreamView): + """Stream view used only for Chromecast compatibility.""" + + url = r"/api/hls/{token:[a-f0-9]+}/master_playlist.m3u8" + name = "api:stream:hls:master_playlist" + cors_allowed = True + + @staticmethod + def render(track): + """Render M3U8 file.""" + # Need to calculate max bandwidth as input_container.bit_rate doesn't seem to work + # Calculate file size / duration and use a multiplier to account for variation + segment = track.get_segment(track.segments[-1]) + bandwidth = round( + segment.segment.seek(0, io.SEEK_END) * 8 / segment.duration * 3 + ) + codecs = get_codec_string(segment.segment) + lines = [ + "#EXTM3U", + f'#EXT-X-STREAM-INF:BANDWIDTH={bandwidth},CODECS="{codecs}"', + "playlist.m3u8", + ] + return "\n".join(lines) + "\n" + + async def handle(self, request, stream, sequence): + """Return m3u8 playlist.""" + track = stream.add_provider("hls") + stream.start() + # Wait for a segment to be ready + if not track.segments: + await track.recv() + headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]} + return web.Response(body=self.render(track).encode("utf-8"), headers=headers) class HlsPlaylistView(StreamView): @@ -26,18 +63,50 @@ class HlsPlaylistView(StreamView): name = "api:stream:hls:playlist" cors_allowed = True + @staticmethod + def render_preamble(track): + """Render preamble.""" + return [ + "#EXT-X-VERSION:7", + f"#EXT-X-TARGETDURATION:{track.target_duration}", + '#EXT-X-MAP:URI="init.mp4"', + ] + + @staticmethod + def render_playlist(track): + """Render playlist.""" + segments = track.segments + + if not segments: + return [] + + playlist = ["#EXT-X-MEDIA-SEQUENCE:{}".format(segments[0])] + + for sequence in segments: + segment = track.get_segment(sequence) + playlist.extend( + [ + "#EXTINF:{:.04f},".format(float(segment.duration)), + f"./segment/{segment.sequence}.m4s", + ] + ) + + return playlist + + def render(self, track): + """Render M3U8 file.""" + lines = ["#EXTM3U"] + self.render_preamble(track) + self.render_playlist(track) + return "\n".join(lines) + "\n" + async def handle(self, request, stream, sequence): """Return m3u8 playlist.""" - renderer = M3U8Renderer(stream) track = stream.add_provider("hls") stream.start() # Wait for a segment to be ready if not track.segments: await track.recv() headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]} - return web.Response( - body=renderer.render(track).encode("utf-8"), headers=headers - ) + return web.Response(body=self.render(track).encode("utf-8"), headers=headers) class HlsInitView(StreamView): @@ -77,49 +146,6 @@ class HlsSegmentView(StreamView): ) -class M3U8Renderer: - """M3U8 Render Helper.""" - - def __init__(self, stream): - """Initialize renderer.""" - self.stream = stream - - @staticmethod - def render_preamble(track): - """Render preamble.""" - return [ - "#EXT-X-VERSION:7", - f"#EXT-X-TARGETDURATION:{track.target_duration}", - '#EXT-X-MAP:URI="init.mp4"', - ] - - @staticmethod - def render_playlist(track): - """Render playlist.""" - segments = track.segments - - if not segments: - return [] - - playlist = ["#EXT-X-MEDIA-SEQUENCE:{}".format(segments[0])] - - for sequence in segments: - segment = track.get_segment(sequence) - playlist.extend( - [ - "#EXTINF:{:.04f},".format(float(segment.duration)), - f"./segment/{segment.sequence}.m4s", - ] - ) - - return playlist - - def render(self, track): - """Render M3U8 file.""" - lines = ["#EXTM3U"] + self.render_preamble(track) + self.render_playlist(track) - return "\n".join(lines) + "\n" - - @PROVIDERS.register("hls") class HlsStreamOutput(StreamOutput): """Represents HLS Output formats.""" @@ -137,7 +163,7 @@ class HlsStreamOutput(StreamOutput): @property def audio_codecs(self) -> str: """Return desired audio codecs.""" - return {"aac", "ac3", "mp3"} + return {"aac", "mp3"} @property def video_codecs(self) -> tuple: diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 82b146cc51f..d0b8789f602 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -78,7 +78,7 @@ class RecorderOutput(StreamOutput): @property def audio_codecs(self) -> str: """Return desired audio codec.""" - return {"aac", "ac3", "mp3"} + return {"aac", "mp3"} @property def video_codecs(self) -> tuple: From 088558b8dfd453cb4b85ccba454204f08984a372 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 27 Sep 2020 23:03:47 +0200 Subject: [PATCH 393/514] Increase the timeout during config entry setup in Shelly integration (#40684) --- 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 c3b701449c2..c63858f0652 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -40,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): temperature_unit, ) try: - async with async_timeout.timeout(5): + async with async_timeout.timeout(10): device = await aioshelly.Device.create( aiohttp_client.async_get_clientsession(hass), options, From 13b6fe2bbaf68f95e7bf4d5a82beedb9b4005bbf Mon Sep 17 00:00:00 2001 From: Colin Frei Date: Sun, 27 Sep 2020 23:19:46 +0200 Subject: [PATCH 394/514] Fix fitbit current URL not available while configuring (#40547) Co-authored-by: Franck Nijhof Co-authored-by: Paulus Schoutsen --- homeassistant/components/fitbit/sensor.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index f0914ab35f0..f6e3fd90fe5 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -185,9 +185,7 @@ 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, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}" - ) + start_url = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}" description = f"""Please create a Fitbit developer app at https://dev.fitbit.com/apps/new. @@ -222,7 +220,7 @@ def request_oauth_completion(hass): def fitbit_configuration_callback(callback_data): """Handle configuration updates.""" - start_url = f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_START}" + start_url = f"{get_url(hass)}{FITBIT_AUTH_START}" description = f"Please authorize Fitbit by visiting {start_url}" @@ -314,9 +312,7 @@ 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, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}" - ) + redirect_uri = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}" fitbit_auth_start_url, _ = oauth.authorize_token_url( redirect_uri=redirect_uri, From 0a16ae6482174f29edb37453061fac3bfe179e1b Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sun, 27 Sep 2020 18:13:46 -0400 Subject: [PATCH 395/514] Upgrade zigpy-znp from 0.1.1 to 0.2.0 (#40674) --- 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 b03d4afd971..05d6ccd061c 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -12,7 +12,7 @@ "zigpy==0.24.1", "zigpy-xbee==0.13.0", "zigpy-zigate==0.6.2", - "zigpy-znp==0.1.1" + "zigpy-znp==0.2.0" ], "codeowners": ["@dmulcahey", "@adminiuga"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0c9c287be2e..673c99809e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2326,7 +2326,7 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.6.2 # homeassistant.components.zha -zigpy-znp==0.1.1 +zigpy-znp==0.2.0 # homeassistant.components.zha zigpy==0.24.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3fe14f2404e..ae5cb6fe359 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1086,7 +1086,7 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.6.2 # homeassistant.components.zha -zigpy-znp==0.1.1 +zigpy-znp==0.2.0 # homeassistant.components.zha zigpy==0.24.1 From f71816d328e23a87d590dfc5d28614a18a0a2db0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 28 Sep 2020 01:25:04 +0200 Subject: [PATCH 396/514] Fix issue with HobbyBoard moisture meter (#40680) --- homeassistant/components/onewire/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 7409be73712..0a7976b877b 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -156,7 +156,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): owproxy.read(f"{device}moisture/is_leaf.{s_id}").decode() ) if is_leaf: - sensor_key = f"wetness_{id}" + sensor_key = f"wetness_{s_id}" sensor_id = os.path.split(os.path.split(device)[0])[1] device_file = os.path.join(os.path.split(device)[0], sensor_value) devs.append( From f886ea9e3d1e461cd59444cff152e8fcd6c1081b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 28 Sep 2020 02:02:20 +0200 Subject: [PATCH 397/514] Move onewire constants to separate file (#40550) * Move constants to separate file * Ignore coverage of const.py --- .coveragerc | 1 + homeassistant/components/onewire/const.py | 12 ++++++++++++ homeassistant/components/onewire/sensor.py | 12 +++++++----- 3 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/onewire/const.py diff --git a/.coveragerc b/.coveragerc index a7438497aff..c25b0f45c7e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -606,6 +606,7 @@ omit = homeassistant/components/omnilogic/__init__.py homeassistant/components/omnilogic/common.py homeassistant/components/omnilogic/sensor.py + homeassistant/components/onewire/const.py homeassistant/components/onewire/sensor.py homeassistant/components/onkyo/media_player.py homeassistant/components/onvif/__init__.py diff --git a/homeassistant/components/onewire/const.py b/homeassistant/components/onewire/const.py new file mode 100644 index 00000000000..af68135af10 --- /dev/null +++ b/homeassistant/components/onewire/const.py @@ -0,0 +1,12 @@ +"""Constants for 1-Wire component.""" +CONF_MOUNT_DIR = "mount_dir" +CONF_NAMES = "names" + +DEFAULT_OWSERVER_PORT = 4304 +DEFAULT_SYSBUS_MOUNT_DIR = "/sys/bus/w1/devices/" + +DOMAIN = "onewire" + +SUPPORTED_PLATFORMS = [ + "sensor", +] diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 0a7976b877b..1d0d3c2ebba 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -20,13 +20,15 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from .const import ( + CONF_MOUNT_DIR, + CONF_NAMES, + DEFAULT_OWSERVER_PORT, + DEFAULT_SYSBUS_MOUNT_DIR, +) + _LOGGER = logging.getLogger(__name__) -CONF_MOUNT_DIR = "mount_dir" -CONF_NAMES = "names" - -DEFAULT_OWSERVER_PORT = 4304 -DEFAULT_SYSBUS_MOUNT_DIR = "/sys/bus/w1/devices/" DEVICE_SENSORS = { # Family : { SensorType: owfs path } "10": {"temperature": "temperature"}, From e9e17122e7f47599f2bab2fa6b63703e9a27da99 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 28 Sep 2020 00:03:28 +0000 Subject: [PATCH 398/514] [ci skip] Translation update --- .../components/almond/translations/ko.json | 2 +- .../binary_sensor/translations/uk.json | 14 ++++ .../components/blink/translations/ko.json | 2 + .../components/bond/translations/ko.json | 4 + .../components/broadlink/translations/it.json | 1 + .../components/broadlink/translations/ko.json | 37 ++++++++- .../components/canary/translations/it.json | 31 ++++++++ .../components/canary/translations/ko.json | 1 + .../components/climate/translations/uk.json | 19 ++++- .../components/cover/translations/uk.json | 3 + .../components/eafm/translations/ko.json | 16 ++++ .../components/fan/translations/uk.json | 6 ++ .../components/flo/translations/ko.json | 20 +++++ .../components/freebox/translations/it.json | 6 +- .../freebox/translations/zh-Hant.json | 6 +- .../components/goalzero/translations/ca.json | 22 ++++++ .../components/goalzero/translations/en.json | 6 +- .../home_connect/translations/ko.json | 2 +- .../humidifier/translations/uk.json | 8 ++ .../components/insteon/translations/ko.json | 77 +++++++++++++++++++ .../components/iqvia/translations/ko.json | 3 + .../components/kodi/translations/ko.json | 35 +++++++++ .../components/light/translations/uk.json | 6 ++ .../meteo_france/translations/ko.json | 15 ++++ .../components/monoprice/translations/it.json | 2 +- .../monoprice/translations/zh-Hant.json | 2 +- .../components/netatmo/translations/ko.json | 2 +- .../nightscout/translations/ca.json | 6 +- .../nightscout/translations/en.json | 12 +-- .../nightscout/translations/ko.json | 3 + .../nightscout/translations/ru.json | 6 +- .../nightscout/translations/zh-Hant.json | 6 +- .../components/omnilogic/translations/es.json | 30 ++++++++ .../components/omnilogic/translations/it.json | 30 ++++++++ .../components/omnilogic/translations/ko.json | 30 ++++++++ .../ovo_energy/translations/ko.json | 4 +- .../components/plugwise/translations/it.json | 5 +- .../components/plugwise/translations/ko.json | 3 +- .../components/remote/translations/uk.json | 6 ++ .../components/rfxtrx/translations/ko.json | 7 ++ .../components/risco/translations/ko.json | 15 ++++ .../components/roon/translations/ko.json | 14 ++++ .../components/sensor/translations/uk.json | 5 ++ .../components/sentry/translations/ko.json | 16 ++++ .../components/shelly/translations/es.json | 3 +- .../components/shelly/translations/it.json | 3 +- .../components/shelly/translations/ko.json | 3 + .../components/shelly/translations/ru.json | 3 +- .../shelly/translations/zh-Hant.json | 3 +- .../components/smappee/translations/ca.json | 2 +- .../components/smappee/translations/ko.json | 8 +- .../components/somfy/translations/es.json | 3 +- .../components/somfy/translations/it.json | 5 +- .../components/somfy/translations/ko.json | 3 +- .../somfy/translations/zh-Hant.json | 5 +- .../components/spider/translations/ko.json | 8 ++ .../components/spotify/translations/ko.json | 3 +- .../components/switch/translations/uk.json | 6 ++ .../components/tag/translations/ko.json | 3 + .../components/toon/translations/ko.json | 2 +- .../components/unifi/translations/it.json | 3 +- .../components/vizio/translations/ko.json | 1 + .../components/wilight/translations/ko.json | 12 +++ .../components/withings/translations/ko.json | 2 +- .../xiaomi_aqara/translations/ko.json | 4 +- .../zodiac/translations/sensor.it.json | 18 +++++ .../zodiac/translations/sensor.ko.json | 18 +++++ .../zoneminder/translations/it.json | 30 ++++++++ 68 files changed, 650 insertions(+), 47 deletions(-) create mode 100644 homeassistant/components/canary/translations/it.json create mode 100644 homeassistant/components/eafm/translations/ko.json create mode 100644 homeassistant/components/flo/translations/ko.json create mode 100644 homeassistant/components/goalzero/translations/ca.json create mode 100644 homeassistant/components/humidifier/translations/uk.json create mode 100644 homeassistant/components/kodi/translations/ko.json create mode 100644 homeassistant/components/omnilogic/translations/es.json create mode 100644 homeassistant/components/omnilogic/translations/it.json create mode 100644 homeassistant/components/omnilogic/translations/ko.json create mode 100644 homeassistant/components/rfxtrx/translations/ko.json create mode 100644 homeassistant/components/spider/translations/ko.json create mode 100644 homeassistant/components/tag/translations/ko.json create mode 100644 homeassistant/components/wilight/translations/ko.json create mode 100644 homeassistant/components/zodiac/translations/sensor.it.json create mode 100644 homeassistant/components/zodiac/translations/sensor.ko.json create mode 100644 homeassistant/components/zoneminder/translations/it.json diff --git a/homeassistant/components/almond/translations/ko.json b/homeassistant/components/almond/translations/ko.json index cb1f53882f8..08cb120bf9d 100644 --- a/homeassistant/components/almond/translations/ko.json +++ b/homeassistant/components/almond/translations/ko.json @@ -4,7 +4,7 @@ "already_setup": "\ud558\ub098\uc758 Almond \uacc4\uc815\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "cannot_connect": "Almond \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "missing_configuration": "Almond \uc124\uc815 \ubc29\ubc95\uc5d0 \ub300\ud55c \uc124\uba85\uc11c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694.", - "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6b0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" + "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/binary_sensor/translations/uk.json b/homeassistant/components/binary_sensor/translations/uk.json index 7b01acae4fb..29767f6d6d6 100644 --- a/homeassistant/components/binary_sensor/translations/uk.json +++ b/homeassistant/components/binary_sensor/translations/uk.json @@ -1,4 +1,18 @@ { + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} \u043d\u0438\u0437\u044c\u043a\u0438\u0439 \u0440\u0456\u0432\u0435\u043d\u044c \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430", + "is_not_bat_low": "{entity_name} \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0438\u043c \u0437\u0430\u0440\u044f\u0434 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430" + }, + "trigger_type": { + "bat_low": "{entity_name} \u043d\u0438\u0437\u044c\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430", + "not_bat_low": "{entity_name} \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0438\u0439 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440", + "not_opened": "{entity_name} \u0437\u0430\u043a\u0440\u0438\u0442\u043e", + "opened": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e", + "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e", + "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" + } + }, "state": { "_": { "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", diff --git a/homeassistant/components/blink/translations/ko.json b/homeassistant/components/blink/translations/ko.json index ef3ffc108e5..ac8c96e4f2d 100644 --- a/homeassistant/components/blink/translations/ko.json +++ b/homeassistant/components/blink/translations/ko.json @@ -4,6 +4,8 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "invalid_access_token": "\uc798\ubabb\ub41c \uc778\uc99d", "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" }, diff --git a/homeassistant/components/bond/translations/ko.json b/homeassistant/components/bond/translations/ko.json index d50380c81eb..61576d70431 100644 --- a/homeassistant/components/bond/translations/ko.json +++ b/homeassistant/components/bond/translations/ko.json @@ -5,7 +5,11 @@ "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" }, + "flow_title": "\ubcf8\ub4dc : {bond_id} ( {host} )", "step": { + "confirm": { + "description": "{bond_id} \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + }, "user": { "data": { "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070", diff --git a/homeassistant/components/broadlink/translations/it.json b/homeassistant/components/broadlink/translations/it.json index 939925104fd..7f26e33e14f 100644 --- a/homeassistant/components/broadlink/translations/it.json +++ b/homeassistant/components/broadlink/translations/it.json @@ -5,6 +5,7 @@ "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", + "not_supported": "Dispositivo non supportato", "unknown": "Errore imprevisto" }, "error": { diff --git a/homeassistant/components/broadlink/translations/ko.json b/homeassistant/components/broadlink/translations/ko.json index a3121d0b7de..47ebf3db64a 100644 --- a/homeassistant/components/broadlink/translations/ko.json +++ b/homeassistant/components/broadlink/translations/ko.json @@ -1,7 +1,42 @@ { "config": { "abort": { - "not_supported": "\uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \uc7a5\uce58" + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "already_in_progress": "\uc774 \uae30\uae30\uc5d0 \ub300\ud574 \uc774\ubbf8 \uc9c4\ud589\uc911\uc778 \uad6c\uc131\uc774 \uc788\uc2b5\ub2c8\ub2e4.", + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "invalid_host": "\uc798\ubabb\ub41c \ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c", + "not_supported": "\uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \uc7a5\uce58", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "invalid_host": "\uc798\ubabb\ub41c \ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + }, + "flow_title": "{name} ({host} \uc758 {model})", + "step": { + "auth": { + "title": "\uc7a5\uce58\uc5d0 \uc778\uc99d" + }, + "finish": { + "title": "\uc7a5\uce58 \uc774\ub984\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624" + }, + "reset": { + "title": "\uc7a5\uce58 \uc7a0\uae08 \ud574\uc81c" + }, + "unlock": { + "data": { + "unlock": "\uc608" + }, + "description": "\uc7a5\uce58\uac00 \uc7a0\uaca8 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub85c \uc778\ud574 Home Assistant\uc5d0\uc11c \uc778\uc99d \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc7a0\uae08\uc744 \ud574\uc81c \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\uc7a5\uce58 \uc7a0\uae08 \ud574\uc81c (\uc635\uc158)" + }, + "user": { + "data": { + "timeout": "\uc81c\ud55c \uc2dc\uac04" + }, + "title": "\uc7a5\uce58\uc5d0 \uc5f0\uacb0" + } } } } \ No newline at end of file diff --git a/homeassistant/components/canary/translations/it.json b/homeassistant/components/canary/translations/it.json new file mode 100644 index 00000000000..b29a758acaa --- /dev/null +++ b/homeassistant/components/canary/translations/it.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", + "unknown": "Errore imprevisto" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "flow_title": "Canary: {name}", + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "title": "Connettiti a Canary" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Argomenti passati a ffmpeg per le fotocamere", + "timeout": "Richiesta Timeout (secondi)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/ko.json b/homeassistant/components/canary/translations/ko.json index 3a68ce2da6c..0b1d82bb20a 100644 --- a/homeassistant/components/canary/translations/ko.json +++ b/homeassistant/components/canary/translations/ko.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328" }, + "flow_title": "Canary: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/climate/translations/uk.json b/homeassistant/components/climate/translations/uk.json index 227e0e1f4ef..8d636c386e5 100644 --- a/homeassistant/components/climate/translations/uk.json +++ b/homeassistant/components/climate/translations/uk.json @@ -1,12 +1,27 @@ { + "device_automation": { + "action_type": { + "set_hvac_mode": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438 \u0440\u0435\u0436\u0438\u043c HVAC \u043d\u0430 {entity_name}", + "set_preset_mode": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438 \u043f\u043e\u043f\u0435\u0440\u0435\u0434\u043d\u044c\u043e \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043d\u0430 {entity_name}" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u0432 \u043f\u0435\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c HVAC", + "is_preset_mode": "{entity_name} \u043d\u0430\u0441\u0442\u0440\u043e\u0454\u043d\u043e \u043d\u0430 \u043f\u0435\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} \u0432\u0438\u043c\u0456\u0440\u044f\u043d\u0430 \u0432\u043e\u043b\u043e\u0433\u0456\u0441\u0442\u044c \u0437\u043c\u0456\u043d\u0435\u043d\u0430", + "current_temperature_changed": "{entity_name} \u0432\u0438\u043c\u0456\u0440\u044f\u043d\u0443 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0443 \u0437\u043c\u0456\u043d\u0435\u043d\u043e", + "hvac_mode_changed": "{entity_name} \u0420\u0435\u0436\u0438\u043c HVAC \u0437\u043c\u0456\u043d\u0435\u043d\u043e" + } + }, "state": { "_": { "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0438\u0439", "cool": "\u041e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f", "dry": "\u041e\u0441\u0443\u0448\u0435\u043d\u043d\u044f", "fan_only": "\u041b\u0438\u0448\u0435 \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0442\u043e\u0440", - "heat": "\u041e\u0431\u0456\u0433\u0440\u0456\u0432\u0430\u043d\u043d\u044f", - "heat_cool": "\u041e\u043f\u0430\u043b\u0435\u043d\u043d\u044f/\u041e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "heat": "\u041d\u0430\u0433\u0440\u0456\u0432\u0430\u043d\u043d\u044f", + "heat_cool": "\u041d\u0430\u0433\u0440\u0456\u0432\u0430\u043d\u043d\u044f/\u041e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f", "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e" } }, diff --git a/homeassistant/components/cover/translations/uk.json b/homeassistant/components/cover/translations/uk.json index 0485a9bb371..66cd0c77c73 100644 --- a/homeassistant/components/cover/translations/uk.json +++ b/homeassistant/components/cover/translations/uk.json @@ -2,6 +2,9 @@ "device_automation": { "action_type": { "stop": "\u0417\u0443\u043f\u0438\u043d\u0438\u0442\u0438 {entity_name}" + }, + "trigger_type": { + "opened": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e" } }, "state": { diff --git a/homeassistant/components/eafm/translations/ko.json b/homeassistant/components/eafm/translations/ko.json new file mode 100644 index 00000000000..4e7bfc9dc93 --- /dev/null +++ b/homeassistant/components/eafm/translations/ko.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "no_stations": "\ud64d\uc218 \ubaa8\ub2c8\ud130\ub9c1 \uc2a4\ud14c\uc774\uc158\uc774 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "station": "\uc2a4\ud14c\uc774\uc158" + }, + "description": "\ubaa8\ub2c8\ud130\ub9c1\ud560 \uc2a4\ud14c\uc774\uc158 \uc120\ud0dd", + "title": "\ud64d\uc218 \ubaa8\ub2c8\ud130\ub9c1 \uc2a4\ud14c\uc774\uc158 \ucd94\uc801" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/translations/uk.json b/homeassistant/components/fan/translations/uk.json index 80b64c28c2f..3fd103cd244 100644 --- a/homeassistant/components/fan/translations/uk.json +++ b/homeassistant/components/fan/translations/uk.json @@ -1,4 +1,10 @@ { + "device_automation": { + "trigger_type": { + "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e", + "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" + } + }, "state": { "_": { "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", diff --git a/homeassistant/components/flo/translations/ko.json b/homeassistant/components/flo/translations/ko.json new file mode 100644 index 00000000000..7235d67c278 --- /dev/null +++ b/homeassistant/components/flo/translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8" + } + } + } + }, + "title": "flo" +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/it.json b/homeassistant/components/freebox/translations/it.json index 11d27eebd69..33f1ccbc1eb 100644 --- a/homeassistant/components/freebox/translations/it.json +++ b/homeassistant/components/freebox/translations/it.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Host gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { - "connection_failed": "Impossibile connettersi, si prega di riprovare", + "connection_failed": "Impossibile connettersi", "register_failed": "Errore in fase di registrazione, si prega di riprovare", - "unknown": "Errore sconosciuto: riprovare pi\u00f9 tardi" + "unknown": "Errore imprevisto" }, "step": { "link": { diff --git a/homeassistant/components/freebox/translations/zh-Hant.json b/homeassistant/components/freebox/translations/zh-Hant.json index be643ab9fd9..47ccc2e57b8 100644 --- a/homeassistant/components/freebox/translations/zh-Hant.json +++ b/homeassistant/components/freebox/translations/zh-Hant.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "connection_failed": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "connection_failed": "\u9023\u7dda\u5931\u6557", "register_failed": "\u8a3b\u518a\u5931\u6557\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66", - "unknown": "\u672a\u77e5\u932f\u8aa4\uff1a\u8acb\u7a0d\u5f8c\u518d\u8a66" + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { "link": { diff --git a/homeassistant/components/goalzero/translations/ca.json b/homeassistant/components/goalzero/translations/ca.json new file mode 100644 index 00000000000..56d181f5d98 --- /dev/null +++ b/homeassistant/components/goalzero/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_host": "Aquest no \u00e9s el Yeti que est\u00e0s buscant", + "unknown": "Error desconegut" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "name": "Name" + }, + "description": "En primer lloc, has de baixar-te l'aplicaci\u00f3 Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\nSegueix les instruccions per connectar el teu Yeti a la teva xarxa Wifi. A continuaci\u00f3, has d'obtenir la IP d'amfitri\u00f3 del teu encaminador (router). Cal que aquest tingui la configuraci\u00f3 DHCP activada per al teu dispositiu per aix\u00ed garantir que la IP no canvi\u00ef. Si cal, consulta el manual del teu encaminador.", + "title": "Goal Zero Yeti" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/en.json b/homeassistant/components/goalzero/translations/en.json index 412bef4c1d9..98cfa4f6f33 100644 --- a/homeassistant/components/goalzero/translations/en.json +++ b/homeassistant/components/goalzero/translations/en.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Already Configured" + "already_configured": "Account is already configured" }, "error": { - "cannot_connect": "Error connecting to host", - "invalid_host": "This is not a Yeti", + "cannot_connect": "Failed to connect", + "invalid_host": "This is not the Yeti you are looking for", "unknown": "Unknown Error" }, "step": { diff --git a/homeassistant/components/home_connect/translations/ko.json b/homeassistant/components/home_connect/translations/ko.json index 2b8e0c0af0f..8d1f5554e7f 100644 --- a/homeassistant/components/home_connect/translations/ko.json +++ b/homeassistant/components/home_connect/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "missing_configuration": "Home Connect \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_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6b0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" + "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" }, "create_entry": { "default": "Home Connect \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." diff --git a/homeassistant/components/humidifier/translations/uk.json b/homeassistant/components/humidifier/translations/uk.json new file mode 100644 index 00000000000..4081c4e13fc --- /dev/null +++ b/homeassistant/components/humidifier/translations/uk.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e", + "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/ko.json b/homeassistant/components/insteon/translations/ko.json index 37b62b95cdc..7c77bd49e27 100644 --- a/homeassistant/components/insteon/translations/ko.json +++ b/homeassistant/components/insteon/translations/ko.json @@ -1,9 +1,31 @@ { "config": { "abort": { + "already_configured": "Insteon \ubaa8\ub380 \uc5f0\uacb0\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub428. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." }, + "error": { + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "select_single": "\ud558\ub098\uc758 \uc635\uc158\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624." + }, "step": { + "hub1": { + "data": { + "host": "\ud5c8\ube0c IP \uc8fc\uc18c", + "port": "IP \ud3ec\ud2b8" + }, + "description": "Insteon Hub \ubc84\uc804 1 (2014 \ub144 \uc774\uc804)\uc744 \uad6c\uc131\ud569\ub2c8\ub2e4.", + "title": "Insteon Hub \ubc84\uc804 1" + }, + "hub2": { + "data": { + "host": "\ud5c8\ube0c IP \uc8fc\uc18c", + "port": "IP \ud3ec\ud2b8" + }, + "description": "Insteon Hub \ubc84\uc804 2\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.", + "title": "Insteon Hub \ubc84\uc804 2" + }, "hubv1": { "data": { "host": "IP \uc8fc\uc18c", @@ -22,6 +44,16 @@ "description": "Insteon Hub \ubc84\uc804 2\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.", "title": "Insteon Hub \ubc84\uc804 2" }, + "init": { + "data": { + "hubv1": "\ud5c8\ube0c \ubc84\uc804 1 (2014 \ub144 \uc774\uc804)" + } + }, + "plm": { + "data": { + "device": "USB \uc7a5\uce58 \uacbd\ub85c" + } + }, "user": { "data": { "modem_type": "\ubaa8\ub380 \uc720\ud615." @@ -30,5 +62,50 @@ "title": "Insteon" } } + }, + "options": { + "abort": { + "cannot_connect": "Insteon \ubaa8\ub380\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "select_single": "\uc635\uc158 \uc120\ud0dd" + }, + "step": { + "add_override": { + "data": { + "cat": "\uc7a5\uce58 \ubc94\uc8fc(\uc608: 0x10)" + } + }, + "add_x10": { + "data": { + "steps": "\ub514\uba38 \ub2e8\uacc4(\ub77c\uc774\ud2b8 \uc7a5\uce58\uc5d0\ub9cc, \uae30\ubcf8\uac12 22)", + "unitcode": "\ub2e8\uc704 \ucf54\ub4dc (1-16)" + }, + "description": "Insteon Hub \ube44\ubc00\ubc88\ud638\ub97c \ubcc0\uacbd\ud569\ub2c8\ub2e4." + }, + "init": { + "data": { + "add_override": "\uc7a5\uce58 Override \ucd94\uac00", + "add_x10": "X10 \uc7a5\uce58\ub97c \ucd94\uac00\ud569\ub2c8\ub2e4.", + "change_hub_config": "\ud5c8\ube0c \uad6c\uc131\uc744 \ubcc0\uacbd\ud569\ub2c8\ub2e4.", + "remove_override": "\uc7a5\uce58 Override \uc81c\uac70", + "remove_x10": "X10 \uc7a5\uce58\ub97c \uc81c\uac70\ud569\ub2c8\ub2e4." + }, + "description": "\uad6c\uc131 \ud560 \uc635\uc158\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624." + }, + "remove_override": { + "data": { + "address": "\uc81c\uac70 \ud560 \uc7a5\uce58 \uc8fc\uc18c\ub97c \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624." + }, + "description": "\uc7a5\uce58 Override \uc81c\uac70" + }, + "remove_x10": { + "data": { + "address": "\uc81c\uac70 \ud560 \uc7a5\uce58 \uc8fc\uc18c\ub97c \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624." + }, + "description": "X10 \uc7a5\uce58 \uc81c\uac70" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/iqvia/translations/ko.json b/homeassistant/components/iqvia/translations/ko.json index f3dd4f82b62..f6a914bd07d 100644 --- a/homeassistant/components/iqvia/translations/ko.json +++ b/homeassistant/components/iqvia/translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\uc774 \uc6b0\ud3b8 \ubc88\ud638\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, "error": { "invalid_zip_code": "\uc6b0\ud3b8\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/kodi/translations/ko.json b/homeassistant/components/kodi/translations/ko.json new file mode 100644 index 00000000000..6dc6b8bf87a --- /dev/null +++ b/homeassistant/components/kodi/translations/ko.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + }, + "step": { + "credentials": { + "description": "Kodi \uc0ac\uc6a9\uc790\uba85\uacfc \uc554\ud638\ub97c \uc785\ub825\ud558\uc2ed\uc2dc\uc624. \uc774\ub7ec\ud55c \ub0b4\uc6a9\uc740 \uc2dc\uc2a4\ud15c/\uc124\uc815/\ub124\ud2b8\uc6cc\ud06c/\uc11c\ube44\uc2a4\uc5d0\uc11c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "discovery_confirm": { + "description": "Kodi (` {name} `)\ub97c Home Assistant\uc5d0 \ucd94\uac00 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Kodi \ubc1c\uacac" + }, + "host": { + "data": { + "ssl": "SSL\uc744 \ud1b5\ud574 \uc5f0\uacb0" + }, + "description": "Kodi \uc5f0\uacb0 \uc815\ubcf4. \uc2dc\uc2a4\ud15c / \uc124\uc815 / \ub124\ud2b8\uc6cc\ud06c / \uc11c\ube44\uc2a4\uc5d0\uc11c \"HTTP\ub97c \ud1b5\ud55c Kodi \uc81c\uc5b4 \ud5c8\uc6a9\"\uc744 \ud65c\uc131\ud654\ud588\ub294\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624." + }, + "user": { + "description": "Kodi \uc5f0\uacb0 \uc815\ubcf4. \uc2dc\uc2a4\ud15c / \uc124\uc815 / \ub124\ud2b8\uc6cc\ud06c / \uc11c\ube44\uc2a4\uc5d0\uc11c \"HTTP\ub97c \ud1b5\ud55c Kodi \uc81c\uc5b4 \ud5c8\uc6a9\"\uc744 \ud65c\uc131\ud654\ud588\ub294\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624." + }, + "ws_port": { + "description": "WebSocket \ud3ec\ud2b8 (Kodi\uc5d0\uc11c TCP \ud3ec\ud2b8\ub77c\uace0\ub3c4 \ud568). WebSocket\uc744 \ud1b5\ud574 \uc5f0\uacb0\ud558\ub824\uba74 \uc2dc\uc2a4\ud15c / \uc124\uc815 / \ub124\ud2b8\uc6cc\ud06c / \uc11c\ube44\uc2a4\uc5d0\uc11c \"\ud504\ub85c\uadf8\ub7a8\uc774 Kodi\ub97c \uc81c\uc5b4\ud558\ub3c4\ub85d \ud5c8\uc6a9\"\uc744 \ud65c\uc131\ud654\ud574\uc57c\ud569\ub2c8\ub2e4. WebSocket\uc774 \ud65c\uc131\ud654\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \ud3ec\ud2b8\ub97c \uc81c\uac70\ud558\uace0 \ube44\uc6cc \ub461\ub2c8\ub2e4." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/translations/uk.json b/homeassistant/components/light/translations/uk.json index 06c880fff77..67685889c54 100644 --- a/homeassistant/components/light/translations/uk.json +++ b/homeassistant/components/light/translations/uk.json @@ -1,4 +1,10 @@ { + "device_automation": { + "trigger_type": { + "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e", + "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" + } + }, "state": { "_": { "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", diff --git a/homeassistant/components/meteo_france/translations/ko.json b/homeassistant/components/meteo_france/translations/ko.json index 166ddaa68ab..4b8dc3204dd 100644 --- a/homeassistant/components/meteo_france/translations/ko.json +++ b/homeassistant/components/meteo_france/translations/ko.json @@ -4,7 +4,13 @@ "already_configured": "\ub3c4\uc2dc\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694" }, + "error": { + "empty": "\ub3c4\uc2dc \uac80\uc0c9 \uacb0\uacfc \uc5c6\uc74c: \ub3c4\uc2dc \ud544\ub4dc\ub97c \ud655\uc778\ud558\uc2ed\uc2dc\uc624." + }, "step": { + "cities": { + "title": "\ud504\ub791\uc2a4 \uae30\uc0c1\uccad (M\u00e9t\u00e9o-France)" + }, "user": { "data": { "city": "\ub3c4\uc2dc" @@ -13,5 +19,14 @@ "title": "\ud504\ub791\uc2a4 \uae30\uc0c1\uccad (M\u00e9t\u00e9o-France)" } } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "\uc608\uce21 \ubaa8\ub4dc" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/it.json b/homeassistant/components/monoprice/translations/it.json index b89758a9da3..d084929e320 100644 --- a/homeassistant/components/monoprice/translations/it.json +++ b/homeassistant/components/monoprice/translations/it.json @@ -4,7 +4,7 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { - "cannot_connect": "Impossibile connettersi, si prega di riprovare", + "cannot_connect": "Impossibile connettersi", "unknown": "Errore imprevisto" }, "step": { diff --git a/homeassistant/components/monoprice/translations/zh-Hant.json b/homeassistant/components/monoprice/translations/zh-Hant.json index ca1923d62d6..e653bda9205 100644 --- a/homeassistant/components/monoprice/translations/zh-Hant.json +++ b/homeassistant/components/monoprice/translations/zh-Hant.json @@ -4,7 +4,7 @@ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "cannot_connect": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { diff --git a/homeassistant/components/netatmo/translations/ko.json b/homeassistant/components/netatmo/translations/ko.json index 1793e0a765a..8165941f0d8 100644 --- a/homeassistant/components/netatmo/translations/ko.json +++ b/homeassistant/components/netatmo/translations/ko.json @@ -3,7 +3,7 @@ "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.", - "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6b0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" + "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" }, "create_entry": { "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/nightscout/translations/ca.json b/homeassistant/components/nightscout/translations/ca.json index eb06d94de3b..21a472680b4 100644 --- a/homeassistant/components/nightscout/translations/ca.json +++ b/homeassistant/components/nightscout/translations/ca.json @@ -5,14 +5,18 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, "flow_title": "Nightscout", "step": { "user": { "data": { + "api_key": "Clau API", "url": "URL" - } + }, + "description": "- URL: l'adre\u00e7a de la teva inst\u00e0ncia de Nightscout. Per exemple: https://myhomeassistant.duckdns.org:5423 \n- Clau API (opcional): utilitza-la nom\u00e9s si la teva inst\u00e0ncia est\u00e0 protegida (auth_default_roles != readable).", + "title": "Introdueix la informaci\u00f3 del teu servidor Nightscout." } } } diff --git a/homeassistant/components/nightscout/translations/en.json b/homeassistant/components/nightscout/translations/en.json index ffe5cce81a6..d8b4c441283 100644 --- a/homeassistant/components/nightscout/translations/en.json +++ b/homeassistant/components/nightscout/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" }, "flow_title": "Nightscout", "step": { "user": { "data": { - "api_key": "[%key:common::config_flow::data::api_key%]", - "url": "[%key:common::config_flow::data::url%]" + "api_key": "API Key", + "url": "URL" }, "description": "- URL: the address of your nightscout instance. I.e.: https://myhomeassistant.duckdns.org:5423\n- API Key (Optional): Only use if your instance is protected (auth_default_roles != readable).", "title": "Enter your Nightscout server information." diff --git a/homeassistant/components/nightscout/translations/ko.json b/homeassistant/components/nightscout/translations/ko.json index 17dee71d640..0235c446e75 100644 --- a/homeassistant/components/nightscout/translations/ko.json +++ b/homeassistant/components/nightscout/translations/ko.json @@ -2,6 +2,9 @@ "config": { "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" } } } \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/ru.json b/homeassistant/components/nightscout/translations/ru.json index cf904f0134c..738c4dfa9a3 100644 --- a/homeassistant/components/nightscout/translations/ru.json +++ b/homeassistant/components/nightscout/translations/ru.json @@ -5,14 +5,18 @@ }, "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": "Nightscout", "step": { "user": { "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", "url": "URL-\u0430\u0434\u0440\u0435\u0441" - } + }, + "description": "- URL: \u0430\u0434\u0440\u0435\u0441 \u0412\u0430\u0448\u0435\u0433\u043e Nightscout. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: https://myhomeassistant.duckdns.org:5423\n- \u041a\u043b\u044e\u0447 API (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e): \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435, \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 \u0412\u0430\u0448 Nightcout \u0437\u0430\u0449\u0438\u0449\u0435\u043d (auth_default_roles != readable).", + "title": "Nightscout" } } } diff --git a/homeassistant/components/nightscout/translations/zh-Hant.json b/homeassistant/components/nightscout/translations/zh-Hant.json index fa9f3d12427..5066f5a2edb 100644 --- a/homeassistant/components/nightscout/translations/zh-Hant.json +++ b/homeassistant/components/nightscout/translations/zh-Hant.json @@ -5,14 +5,18 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "flow_title": "Nightscout", "step": { "user": { "data": { + "api_key": "API \u5bc6\u9470", "url": "\u7db2\u5740" - } + }, + "description": "- URL\uff1aNightscout \u8a2d\u5099\u4f4d\u5740\u3002\u4f8b\u5982\uff1ahttps://myhomeassistant.duckdns.org:5423\n- API \u5bc6\u9470\uff08\u9078\u9805\uff09\uff1a\u50c5\u65bc\u8a2d\u5099\u70ba\u4fdd\u8b77\u72c0\u614b\uff08(auth_default_roles != readable\uff09\u4e0b\u4f7f\u7528\u3002", + "title": "\u8f38\u5165 Nightscout \u4f3a\u670d\u5668\u8cc7\u8a0a\u3002" } } } diff --git a/homeassistant/components/omnilogic/translations/es.json b/homeassistant/components/omnilogic/translations/es.json new file mode 100644 index 00000000000..849cd73b40f --- /dev/null +++ b/homeassistant/components/omnilogic/translations/es.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." + }, + "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" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "polling_interval": "Intervalo de sondeo (en segundos)" + } + } + } + }, + "title": "Omnilogic" +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/it.json b/homeassistant/components/omnilogic/translations/it.json new file mode 100644 index 00000000000..38ace995177 --- /dev/null +++ b/homeassistant/components/omnilogic/translations/it.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "polling_interval": "Intervallo di scansione (in secondi)" + } + } + } + }, + "title": "Omnilogic" +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/ko.json b/homeassistant/components/omnilogic/translations/ko.json new file mode 100644 index 00000000000..686ca520bff --- /dev/null +++ b/homeassistant/components/omnilogic/translations/ko.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uc124\uc815\ub418\uc5b4 \uc788\uc74c. \ud558\ub098\uc758 \uc124\uc815\ub9cc \uac00\ub2a5\ud568." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + }, + "step": { + "user": { + "data": { + "password": "\uc554\ud638", + "username": "\uc0ac\uc6a9\uc790\uba85" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "polling_interval": "\ud3f4\ub9c1 \uac04\uaca9(\ucd08)" + } + } + } + }, + "title": "Omnilogic" +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/ko.json b/homeassistant/components/ovo_energy/translations/ko.json index 09d002bc161..7d6882c2bd3 100644 --- a/homeassistant/components/ovo_energy/translations/ko.json +++ b/homeassistant/components/ovo_energy/translations/ko.json @@ -1,7 +1,9 @@ { "config": { "error": { - "connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + "already_configured": "\uacc4\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "authorization_error": "\uc778\uc99d \uc624\ub958\uc785\ub2c8\ub2e4. \uc790\uaca9 \uc99d\uba85\uc744 \ud655\uc778\ud558\uc2ed\uc2dc\uc624.", + "connection_error": "\uc5f0\uacb0 \uc2e4\ud328" }, "step": { "user": { diff --git a/homeassistant/components/plugwise/translations/it.json b/homeassistant/components/plugwise/translations/it.json index b1b3365125a..0a215c39f65 100644 --- a/homeassistant/components/plugwise/translations/it.json +++ b/homeassistant/components/plugwise/translations/it.json @@ -13,9 +13,10 @@ "user": { "data": { "host": "Indirizzo IP Smile", - "password": "ID Smile" + "password": "ID Smile", + "port": "Numero porta Smile" }, - "description": "Dettagli", + "description": "Si prega di inserire:", "title": "Connettersi al dispositivo" } } diff --git a/homeassistant/components/plugwise/translations/ko.json b/homeassistant/components/plugwise/translations/ko.json index 089e2daea82..04fcb738285 100644 --- a/homeassistant/components/plugwise/translations/ko.json +++ b/homeassistant/components/plugwise/translations/ko.json @@ -13,7 +13,8 @@ "user": { "data": { "host": "Smile IP \uc8fc\uc18c", - "password": "Smile ID" + "password": "Smile ID", + "port": "\uc2a4\ub9c8\uc77c \ud3ec\ud2b8 \ubc88\ud638" }, "description": "\uc138\ubd80 \uc815\ubcf4", "title": "Smile \uc5d0 \uc5f0\uacb0\ud558\uae30" diff --git a/homeassistant/components/remote/translations/uk.json b/homeassistant/components/remote/translations/uk.json index bc52ed67ae5..2feda4928e5 100644 --- a/homeassistant/components/remote/translations/uk.json +++ b/homeassistant/components/remote/translations/uk.json @@ -1,4 +1,10 @@ { + "device_automation": { + "trigger_type": { + "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e", + "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u043e" + } + }, "state": { "_": { "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", diff --git a/homeassistant/components/rfxtrx/translations/ko.json b/homeassistant/components/rfxtrx/translations/ko.json new file mode 100644 index 00000000000..aa8512da285 --- /dev/null +++ b/homeassistant/components/rfxtrx/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/risco/translations/ko.json b/homeassistant/components/risco/translations/ko.json index 1268388acce..37d9a61307b 100644 --- a/homeassistant/components/risco/translations/ko.json +++ b/homeassistant/components/risco/translations/ko.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + } + }, "options": { "step": { "ha_to_risco": { @@ -11,6 +21,11 @@ "description": "Home Assistant \uc54c\ub78c\uc744 \ud65c\uc131\ud654 \ud560 \ub54c Risco \uc54c\ub78c\uc758 \uc0c1\ud0dc\ub97c \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624.", "title": "Home Assistant \uc0c1\ud0dc\ub97c Risco \uc0c1\ud0dc\ub85c \ub9e4\ud551" }, + "init": { + "data": { + "scan_interval": "Risco\ub97c \ud3f4\ub9c1\ud558\ub294 \ube48\ub3c4 (\ucd08)" + } + }, "risco_to_ha": { "data": { "A": "\uadf8\ub8f9 A", diff --git a/homeassistant/components/roon/translations/ko.json b/homeassistant/components/roon/translations/ko.json index 50c22e9e256..cdffd6e88ae 100644 --- a/homeassistant/components/roon/translations/ko.json +++ b/homeassistant/components/roon/translations/ko.json @@ -4,8 +4,22 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { + "duplicate_entry": "\ud574\ub2f9 \ud638\uc2a4\ud2b8\ub294 \uc774\ubbf8 \ucd94\uac00\ub418\uc5c8\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": { + "link": { + "description": "Roon\uc5d0\uc11c \ud648 \uc5b4\uc2dc\uc2a4\ud134\ud2b8\ub97c \uc778\uc99d\ud574\uc57c\ud569\ub2c8\ub2e4. \uc81c\ucd9c\uc744 \ud074\ub9ad \ud55c \ud6c4 Roon Core \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \uc124\uc815\uc744 \uc5f4\uace0 \ud655\uc7a5 \ud0ed\uc5d0\uc11c HomeAssistant\ub97c \ud65c\uc131\ud654\ud569\ub2c8\ub2e4.", + "title": "Roon\uc5d0\uc11c HomeAssistant \uc778\uc99d" + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8" + }, + "description": "Roon \uc11c\ubc84 Hostname \ub610\ub294 IP\ub97c \uc785\ub825\ud558\uc2ed\uc2dc\uc624.", + "title": "Roon \uc11c\ubc84 \uad6c\uc131" + } } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/uk.json b/homeassistant/components/sensor/translations/uk.json index 56e587bb44c..391415409f5 100644 --- a/homeassistant/components/sensor/translations/uk.json +++ b/homeassistant/components/sensor/translations/uk.json @@ -1,4 +1,9 @@ { + "device_automation": { + "condition_type": { + "is_battery_level": "\u041f\u043e\u0442\u043e\u0447\u043d\u0438\u0439 \u0440\u0456\u0432\u0435\u043d\u044c \u0437\u0430\u0440\u044f\u0434\u0443 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430 {entity_name}" + } + }, "state": { "_": { "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", diff --git a/homeassistant/components/sentry/translations/ko.json b/homeassistant/components/sentry/translations/ko.json index 7e60891e166..f3adbefa3d7 100644 --- a/homeassistant/components/sentry/translations/ko.json +++ b/homeassistant/components/sentry/translations/ko.json @@ -13,5 +13,21 @@ "title": "Sentry" } } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "\ud658\uacbd\uc758 \uc120\ud0dd\uc801 \uba85\uce6d", + "event_custom_components": "\uc0ac\uc6a9\uc790 \uc9c0\uc815 \uad6c\uc131 \uc694\uc18c\uc5d0\uc11c \uc774\ubca4\ud2b8 \ubcf4\ub0b4\uae30", + "event_handled": "\ucc98\ub9ac\ub41c \uc774\ubca4\ud2b8 \ubcf4\ub0b4\uae30", + "event_third_party_packages": "\uc368\ub4dc\ud30c\ud2f0 \ud328\ud0a4\uc9c0\uc5d0\uc11c \uc774\ubca4\ud2b8 \ubcf4\ub0b4\uae30", + "logging_event_level": "Log level Sentry\ub294 \ub2e4\uc74c\uc5d0 \ub300\ud55c \uc774\ubca4\ud2b8\ub97c \ub4f1\ub85d\ud569\ub2c8\ub2e4.", + "logging_level": "Log level sentry\ub294 \ub2e4\uc74c\uc5d0 \ub300\ud55c \ub85c\uadf8\ub97c \ube0c\ub808\ub4dc \ud06c\ub7fc\uc73c\ub85c \uae30\ub85d\ud569\ub2c8\ub2e4.", + "tracing": "\uc131\ub2a5 \ucd94\uc801 \ud65c\uc131\ud654", + "tracing_sample_rate": "\uc0d8\ud50c\ub9c1 \uc18d\ub3c4 \ucd94\uc801; 0.0\uc5d0\uc11c 1.0 \uc0ac\uc774 (1.0 = 100 %)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index bdc05b734ba..5d662a1eb6d 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/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", + "unsupported_firmware": "El dispositivo est\u00e1 usando una versi\u00f3n de firmware no compatible." }, "error": { "auth_not_supported": "Los dispositivos Shelly que requieren autenticaci\u00f3n no son compatibles actualmente.", diff --git a/homeassistant/components/shelly/translations/it.json b/homeassistant/components/shelly/translations/it.json index 595a57b0a00..b1584c675b3 100644 --- a/homeassistant/components/shelly/translations/it.json +++ b/homeassistant/components/shelly/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", + "unsupported_firmware": "Il dispositivo utilizza una versione del firmware non supportata." }, "error": { "auth_not_supported": "I dispositivi Shelly che richiedono l'autenticazione non sono attualmente supportati.", diff --git a/homeassistant/components/shelly/translations/ko.json b/homeassistant/components/shelly/translations/ko.json index 3bf8db7d50e..5fb84e0ac90 100644 --- a/homeassistant/components/shelly/translations/ko.json +++ b/homeassistant/components/shelly/translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "unsupported_firmware": "\uc774 \uc7a5\uce58\ub294 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \ud38c\uc6e8\uc5b4 \ubc84\uc804\uc744 \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4." + }, "error": { "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d" }, diff --git a/homeassistant/components/shelly/translations/ru.json b/homeassistant/components/shelly/translations/ru.json index 570e6f8d7c7..90f68d0a65f 100644 --- a/homeassistant/components/shelly/translations/ru.json +++ b/homeassistant/components/shelly/translations/ru.json @@ -1,7 +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." + "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.", + "unsupported_firmware": "\u0412 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0438." }, "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.", diff --git a/homeassistant/components/shelly/translations/zh-Hant.json b/homeassistant/components/shelly/translations/zh-Hant.json index e8fe857c476..4c57a3df755 100644 --- a/homeassistant/components/shelly/translations/zh-Hant.json +++ b/homeassistant/components/shelly/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", + "unsupported_firmware": "\u8a2d\u5099\u4f7f\u7528\u7684\u97cc\u9ad4\u4e0d\u652f\u63f4\u3002" }, "error": { "auth_not_supported": "\u76ee\u524d\u4e0d\u652f\u63f4 Shelly \u8a2d\u5099\u6240\u9700\u8a8d\u8b49\u3002", diff --git a/homeassistant/components/smappee/translations/ca.json b/homeassistant/components/smappee/translations/ca.json index df15bdf3ed4..ee905f0ca84 100644 --- a/homeassistant/components/smappee/translations/ca.json +++ b/homeassistant/components/smappee/translations/ca.json @@ -24,7 +24,7 @@ "description": "Introdueix l'amfitri\u00f3 per iniciar la integraci\u00f3 local de Smappee" }, "pick_implementation": { - "title": "Selecciona un m\u00e8tode d'autenticaci\u00f3" + "title": "Selecci\u00f3 del m\u00e8tode d'autenticaci\u00f3" }, "zeroconf_confirm": { "description": "Vols afegir el dispositiu Smappee amb n\u00famero de s\u00e8rie `{serialnumber}` a Home Assistant?", diff --git a/homeassistant/components/smappee/translations/ko.json b/homeassistant/components/smappee/translations/ko.json index 129cc0814bf..b3e37ee6d01 100644 --- a/homeassistant/components/smappee/translations/ko.json +++ b/homeassistant/components/smappee/translations/ko.json @@ -1,11 +1,17 @@ { "config": { "abort": { + "already_configured_device": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "missing_configuration": "\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_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6b0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" + "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" }, "step": { + "local": { + "data": { + "host": "\ud638\uc2a4\ud2b8" + } + }, "pick_implementation": { "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" } diff --git a/homeassistant/components/somfy/translations/es.json b/homeassistant/components/somfy/translations/es.json index 992752d8420..6d11afcba47 100644 --- a/homeassistant/components/somfy/translations/es.json +++ b/homeassistant/components/somfy/translations/es.json @@ -4,7 +4,8 @@ "already_setup": "Solo puedes configurar una cuenta de Somfy.", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n", "missing_configuration": "El componente Somfy no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." }, "create_entry": { "default": "Autenticado correctamente con Somfy." diff --git a/homeassistant/components/somfy/translations/it.json b/homeassistant/components/somfy/translations/it.json index 001739a7a99..ad479629498 100644 --- a/homeassistant/components/somfy/translations/it.json +++ b/homeassistant/components/somfy/translations/it.json @@ -4,10 +4,11 @@ "already_setup": "\u00c8 possibile configurare un solo account Somfy.", "authorize_url_timeout": "Tempo scaduto nel generare l'url di autorizzazione", "missing_configuration": "Il componente Somfy non \u00e8 configurato. Si prega di seguire la documentazione.", - "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})" + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "create_entry": { - "default": "Autenticato con successo con Somfy." + "default": "Autenticazione riuscita" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/somfy/translations/ko.json b/homeassistant/components/somfy/translations/ko.json index 157640c1fa7..43d4ba146b3 100644 --- a/homeassistant/components/somfy/translations/ko.json +++ b/homeassistant/components/somfy/translations/ko.json @@ -4,7 +4,8 @@ "already_setup": "\ud558\ub098\uc758 Somfy \uacc4\uc815\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "missing_configuration": "Somfy \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_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6b0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" + "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})", + "single_instance_allowed": "\uc774\ubbf8 \uc124\uc815\ub418\uc5b4 \uc788\uc74c. \ud558\ub098\uc758 \uc124\uc815\ub9cc \uac00\ub2a5\ud568." }, "create_entry": { "default": "Somfy \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." diff --git a/homeassistant/components/somfy/translations/zh-Hant.json b/homeassistant/components/somfy/translations/zh-Hant.json index c0f1aefd157..5e41fbbfe2e 100644 --- a/homeassistant/components/somfy/translations/zh-Hant.json +++ b/homeassistant/components/somfy/translations/zh-Hant.json @@ -4,10 +4,11 @@ "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Somfy \u5e33\u865f\u3002", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", "missing_configuration": "Somfy \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", - "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})" + "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Somfy \u8a2d\u5099\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/spider/translations/ko.json b/homeassistant/components/spider/translations/ko.json new file mode 100644 index 00000000000..1f08b96ee10 --- /dev/null +++ b/homeassistant/components/spider/translations/ko.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "invalid_auth": "\uc798\ubabb\ub41c \uc778\uc99d", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/ko.json b/homeassistant/components/spotify/translations/ko.json index 2b9ebfe8bf6..37dccd8c1a6 100644 --- a/homeassistant/components/spotify/translations/ko.json +++ b/homeassistant/components/spotify/translations/ko.json @@ -4,7 +4,8 @@ "already_setup": "\ud558\ub098\uc758 Spotify \uacc4\uc815\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "missing_configuration": "Spotify \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_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6b0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" + "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})", + "reauth_account_mismatch": "\uc778\uc99d\ub41c Spotify \uacc4\uc815\uc740 \uc7ac\uc778\uc99d\uc774 \ud544\uc694\ud55c \uacc4\uc815\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." }, "create_entry": { "default": "Spotify \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." diff --git a/homeassistant/components/switch/translations/uk.json b/homeassistant/components/switch/translations/uk.json index 7ac96bd7039..bee9eb957d5 100644 --- a/homeassistant/components/switch/translations/uk.json +++ b/homeassistant/components/switch/translations/uk.json @@ -1,4 +1,10 @@ { + "device_automation": { + "trigger_type": { + "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e", + "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" + } + }, "state": { "_": { "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", diff --git a/homeassistant/components/tag/translations/ko.json b/homeassistant/components/tag/translations/ko.json new file mode 100644 index 00000000000..8cee64dc465 --- /dev/null +++ b/homeassistant/components/tag/translations/ko.json @@ -0,0 +1,3 @@ +{ + "title": "\ud0dc\uadf8" +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/ko.json b/homeassistant/components/toon/translations/ko.json index e0903a8087c..379058f68d1 100644 --- a/homeassistant/components/toon/translations/ko.json +++ b/homeassistant/components/toon/translations/ko.json @@ -6,7 +6,7 @@ "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.", "no_agreements": "\uc774 \uacc4\uc815\uc5d0\ub294 Toon \ub514\uc2a4\ud50c\ub808\uc774\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.", - "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6b0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" + "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" }, "step": { "agreement": { diff --git a/homeassistant/components/unifi/translations/it.json b/homeassistant/components/unifi/translations/it.json index 8a06ca440c5..43fd5b31c23 100644 --- a/homeassistant/components/unifi/translations/it.json +++ b/homeassistant/components/unifi/translations/it.json @@ -60,7 +60,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Sensori di utilizzo della larghezza di banda per i client di rete" + "allow_bandwidth_sensors": "Sensori di utilizzo della larghezza di banda per i client di rete", + "allow_uptime_sensors": "Sensori di tempo di funzionamento per i client di rete" }, "description": "Configurare i sensori delle statistiche", "title": "Opzioni UniFi 3/3" diff --git a/homeassistant/components/vizio/translations/ko.json b/homeassistant/components/vizio/translations/ko.json index c56171e9319..310fd765026 100644 --- a/homeassistant/components/vizio/translations/ko.json +++ b/homeassistant/components/vizio/translations/ko.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "complete_pairing_failed": "\ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc81c\ucd9c\ud558\uae30 \uc804\uc5d0 \uc785\ub825\ud55c PIN \uc774 \uc62c\ubc14\ub978\uc9c0, TV \uc758 \uc804\uc6d0\uc774 \ucf1c\uc838 \uc788\uace0 \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "existing_config_entry_found": "\uc77c\ub828 \ubc88\ud638\uac00 \ub3d9\uc77c\ud55c \uae30\uc874 VIZIO SmartCast \uae30\uae30 \uad6c\uc131 \ud56d\ubaa9\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc774 \ud56d\ubaa9\uc744 \uad6c\uc131\ud558\ub824\uba74 \uae30\uc874 \ud56d\ubaa9\uc744 \uc0ad\uc81c\ud574\uc57c\ud569\ub2c8\ub2e4.", "host_exists": "\uc124\uc815\ub41c \ud638\uc2a4\ud2b8\uc758 VIZIO SmartCast \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "name_exists": "\uc124\uc815\ub41c \uc774\ub984\uc758 VIZIO SmartCast \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, diff --git a/homeassistant/components/wilight/translations/ko.json b/homeassistant/components/wilight/translations/ko.json new file mode 100644 index 00000000000..677b104c065 --- /dev/null +++ b/homeassistant/components/wilight/translations/ko.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "not_wilight_device": "\uc774 \uc7a5\uce58\ub294 WiLight\uac00 \uc544\ub2d9\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "WiLight {name} \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? \n\n \uc9c0\uc6d0 : {components}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/ko.json b/homeassistant/components/withings/translations/ko.json index 2104b2a9570..a01672f2227 100644 --- a/homeassistant/components/withings/translations/ko.json +++ b/homeassistant/components/withings/translations/ko.json @@ -4,7 +4,7 @@ "already_configured": "\ud504\ub85c\ud544\uc5d0 \ub300\ud55c \uad6c\uc131\uc774 \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "missing_configuration": "Withings \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", - "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6b0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" + "no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})" }, "create_entry": { "default": "Withings \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." diff --git a/homeassistant/components/xiaomi_aqara/translations/ko.json b/homeassistant/components/xiaomi_aqara/translations/ko.json index 90a22ace2b8..f222ac58bab 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ko.json +++ b/homeassistant/components/xiaomi_aqara/translations/ko.json @@ -30,7 +30,9 @@ }, "user": { "data": { - "interface": "\uc0ac\uc6a9\ud560 \ub124\ud2b8\uc6cc\ud06c \uc778\ud130\ud398\uc774\uc2a4" + "host": "IP \uc8fc\uc18c (\uc120\ud0dd \uc0ac\ud56d)", + "interface": "\uc0ac\uc6a9\ud560 \ub124\ud2b8\uc6cc\ud06c \uc778\ud130\ud398\uc774\uc2a4", + "mac": "Mac \uc8fc\uc18c(\uc120\ud0dd \uc0ac\ud56d)" }, "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/zodiac/translations/sensor.it.json b/homeassistant/components/zodiac/translations/sensor.it.json new file mode 100644 index 00000000000..f814476b9cd --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.it.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Acquario", + "aries": "Ariete", + "cancer": "Cancro", + "capricorn": "Capricorno", + "gemini": "Gemelli", + "leo": "Leone", + "libra": "Bilancia", + "pisces": "Pesci", + "sagittarius": "Sagittario", + "scorpio": "Scorpione", + "taurus": "Toro", + "virgo": "Vergine" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/sensor.ko.json b/homeassistant/components/zodiac/translations/sensor.ko.json new file mode 100644 index 00000000000..0a9fc83cdea --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.ko.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "\ubb3c\ubcd1 \uc790\ub9ac", + "aries": "\uc591 \uc790\ub9ac", + "cancer": "\uac8c \uc790\ub9ac", + "capricorn": "\uc5fc\uc18c \uc790\ub9ac", + "gemini": "\uc30d\ub465\uc774 \uc790\ub9ac", + "leo": "\uc0ac\uc790 \uc790\ub9ac", + "libra": "\ucc9c\uce6d \uc790\ub9ac", + "pisces": "\ubb3c\uace0\uae30 \uc790\ub9ac", + "sagittarius": "\uad81\uc218 \uc790\ub9ac", + "scorpio": "\uc804\uac08 \uc790\ub9ac", + "taurus": "\ud669\uc18c \uc790\ub9ac", + "virgo": "\ucc98\ub140 \uc790\ub9ac" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/it.json b/homeassistant/components/zoneminder/translations/it.json new file mode 100644 index 00000000000..b4f6b8da9ee --- /dev/null +++ b/homeassistant/components/zoneminder/translations/it.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "auth_fail": "Nome utente o password non corretti.", + "connection_error": "Impossibile connettersi a un server ZoneMinder." + }, + "create_entry": { + "default": "Server ZoneMinder aggiunto." + }, + "error": { + "auth_fail": "Nome utente o password non corretti.", + "connection_error": "Impossibile connettersi a un server ZoneMinder." + }, + "flow_title": "ZoneMinder", + "step": { + "user": { + "data": { + "host": "Host e porta (ad es. 10.10.0.4:8010)", + "password": "Password", + "path": "Percorso ZM", + "path_zms": "Percorso ZMS", + "ssl": "Usa SSL per le connessioni a ZoneMinder", + "username": "Nome utente", + "verify_ssl": "Verifica del certificato SSL" + }, + "title": "Aggiungi Server ZoneMinder." + } + } + } +} \ No newline at end of file From ea6ec42bbdf8cc5058cc2cb27093c627a2cdcf07 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 28 Sep 2020 02:16:29 +0200 Subject: [PATCH 399/514] Add onewire humidity sensors connected via DS2438 (#39780) --- homeassistant/components/onewire/sensor.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 1d0d3c2ebba..23082f2af7f 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -37,6 +37,10 @@ DEVICE_SENSORS = { "26": { "temperature": "temperature", "humidity": "humidity", + "humidity_hih3600": "HIH3600/humidity", + "humidity_hih4000": "HIH4000/humidity", + "humidity_hih5030": "HIH5030/humidity", + "humidity_htm1735": "HTM1735/humidity", "pressure": "B1-R1-A/pressure", "illuminance": "S3-R1-A/illuminance", "voltage_VAD": "VAD", @@ -72,6 +76,10 @@ SENSOR_TYPES = { # SensorType: [ Measured unit, Unit ] "temperature": ["temperature", TEMP_CELSIUS], "humidity": ["humidity", PERCENTAGE], + "humidity_hih3600": ["humidity", PERCENTAGE], + "humidity_hih4000": ["humidity", PERCENTAGE], + "humidity_hih5030": ["humidity", PERCENTAGE], + "humidity_htm1735": ["humidity", PERCENTAGE], "humidity_raw": ["humidity", PERCENTAGE], "pressure": ["pressure", "mb"], "illuminance": ["illuminance", LIGHT_LUX], From 0ca19133d9643f28b9c16862ae9c452ba310d5a5 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 28 Sep 2020 03:00:44 +0200 Subject: [PATCH 400/514] Add tests for modbus binary sensor (#40367) * Add test coverage of binary sensor. Update conftest to be generic. * Pass total config structure to run_base_test. Simple devices like sensor/switch do only have one entry in the dict array, whereas e.g. switch have multiple entries. * Use STATE_ON / _OFF for binary_sensor test. * Update coveragerc Only exclude files that uses a third party library. * Remove modbus/* from coveragerc * Remove modbus from .coveragerc * Update .coveragerc Co-authored-by: Chris Talkington --- .coveragerc | 4 +- tests/components/modbus/conftest.py | 39 ++++---- .../modbus/test_modbus_binary_sensor.py | 98 +++++++++++++++++++ tests/components/modbus/test_modbus_sensor.py | 91 +++++++++-------- 4 files changed, 174 insertions(+), 58 deletions(-) create mode 100644 tests/components/modbus/test_modbus_binary_sensor.py diff --git a/.coveragerc b/.coveragerc index c25b0f45c7e..eb137602a75 100644 --- a/.coveragerc +++ b/.coveragerc @@ -538,7 +538,9 @@ omit = homeassistant/components/mjpeg/camera.py homeassistant/components/mobile_app/* homeassistant/components/mochad/* - homeassistant/components/modbus/* + homeassistant/components/modbus/climate.py + homeassistant/components/modbus/cover.py + homeassistant/components/modbus/switch.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/mpchc/media_player.py homeassistant/components/mpd/media_player.py diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 885aa5fc235..66f4d542525 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -6,14 +6,13 @@ from unittest import mock import pytest from homeassistant.components.modbus.const import ( + CALL_TYPE_COIL, + CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_INPUT, - CONF_REGISTER, - CONF_REGISTER_TYPE, - CONF_REGISTERS, DEFAULT_HUB, MODBUS_DOMAIN as DOMAIN, ) -from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_SCAN_INTERVAL +from homeassistant.const import CONF_PLATFORM, CONF_SCAN_INTERVAL from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -38,31 +37,40 @@ class ReadResult: def __init__(self, register_words): """Init.""" self.registers = register_words + self.bits = register_words -async def run_test( - hass, use_mock_hub, register_config, entity_domain, register_words, expected +async def run_base_test( + sensor_name, + hass, + use_mock_hub, + data_array, + register_type, + entity_domain, + register_words, + expected, ): - """Run test for given config and check that sensor outputs expected result.""" + """Run test for given config.""" # Full sensor configuration - sensor_name = "modbus_test_sensor" scan_interval = 5 config = { entity_domain: { CONF_PLATFORM: "modbus", CONF_SCAN_INTERVAL: scan_interval, - CONF_REGISTERS: [ - dict(**{CONF_NAME: sensor_name, CONF_REGISTER: 1234}, **register_config) - ], + **data_array, } } # Setup inputs for the sensor read_result = ReadResult(register_words) - if register_config.get(CONF_REGISTER_TYPE) == CALL_TYPE_REGISTER_INPUT: + if register_type == CALL_TYPE_COIL: + use_mock_hub.read_coils.return_value = read_result + elif register_type == CALL_TYPE_DISCRETE: + use_mock_hub.read_discrete_inputs.return_value = read_result + elif register_type == CALL_TYPE_REGISTER_INPUT: use_mock_hub.read_input_registers.return_value = read_result - else: + else: # CALL_TYPE_REGISTER_HOLDING use_mock_hub.read_holding_registers.return_value = read_result # Initialize sensor @@ -76,8 +84,3 @@ async def run_test( with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): async_fire_time_changed(hass, now) await hass.async_block_till_done() - - # Check state - entity_id = f"{entity_domain}.{sensor_name}" - state = hass.states.get(entity_id).state - assert state == expected diff --git a/tests/components/modbus/test_modbus_binary_sensor.py b/tests/components/modbus/test_modbus_binary_sensor.py new file mode 100644 index 00000000000..ff64ad8723a --- /dev/null +++ b/tests/components/modbus/test_modbus_binary_sensor.py @@ -0,0 +1,98 @@ +"""The tests for the Modbus sensor component.""" +import logging + +from homeassistant.components.binary_sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.modbus.const import ( + CALL_TYPE_COIL, + CALL_TYPE_DISCRETE, + CONF_ADDRESS, + CONF_INPUT_TYPE, + CONF_INPUTS, +) +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON + +from .conftest import run_base_test + +_LOGGER = logging.getLogger(__name__) + + +async def run_sensor_test(hass, use_mock_hub, register_config, value, expected): + """Run test for given config.""" + sensor_name = "modbus_test_binary_sensor" + entity_domain = SENSOR_DOMAIN + data_array = { + CONF_INPUTS: [ + dict(**{CONF_NAME: sensor_name, CONF_ADDRESS: 1234}, **register_config) + ] + } + await run_base_test( + sensor_name, + hass, + use_mock_hub, + data_array, + register_config.get(CONF_INPUT_TYPE), + entity_domain, + value, + expected, + ) + + # Check state + entity_id = f"{entity_domain}.{sensor_name}" + state = hass.states.get(entity_id).state + assert state == expected + + +async def test_coil_true(hass, mock_hub): + """Test conversion of single word register.""" + register_config = { + CONF_INPUT_TYPE: CALL_TYPE_COIL, + } + await run_sensor_test( + hass, + mock_hub, + register_config, + [0xFF], + STATE_ON, + ) + + +async def test_coil_false(hass, mock_hub): + """Test conversion of single word register.""" + register_config = { + CONF_INPUT_TYPE: CALL_TYPE_COIL, + } + await run_sensor_test( + hass, + mock_hub, + register_config, + [0x00], + STATE_OFF, + ) + + +async def test_discrete_true(hass, mock_hub): + """Test conversion of single word register.""" + register_config = { + CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, + } + await run_sensor_test( + hass, + mock_hub, + register_config, + [0xFF], + expected="on", + ) + + +async def test_discrete_false(hass, mock_hub): + """Test conversion of single word register.""" + register_config = { + CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, + } + await run_sensor_test( + hass, + mock_hub, + register_config, + [0x00], + expected="off", + ) diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index ab4d745dc50..5ade2f197dd 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -8,7 +8,9 @@ from homeassistant.components.modbus.const import ( CONF_DATA_TYPE, CONF_OFFSET, CONF_PRECISION, + CONF_REGISTER, CONF_REGISTER_TYPE, + CONF_REGISTERS, CONF_REVERSE_ORDER, CONF_SCALE, DATA_TYPE_FLOAT, @@ -17,12 +19,42 @@ from homeassistant.components.modbus.const import ( DATA_TYPE_UINT, ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import CONF_NAME -from .conftest import run_test +from .conftest import run_base_test _LOGGER = logging.getLogger(__name__) +async def run_sensor_test( + hass, use_mock_hub, register_config, register_words, expected +): + """Run test for sensor.""" + sensor_name = "modbus_test_sensor" + entity_domain = SENSOR_DOMAIN + data_array = { + CONF_REGISTERS: [ + dict(**{CONF_NAME: sensor_name, CONF_REGISTER: 1234}, **register_config) + ] + } + + await run_base_test( + sensor_name, + hass, + use_mock_hub, + data_array, + register_config.get(CONF_REGISTER_TYPE), + entity_domain, + register_words, + expected, + ) + + # Check state + entity_id = f"{entity_domain}.{sensor_name}" + state = hass.states.get(entity_id).state + assert state == expected + + async def test_simple_word_register(hass, mock_hub): """Test conversion of single word register.""" register_config = { @@ -32,11 +64,10 @@ async def test_simple_word_register(hass, mock_hub): CONF_OFFSET: 0, CONF_PRECISION: 0, } - await run_test( + await run_sensor_test( hass, mock_hub, register_config, - SENSOR_DOMAIN, register_words=[0], expected="0", ) @@ -45,11 +76,10 @@ async def test_simple_word_register(hass, mock_hub): async def test_optional_conf_keys(hass, mock_hub): """Test handling of optional configuration keys.""" register_config = {} - await run_test( + await run_sensor_test( hass, mock_hub, register_config, - SENSOR_DOMAIN, register_words=[0x8000], expected="-32768", ) @@ -64,11 +94,10 @@ async def test_offset(hass, mock_hub): CONF_OFFSET: 13, CONF_PRECISION: 0, } - await run_test( + await run_sensor_test( hass, mock_hub, register_config, - SENSOR_DOMAIN, register_words=[7], expected="20", ) @@ -83,11 +112,10 @@ async def test_scale_and_offset(hass, mock_hub): CONF_OFFSET: 13, CONF_PRECISION: 0, } - await run_test( + await run_sensor_test( hass, mock_hub, register_config, - SENSOR_DOMAIN, register_words=[7], expected="34", ) @@ -102,11 +130,10 @@ async def test_ints_can_have_precision(hass, mock_hub): CONF_OFFSET: 13, CONF_PRECISION: 4, } - await run_test( + await run_sensor_test( hass, mock_hub, register_config, - SENSOR_DOMAIN, register_words=[7], expected="34.0000", ) @@ -121,11 +148,10 @@ async def test_floats_get_rounded_correctly(hass, mock_hub): CONF_OFFSET: 0, CONF_PRECISION: 0, } - await run_test( + await run_sensor_test( hass, mock_hub, register_config, - SENSOR_DOMAIN, register_words=[1], expected="2", ) @@ -140,11 +166,10 @@ async def test_parameters_as_strings(hass, mock_hub): CONF_OFFSET: "5", CONF_PRECISION: "1", } - await run_test( + await run_sensor_test( hass, mock_hub, register_config, - SENSOR_DOMAIN, register_words=[9], expected="18.5", ) @@ -159,11 +184,10 @@ async def test_floating_point_scale(hass, mock_hub): CONF_OFFSET: 0, CONF_PRECISION: 2, } - await run_test( + await run_sensor_test( hass, mock_hub, register_config, - SENSOR_DOMAIN, register_words=[1], expected="2.40", ) @@ -178,11 +202,10 @@ async def test_floating_point_offset(hass, mock_hub): CONF_OFFSET: -10.3, CONF_PRECISION: 1, } - await run_test( + await run_sensor_test( hass, mock_hub, register_config, - SENSOR_DOMAIN, register_words=[2], expected="-8.3", ) @@ -197,11 +220,10 @@ async def test_signed_two_word_register(hass, mock_hub): CONF_OFFSET: 0, CONF_PRECISION: 0, } - await run_test( + await run_sensor_test( hass, mock_hub, register_config, - SENSOR_DOMAIN, register_words=[0x89AB, 0xCDEF], expected="-1985229329", ) @@ -216,11 +238,10 @@ async def test_unsigned_two_word_register(hass, mock_hub): CONF_OFFSET: 0, CONF_PRECISION: 0, } - await run_test( + await run_sensor_test( hass, mock_hub, register_config, - SENSOR_DOMAIN, register_words=[0x89AB, 0xCDEF], expected=str(0x89ABCDEF), ) @@ -233,11 +254,10 @@ async def test_reversed(hass, mock_hub): CONF_DATA_TYPE: DATA_TYPE_UINT, CONF_REVERSE_ORDER: True, } - await run_test( + await run_sensor_test( hass, mock_hub, register_config, - SENSOR_DOMAIN, register_words=[0x89AB, 0xCDEF], expected=str(0xCDEF89AB), ) @@ -252,11 +272,10 @@ async def test_four_word_register(hass, mock_hub): CONF_OFFSET: 0, CONF_PRECISION: 0, } - await run_test( + await run_sensor_test( hass, mock_hub, register_config, - SENSOR_DOMAIN, register_words=[0x89AB, 0xCDEF, 0x0123, 0x4567], expected="9920249030613615975", ) @@ -271,11 +290,10 @@ async def test_four_word_register_precision_is_intact_with_int_params(hass, mock CONF_OFFSET: 3, CONF_PRECISION: 0, } - await run_test( + await run_sensor_test( hass, mock_hub, register_config, - SENSOR_DOMAIN, register_words=[0x0123, 0x4567, 0x89AB, 0xCDEF], expected="163971058432973793", ) @@ -290,11 +308,10 @@ async def test_four_word_register_precision_is_lost_with_float_params(hass, mock CONF_OFFSET: 3.0, CONF_PRECISION: 0, } - await run_test( + await run_sensor_test( hass, mock_hub, register_config, - SENSOR_DOMAIN, register_words=[0x0123, 0x4567, 0x89AB, 0xCDEF], expected="163971058432973792", ) @@ -310,11 +327,10 @@ async def test_two_word_input_register(hass, mock_hub): CONF_OFFSET: 0, CONF_PRECISION: 0, } - await run_test( + await run_sensor_test( hass, mock_hub, register_config, - SENSOR_DOMAIN, register_words=[0x89AB, 0xCDEF], expected=str(0x89ABCDEF), ) @@ -330,11 +346,10 @@ async def test_two_word_holding_register(hass, mock_hub): CONF_OFFSET: 0, CONF_PRECISION: 0, } - await run_test( + await run_sensor_test( hass, mock_hub, register_config, - SENSOR_DOMAIN, register_words=[0x89AB, 0xCDEF], expected=str(0x89ABCDEF), ) @@ -350,11 +365,10 @@ async def test_float_data_type(hass, mock_hub): CONF_OFFSET: 0, CONF_PRECISION: 5, } - await run_test( + await run_sensor_test( hass, mock_hub, register_config, - SENSOR_DOMAIN, register_words=[16286, 1617], expected="1.23457", ) @@ -370,11 +384,10 @@ async def test_string_data_type(hass, mock_hub): CONF_OFFSET: 0, CONF_PRECISION: 0, } - await run_test( + await run_sensor_test( hass, mock_hub, register_config, - SENSOR_DOMAIN, register_words=[0x3037, 0x2D30, 0x352D, 0x3230, 0x3230, 0x2031, 0x343A, 0x3335], expected="07-05-2020 14:35", ) From 988bc8b35fe15aa97cbc9a4226cde6dc1a533067 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 27 Sep 2020 22:23:53 -0400 Subject: [PATCH 401/514] Bump up zha dependency (#40689) * Bump up zha dependency * Update ZHA dependency for zigpy --- 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 05d6ccd061c..4d1d0dc1ac4 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.10.0", - "zigpy==0.24.1", + "zigpy==0.24.3", "zigpy-xbee==0.13.0", "zigpy-zigate==0.6.2", "zigpy-znp==0.2.0" diff --git a/requirements_all.txt b/requirements_all.txt index 673c99809e2..a02dd305543 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2329,7 +2329,7 @@ zigpy-zigate==0.6.2 zigpy-znp==0.2.0 # homeassistant.components.zha -zigpy==0.24.1 +zigpy==0.24.3 # homeassistant.components.zoneminder zm-py==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae5cb6fe359..9e76b78c42c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1089,7 +1089,7 @@ zigpy-zigate==0.6.2 zigpy-znp==0.2.0 # homeassistant.components.zha -zigpy==0.24.1 +zigpy==0.24.3 # homeassistant.components.zoneminder zm-py==0.4.0 From 373a1cabe6929a816c45cae6dbcb590247875558 Mon Sep 17 00:00:00 2001 From: Alex <7845120+newAM@users.noreply.github.com> Date: Sun, 27 Sep 2020 23:31:35 -0700 Subject: [PATCH 402/514] Re-enable hdmi_cec component (#40671) --- CODEOWNERS | 1 + homeassistant/components/hdmi_cec/manifest.json | 5 ++--- requirements_all.txt | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index c1064f4c60b..ede90722253 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -170,6 +170,7 @@ homeassistant/components/growatt_server/* @indykoning homeassistant/components/guardian/* @bachya homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco homeassistant/components/hassio/* @home-assistant/hass-io +homeassistant/components/hdmi_cec/* @newAM homeassistant/components/heatmiser/* @andylockran homeassistant/components/heos/* @andrewsayre homeassistant/components/here_travel_time/* @eifinger diff --git a/homeassistant/components/hdmi_cec/manifest.json b/homeassistant/components/hdmi_cec/manifest.json index c3817d25776..4c307582281 100644 --- a/homeassistant/components/hdmi_cec/manifest.json +++ b/homeassistant/components/hdmi_cec/manifest.json @@ -1,8 +1,7 @@ { - "disabled": "Dependency contains code that breaks Home Assistant.", "domain": "hdmi_cec", "name": "HDMI-CEC", "documentation": "https://www.home-assistant.io/integrations/hdmi_cec", - "requirements": ["pyCEC==0.4.13"], - "codeowners": [] + "requirements": ["pyCEC==0.4.14"], + "codeowners": ["@newAM"] } diff --git a/requirements_all.txt b/requirements_all.txt index a02dd305543..a37ec12e311 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1197,6 +1197,9 @@ py-synology==0.2.0 # homeassistant.components.seventeentrack py17track==2.2.2 +# homeassistant.components.hdmi_cec +pyCEC==0.4.14 + # homeassistant.components.control4 pyControl4==0.0.6 From 953f6bad4608600fb610e1f87107f981f17696bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 28 Sep 2020 10:10:10 +0200 Subject: [PATCH 403/514] Allow non-authenticated calls to supervisor logs during onboarding (#40617) --- homeassistant/components/hassio/http.py | 6 +++++- tests/components/hassio/test_http.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 60888d8d301..95f861e6097 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -34,6 +34,10 @@ NO_TIMEOUT = re.compile( r")$" ) +NO_AUTH_ONBOARDING = re.compile( + r"^(?:" r"|supervisor/logs" r"|snapshots/[^/]+/.+" r")$" +) + NO_AUTH = re.compile( r"^(?:" r"|app/.*" r"|addons/[^/]+/logo" r"|addons/[^/]+/icon" r")$" ) @@ -149,7 +153,7 @@ def _get_timeout(path: str) -> int: def _need_auth(hass, path: str) -> bool: """Return if a path need authentication.""" - if not async_is_onboarded(hass) and path.startswith("snapshots"): + if not async_is_onboarded(hass) and NO_AUTH_ONBOARDING.match(path): return False if NO_AUTH.match(path): return False diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index db6e9d1f85e..d069b311ab2 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -155,6 +155,8 @@ def test_need_auth(hass): """Test if the requested path needs authentication.""" assert not _need_auth(hass, "addons/test/logo") assert _need_auth(hass, "snapshots/new/upload") + assert _need_auth(hass, "supervisor/logs") hass.data["onboarding"] = False assert not _need_auth(hass, "snapshots/new/upload") + assert not _need_auth(hass, "supervisor/logs") From 8466682b656c116eff055b5d92f8640ca32bd16d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Stankowski?= Date: Mon, 28 Sep 2020 11:12:35 +0200 Subject: [PATCH 404/514] Bump Airly package to 1.0.0 (#40695) --- homeassistant/components/airly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airly/manifest.json b/homeassistant/components/airly/manifest.json index 8140bc91c5f..77de843ffce 100644 --- a/homeassistant/components/airly/manifest.json +++ b/homeassistant/components/airly/manifest.json @@ -3,7 +3,7 @@ "name": "Airly", "documentation": "https://www.home-assistant.io/integrations/airly", "codeowners": ["@bieniu"], - "requirements": ["airly==0.0.2"], + "requirements": ["airly==1.0.0"], "config_flow": true, "quality_scale": "platinum" } diff --git a/requirements_all.txt b/requirements_all.txt index a37ec12e311..4704c63510e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -233,7 +233,7 @@ aiounifi==23 aioymaps==1.1.0 # homeassistant.components.airly -airly==0.0.2 +airly==1.0.0 # homeassistant.components.aladdin_connect aladdin_connect==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e76b78c42c..be9c3234e2f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ aiounifi==23 aioymaps==1.1.0 # homeassistant.components.airly -airly==0.0.2 +airly==1.0.0 # homeassistant.components.ambiclimate ambiclimate==0.2.1 From 29d87646dd7ba593c9156947c3258014ddaf8557 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 28 Sep 2020 13:35:40 +0200 Subject: [PATCH 405/514] Improve config flow descriptions in Shelly integration (#40396) --- homeassistant/components/shelly/strings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 1a7c8c78189..69a09204227 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -4,6 +4,7 @@ "flow_title": "Shelly: {name}", "step": { "user": { + "description": "Before set up, the battery-powered device must be woken up by pressing the button on the device.", "data": { "host": "[%key:common::config_flow::data::host%]" } @@ -15,7 +16,7 @@ } }, "confirm_discovery": { - "description": "Do you want to set up the {model} at {host}?" + "description": "Do you want to set up the {model} at {host}?\n\nBefore set up, the battery-powered device must be woken up by pressing the button on the device." } }, "error": { From 1c252634b7da5309944bdd1550c6a62b7d45a270 Mon Sep 17 00:00:00 2001 From: Matt Black Date: Mon, 28 Sep 2020 21:42:01 +1000 Subject: [PATCH 406/514] Bump snapcast dependency to 2.1.1 (#40561) --- homeassistant/components/snapcast/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/snapcast/manifest.json b/homeassistant/components/snapcast/manifest.json index 31eb0491eb4..4e65b60280b 100644 --- a/homeassistant/components/snapcast/manifest.json +++ b/homeassistant/components/snapcast/manifest.json @@ -2,6 +2,6 @@ "domain": "snapcast", "name": "Snapcast", "documentation": "https://www.home-assistant.io/integrations/snapcast", - "requirements": ["snapcast==2.0.10"], + "requirements": ["snapcast==2.1.1"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 4704c63510e..4158b0bc9bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2035,7 +2035,7 @@ smarthab==0.21 smhi-pkg==1.0.13 # homeassistant.components.snapcast -snapcast==2.0.10 +snapcast==2.1.1 # homeassistant.components.socialblade socialbladeclient==0.5 From 8bc62f367818a3a7059ecd4d4247a49245599cc3 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Mon, 28 Sep 2020 08:24:30 -0400 Subject: [PATCH 407/514] Fix camera play stream (#40641) Co-authored-by: Justin Wong <46082645+uvjustin@users.noreply.github.com> --- homeassistant/components/camera/__init__.py | 44 ++++++++++++++++--- homeassistant/components/cast/manifest.json | 2 +- homeassistant/components/cast/media_player.py | 5 ++- .../components/media_player/__init__.py | 2 + .../components/media_player/const.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 49 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 49b60183920..25505800709 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -19,6 +19,7 @@ from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_EXTRA, DOMAIN as DOMAIN_MP, SERVICE_PLAY_MEDIA, ) @@ -47,7 +48,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, entity_sources from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.network import get_url from homeassistant.loader import bind_hass @@ -696,14 +697,47 @@ async def async_handle_play_stream_service(camera, service_call): options=camera.stream_options, ) data = { - ATTR_ENTITY_ID: entity_ids, ATTR_MEDIA_CONTENT_ID: f"{get_url(hass)}{url}", ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[fmt], } - await hass.services.async_call( - DOMAIN_MP, SERVICE_PLAY_MEDIA, data, blocking=True, context=service_call.context - ) + # It is required to send a different payload for cast media players + cast_entity_ids = [ + entity + for entity, source in entity_sources(hass).items() + if entity in entity_ids and source["domain"] == "cast" + ] + other_entity_ids = list(set(entity_ids) - set(cast_entity_ids)) + + if cast_entity_ids: + await hass.services.async_call( + DOMAIN_MP, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: cast_entity_ids, + **data, + ATTR_MEDIA_EXTRA: { + "stream_type": "LIVE", + "media_info": { + "hlsVideoSegmentFormat": "fmp4", + }, + }, + }, + blocking=True, + context=service_call.context, + ) + + if other_entity_ids: + await hass.services.async_call( + DOMAIN_MP, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: other_entity_ids, + **data, + }, + blocking=True, + context=service_call.context, + ) async def async_handle_record_service(camera, call): diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 9d9a7bacb7f..03412d3b6df 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==7.4.1"], + "requirements": ["pychromecast==7.5.0"], "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 f948c51655b..788da18e8bd 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -21,6 +21,7 @@ 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 ( + ATTR_MEDIA_EXTRA, MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, @@ -574,7 +575,9 @@ class CastDevice(MediaPlayerEntity): except NotImplementedError: _LOGGER.error("App %s not supported", app_name) else: - self._chromecast.media_controller.play_media(media_id, media_type) + self._chromecast.media_controller.play_media( + media_id, media_type, **kwargs.get(ATTR_MEDIA_EXTRA, {}) + ) # ========== Properties ========== @property diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 47804fcc1cd..1bf0e213a25 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -72,6 +72,7 @@ from .const import ( ATTR_MEDIA_DURATION, ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_EPISODE, + ATTR_MEDIA_EXTRA, ATTR_MEDIA_PLAYLIST, ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT, @@ -140,6 +141,7 @@ MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = { vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string, vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, vol.Optional(ATTR_MEDIA_ENQUEUE): cv.boolean, + vol.Optional(ATTR_MEDIA_EXTRA, default={}): dict, } ATTR_TO_PROPERTY = [ diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 0035fc9f4d2..3db31006341 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -12,6 +12,7 @@ ATTR_MEDIA_CONTENT_ID = "media_content_id" ATTR_MEDIA_CONTENT_TYPE = "media_content_type" ATTR_MEDIA_DURATION = "media_duration" ATTR_MEDIA_ENQUEUE = "enqueue" +ATTR_MEDIA_EXTRA = "extra" ATTR_MEDIA_EPISODE = "media_episode" ATTR_MEDIA_PLAYLIST = "media_playlist" ATTR_MEDIA_POSITION = "media_position" diff --git a/requirements_all.txt b/requirements_all.txt index 4158b0bc9bc..acaf637e37f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1280,7 +1280,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==7.4.1 +pychromecast==7.5.0 # homeassistant.components.cmus pycmus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be9c3234e2f..f49610c5a10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -625,7 +625,7 @@ pyblackbird==0.5 pybotvac==0.0.17 # homeassistant.components.cast -pychromecast==7.4.1 +pychromecast==7.5.0 # homeassistant.components.coolmaster pycoolmasternet-async==0.1.2 From e08ee282ab6e0a913545a81bf85098c2df2adbec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Sep 2020 07:43:22 -0500 Subject: [PATCH 408/514] Abort execution of template renders that overwhelm the system (#40647) --- .../components/websocket_api/commands.py | 14 ++++- homeassistant/helpers/template.py | 50 +++++++++++++++++ homeassistant/util/thread.py | 33 +++++++++++ .../components/websocket_api/test_commands.py | 47 +++++++++++----- tests/helpers/test_template.py | 28 ++++++++++ tests/util/test_thread.py | 55 +++++++++++++++++++ 6 files changed, 212 insertions(+), 15 deletions(-) create mode 100644 tests/util/test_thread.py diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index d80c7934dd4..11d97f58f50 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -239,22 +239,32 @@ def handle_ping(hass, connection, msg): connection.send_message(pong_message(msg["id"])) -@callback @decorators.websocket_command( { vol.Required("type"): "render_template", vol.Required("template"): str, vol.Optional("entity_ids"): cv.entity_ids, vol.Optional("variables"): dict, + vol.Optional("timeout"): vol.Coerce(float), } ) -def handle_render_template(hass, connection, msg): +@decorators.async_response +async def handle_render_template(hass, connection, msg): """Handle render_template command.""" template_str = msg["template"] template = Template(template_str, hass) variables = msg.get("variables") + timeout = msg.get("timeout") info = None + if timeout and await template.async_render_will_timeout(timeout): + connection.send_error( + msg["id"], + const.ERR_TEMPLATE_ERROR, + f"Exceeded maximum execution time of {timeout}s", + ) + return + @callback def _template_listener(event, updates): nonlocal info diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 5564024a92b..721c1407f37 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1,4 +1,5 @@ """Template helper methods for rendering strings with Home Assistant data.""" +import asyncio import base64 import collections.abc from datetime import datetime, timedelta @@ -36,6 +37,7 @@ 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 from homeassistant.util.async_ import run_callback_threadsafe +from homeassistant.util.thread import ThreadWithException # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any @@ -309,6 +311,54 @@ class Template: except jinja2.TemplateError as err: raise TemplateError(err) from err + async def async_render_will_timeout( + self, timeout: float, variables: TemplateVarsType = None, **kwargs: Any + ) -> bool: + """Check to see if rendering a template will timeout during render. + + This is intended to check for expensive templates + that will make the system unstable. The template + is rendered in the executor to ensure it does not + tie up the event loop. + + This function is not a security control and is only + intended to be used as a safety check when testing + templates. + + This method must be run in the event loop. + """ + assert self.hass + + if self.is_static: + return False + + compiled = self._compiled or self._ensure_compiled() + + if variables is not None: + kwargs.update(variables) + + finish_event = asyncio.Event() + + def _render_template(): + try: + compiled.render(kwargs) + except TimeoutError: + pass + finally: + run_callback_threadsafe(self.hass.loop, finish_event.set) + + try: + template_render_thread = ThreadWithException(target=_render_template) + template_render_thread.start() + await asyncio.wait_for(finish_event.wait(), timeout=timeout) + except asyncio.TimeoutError: + template_render_thread.raise_exc(TimeoutError) + return True + finally: + template_render_thread.join() + + return False + @callback def async_render_to_info( self, variables: TemplateVarsType = None, **kwargs: Any diff --git a/homeassistant/util/thread.py b/homeassistant/util/thread.py index e5654e6f8c6..bf61c67172a 100644 --- a/homeassistant/util/thread.py +++ b/homeassistant/util/thread.py @@ -1,4 +1,6 @@ """Threading util helpers.""" +import ctypes +import inspect import sys import threading from typing import Any @@ -24,3 +26,34 @@ def fix_threading_exception_logging() -> None: sys.excepthook(*sys.exc_info()) threading.Thread.run = run # type: ignore + + +def _async_raise(tid: int, exctype: Any) -> None: + """Raise an exception in the threads with id tid.""" + if not inspect.isclass(exctype): + raise TypeError("Only types can be raised (not instances)") + + c_tid = ctypes.c_long(tid) + res = ctypes.pythonapi.PyThreadState_SetAsyncExc(c_tid, ctypes.py_object(exctype)) + + if res == 1: + return + + # "if it returns a number greater than one, you're in trouble, + # and you should call it again with exc=NULL to revert the effect" + ctypes.pythonapi.PyThreadState_SetAsyncExc(c_tid, None) + raise SystemError("PyThreadState_SetAsyncExc failed") + + +class ThreadWithException(threading.Thread): + """A thread class that supports raising exception in the thread from another thread. + + Based on + https://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread/49877671 + + """ + + def raise_exc(self, exctype: Any) -> None: + """Raise the given exception type in the context of this thread.""" + assert self.ident + _async_raise(self.ident, exctype) diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index ea6f2f42bdc..3969ff90706 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -397,9 +397,7 @@ async def test_subscribe_unsubscribe_events_state_changed( assert msg["event"]["data"]["entity_id"] == "light.permitted" -async def test_render_template_renders_template( - hass, websocket_client, hass_admin_user -): +async def test_render_template_renders_template(hass, websocket_client): """Test simple template is rendered and updated.""" hass.states.async_set("light.test", "on") @@ -437,7 +435,7 @@ async def test_render_template_renders_template( async def test_render_template_manual_entity_ids_no_longer_needed( - hass, websocket_client, hass_admin_user + hass, websocket_client ): """Test that updates to specified entity ids cause a template rerender.""" hass.states.async_set("light.test", "on") @@ -475,9 +473,7 @@ async def test_render_template_manual_entity_ids_no_longer_needed( } -async def test_render_template_with_error( - hass, websocket_client, hass_admin_user, caplog -): +async def test_render_template_with_error(hass, websocket_client, caplog): """Test a template with an error.""" await websocket_client.send_json( {"id": 5, "type": "render_template", "template": "{{ my_unknown_var() + 1 }}"} @@ -492,9 +488,7 @@ async def test_render_template_with_error( assert "TemplateError" not in caplog.text -async def test_render_template_with_delayed_error( - hass, websocket_client, hass_admin_user, caplog -): +async def test_render_template_with_delayed_error(hass, websocket_client, caplog): """Test a template with an error that only happens after a state change.""" hass.states.async_set("sensor.test", "on") await hass.async_block_till_done() @@ -539,9 +533,36 @@ async def test_render_template_with_delayed_error( assert "TemplateError" not in caplog.text -async def test_render_template_returns_with_match_all( - hass, websocket_client, hass_admin_user -): +async def test_render_template_with_timeout(hass, websocket_client, caplog): + """Test a template that will timeout.""" + + slow_template_str = """ +{% for var in range(1000) -%} + {% for var in range(1000) -%} + {{ var }} + {%- endfor %} +{%- endfor %} +""" + + await websocket_client.send_json( + { + "id": 5, + "type": "render_template", + "timeout": 0.000001, + "template": slow_template_str, + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR + + assert "TemplateError" not in caplog.text + + +async def test_render_template_returns_with_match_all(hass, websocket_client): """Test that a template that would match with all entities still return success.""" await websocket_client.send_json( {"id": 5, "type": "render_template", "template": "State is: {{ 42 }}"} diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 7cfdd4241b7..be6b1bd2ecf 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2455,3 +2455,31 @@ async def test_lifecycle(hass): assert info.filter("sensor.sensor1") is False assert info.filter_lifecycle("sensor.new") is True assert info.filter_lifecycle("sensor.removed") is True + + +async def test_template_timeout(hass): + """Test to see if a template will timeout.""" + for i in range(2): + hass.states.async_set(f"sensor.sensor{i}", "on") + + tmp = template.Template("{{ states | count }}", hass) + assert await tmp.async_render_will_timeout(3) is False + + tmp2 = template.Template("{{ error_invalid + 1 }}", hass) + assert await tmp2.async_render_will_timeout(3) is False + + tmp3 = template.Template("static", hass) + assert await tmp3.async_render_will_timeout(3) is False + + tmp4 = template.Template("{{ var1 }}", hass) + assert await tmp4.async_render_will_timeout(3, {"var1": "ok"}) is False + + slow_template_str = """ +{% for var in range(1000) -%} + {% for var in range(1000) -%} + {{ var }} + {%- endfor %} +{%- endfor %} +""" + tmp5 = template.Template(slow_template_str, hass) + assert await tmp5.async_render_will_timeout(0.000001) is True diff --git a/tests/util/test_thread.py b/tests/util/test_thread.py new file mode 100644 index 00000000000..d5f05f5c93e --- /dev/null +++ b/tests/util/test_thread.py @@ -0,0 +1,55 @@ +"""Test Home Assistant thread utils.""" + +import asyncio + +import pytest + +from homeassistant.util.async_ import run_callback_threadsafe +from homeassistant.util.thread import ThreadWithException + + +async def test_thread_with_exception_invalid(hass): + """Test throwing an invalid thread exception.""" + + finish_event = asyncio.Event() + + def _do_nothing(*_): + run_callback_threadsafe(hass.loop, finish_event.set) + + test_thread = ThreadWithException(target=_do_nothing) + test_thread.start() + await asyncio.wait_for(finish_event.wait(), timeout=0.1) + + with pytest.raises(TypeError): + test_thread.raise_exc(_EmptyClass()) + test_thread.join() + + +async def test_thread_not_started(hass): + """Test throwing when the thread is not started.""" + + test_thread = ThreadWithException(target=lambda *_: None) + + with pytest.raises(AssertionError): + test_thread.raise_exc(TimeoutError) + + +async def test_thread_fails_raise(hass): + """Test throwing after already ended.""" + + finish_event = asyncio.Event() + + def _do_nothing(*_): + run_callback_threadsafe(hass.loop, finish_event.set) + + test_thread = ThreadWithException(target=_do_nothing) + test_thread.start() + await asyncio.wait_for(finish_event.wait(), timeout=0.1) + test_thread.join() + + with pytest.raises(SystemError): + test_thread.raise_exc(ValueError) + + +class _EmptyClass: + """An empty class.""" From a19e10c57ad44ff18e99d8055bf38a7e16ec4e3c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Sep 2020 08:58:55 -0500 Subject: [PATCH 409/514] Add entity glob matching support to history (#40387) --- homeassistant/components/history/__init__.py | 112 ++- homeassistant/components/logbook/__init__.py | 3 - tests/components/history/test_init.py | 119 ++- tests/components/logbook/test_init.py | 828 +++++++++---------- 4 files changed, 561 insertions(+), 501 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 0e6fabb66aa..f90083c0b50 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -8,7 +8,7 @@ import time from typing import Optional, cast from aiohttp import web -from sqlalchemy import and_, bindparam, func +from sqlalchemy import and_, bindparam, func, not_, or_ from sqlalchemy.ext import baked import voluptuous as vol @@ -29,6 +29,10 @@ from homeassistant.const import ( ) from homeassistant.core import Context, State, split_entity_id import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entityfilter import ( + CONF_ENTITY_GLOBS, + INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, +) import homeassistant.util.dt as dt_util # mypy: allow-untyped-defs, no-check-untyped-defs @@ -41,26 +45,19 @@ CONF_ORDER = "use_include_order" STATE_KEY = "state" LAST_CHANGED_KEY = "last_changed" -# Not reusing from entityfilter because history does not support glob filtering -_FILTER_SCHEMA_INNER = vol.Schema( - { - vol.Optional(CONF_DOMAINS, default=[]): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, - } -) -_FILTER_SCHEMA = vol.Schema( - { - vol.Optional( - CONF_INCLUDE, default=_FILTER_SCHEMA_INNER({}) - ): _FILTER_SCHEMA_INNER, - vol.Optional( - CONF_EXCLUDE, default=_FILTER_SCHEMA_INNER({}) - ): _FILTER_SCHEMA_INNER, - vol.Optional(CONF_ORDER, default=False): cv.boolean, - } -) +GLOB_TO_SQL_CHARS = { + 42: "%", # * + 46: "_", # . +} -CONFIG_SCHEMA = vol.Schema({DOMAIN: _FILTER_SCHEMA}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend( + {vol.Optional(CONF_ORDER, default=False): cv.boolean} + ) + }, + extra=vol.ALLOW_EXTRA, +) SIGNIFICANT_DOMAINS = ( "climate", @@ -563,10 +560,12 @@ def sqlalchemy_filter_from_include_exclude_conf(conf): if exclude: filters.excluded_entities = exclude.get(CONF_ENTITIES, []) filters.excluded_domains = exclude.get(CONF_DOMAINS, []) + filters.excluded_entity_globs = exclude.get(CONF_ENTITY_GLOBS, []) include = conf.get(CONF_INCLUDE) if include: filters.included_entities = include.get(CONF_ENTITIES, []) filters.included_domains = include.get(CONF_DOMAINS, []) + filters.included_entity_globs = include.get(CONF_ENTITY_GLOBS, []) return filters @@ -577,8 +576,11 @@ class Filters: """Initialise the include and exclude filters.""" self.excluded_entities = [] self.excluded_domains = [] + self.excluded_entity_globs = [] + self.included_entities = [] self.included_domains = [] + self.included_entity_globs = [] def apply(self, query, entity_ids=None): """Apply the include/exclude filter on domains and entities on query. @@ -619,52 +621,46 @@ class Filters: if ( self.excluded_entities or self.excluded_domains + or self.excluded_entity_globs or self.included_entities or self.included_domains + or self.included_entity_globs ): baked_query += lambda q: q.filter(self.entity_filter()) def entity_filter(self): """Generate the entity filter query.""" - entity_filter = None - # filter if only excluded domain is configured - if self.excluded_domains and not self.included_domains: - entity_filter = ~States.domain.in_(self.excluded_domains) - if self.included_entities: - entity_filter &= States.entity_id.in_(self.included_entities) - # filter if only included domain is configured - elif not self.excluded_domains and self.included_domains: - entity_filter = States.domain.in_(self.included_domains) - if self.included_entities: - entity_filter |= States.entity_id.in_(self.included_entities) - # filter if included and excluded domain is configured - elif self.excluded_domains and self.included_domains: - entity_filter = ~States.domain.in_(self.excluded_domains) - if self.included_entities: - entity_filter &= States.domain.in_( - self.included_domains - ) | States.entity_id.in_(self.included_entities) - else: - entity_filter &= States.domain.in_( - self.included_domains - ) & ~States.domain.in_(self.excluded_domains) - # no domain filter just included entities - elif ( - not self.excluded_domains - and not self.included_domains - and self.included_entities - ): - entity_filter = States.entity_id.in_(self.included_entities) - # finally apply excluded entities filter if configured - if self.excluded_entities: - if entity_filter is not None: - entity_filter = (entity_filter) & ~States.entity_id.in_( - self.excluded_entities - ) - else: - entity_filter = ~States.entity_id.in_(self.excluded_entities) + includes = [] + if self.included_domains: + includes.append(States.domain.in_(self.included_domains)) + if self.included_entities: + includes.append(States.entity_id.in_(self.included_entities)) + for glob in self.included_entity_globs: + includes.append(_glob_to_like(glob)) - return entity_filter + excludes = [] + if self.excluded_domains: + excludes.append(States.domain.in_(self.excluded_domains)) + if self.excluded_entities: + excludes.append(States.entity_id.in_(self.excluded_entities)) + for glob in self.excluded_entity_globs: + excludes.append(_glob_to_like(glob)) + + if not includes and not excludes: + return None + + if includes and not excludes: + return or_(*includes) + + if not excludes and includes: + return not_(or_(*excludes)) + + return or_(*includes) & not_(or_(*excludes)) + + +def _glob_to_like(glob_str): + """Translate glob to sql.""" + return States.entity_id.like(glob_str.translate(GLOB_TO_SQL_CHARS)) class LazyState(State): diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 29068c1e261..e922c532f7d 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -591,9 +591,6 @@ def _keep_event(hass, event, entities_filter): if event.event_type in HOMEASSISTANT_EVENTS: return entities_filter is None or entities_filter(HA_DOMAIN_ENTITY_ID) - if event.event_type == EVENT_STATE_CHANGED: - return entities_filter is None or entities_filter(event.entity_id) - entity_id = event.data_entity_id if entity_id: return entities_filter is None or entities_filter(entity_id) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index c15e4431f87..3ae947edee2 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -18,7 +18,7 @@ from tests.common import ( init_recorder_component, mock_state_change_event, ) -from tests.components.recorder.common import wait_recording_done +from tests.components.recorder.common import trigger_db_commit, wait_recording_done class TestComponentHistory(unittest.TestCase): @@ -823,3 +823,120 @@ async def test_fetch_period_api_with_include_order(hass, hass_client): params={"filter_entity_id": "non.existing,something.else"}, ) assert response.status == 200 + + +async def test_fetch_period_api_with_entity_glob_include(hass, hass_client): + """Test the fetch period view for history.""" + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component( + hass, + "history", + { + "history": { + "include": {"entity_globs": ["light.k*"]}, + } + }, + ) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + hass.states.async_set("light.kitchen", "on") + hass.states.async_set("light.cow", "on") + hass.states.async_set("light.nomatch", "on") + + 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() + response = await client.get( + f"/api/history/period/{dt_util.utcnow().isoformat()}", + ) + assert response.status == 200 + response_json = await response.json() + assert response_json[0][0]["entity_id"] == "light.kitchen" + + +async def test_fetch_period_api_with_entity_glob_exclude(hass, hass_client): + """Test the fetch period view for history.""" + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component( + hass, + "history", + { + "history": { + "exclude": { + "entity_globs": ["light.k*"], + "domains": "switch", + "entities": "media_player.test", + }, + } + }, + ) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + hass.states.async_set("light.kitchen", "on") + hass.states.async_set("light.cow", "on") + hass.states.async_set("light.match", "on") + hass.states.async_set("switch.match", "on") + hass.states.async_set("media_player.test", "on") + + 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() + response = await client.get( + f"/api/history/period/{dt_util.utcnow().isoformat()}", + ) + assert response.status == 200 + response_json = await response.json() + assert len(response_json) == 2 + assert response_json[0][0]["entity_id"] == "light.cow" + assert response_json[1][0]["entity_id"] == "light.match" + + +async def test_fetch_period_api_with_entity_glob_include_and_exclude(hass, hass_client): + """Test the fetch period view for history.""" + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component( + hass, + "history", + { + "history": { + "exclude": { + "entity_globs": ["light.many*"], + }, + "include": { + "entity_globs": ["light.m*"], + "domains": "switch", + "entities": "media_player.test", + }, + } + }, + ) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + hass.states.async_set("light.kitchen", "on") + hass.states.async_set("light.cow", "on") + hass.states.async_set("light.match", "on") + hass.states.async_set("light.many_state_changes", "on") + hass.states.async_set("switch.match", "on") + hass.states.async_set("media_player.test", "on") + + 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() + response = await client.get( + f"/api/history/period/{dt_util.utcnow().isoformat()}", + ) + assert response.status == 200 + response_json = await response.json() + assert len(response_json) == 3 + assert response_json[0][0]["entity_id"] == "light.match" + assert response_json[1][0]["entity_id"] == "media_player.test" + assert response_json[2][0]["entity_id"] == "switch.match" diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index d805eb40ec1..63ede883216 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -26,6 +26,7 @@ from homeassistant.const import ( CONF_INCLUDE, EVENT_CALL_SERVICE, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, STATE_NOT_HOME, @@ -33,10 +34,7 @@ from homeassistant.const import ( STATE_ON, ) import homeassistant.core as ha -from homeassistant.helpers.entityfilter import ( - CONF_ENTITY_GLOBS, - convert_include_exclude_filter, -) +from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util @@ -167,413 +165,6 @@ class TestComponentLogbook(unittest.TestCase): entries[1], pointC, "bla", domain="sensor", entity_id=entity_id ) - def test_exclude_events_entity(self): - """Test if events are filtered if entity is excluded in config.""" - entity_id = "sensor.bla" - entity_id2 = "sensor.blu" - pointA = dt_util.utcnow() - pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES) - entity_attr_cache = logbook.EntityAttributeCache(self.hass) - - eventA = self.create_state_changed_event(pointA, entity_id, 10) - eventB = self.create_state_changed_event(pointB, entity_id2, 20) - - config = logbook.CONFIG_SCHEMA( - { - ha.DOMAIN: {}, - logbook.DOMAIN: {CONF_EXCLUDE: {CONF_ENTITIES: [entity_id]}}, - } - ) - entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN]) - events = [ - e - for e in ( - MockLazyEventPartialState(EVENT_HOMEASSISTANT_STOP), - eventA, - eventB, - ) - if logbook._keep_event(self.hass, e, entities_filter) - ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) - - assert len(entries) == 2 - self.assert_entry( - entries[0], name="Home Assistant", message="stopped", domain=ha.DOMAIN - ) - self.assert_entry( - entries[1], pointB, "blu", domain="sensor", entity_id=entity_id2 - ) - - def test_exclude_events_domain(self): - """Test if events are filtered if domain is excluded in config.""" - entity_id = "switch.bla" - entity_id2 = "sensor.blu" - pointA = dt_util.utcnow() - pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES) - entity_attr_cache = logbook.EntityAttributeCache(self.hass) - - eventA = self.create_state_changed_event(pointA, entity_id, 10) - eventB = self.create_state_changed_event(pointB, entity_id2, 20) - - config = logbook.CONFIG_SCHEMA( - { - ha.DOMAIN: {}, - logbook.DOMAIN: {CONF_EXCLUDE: {CONF_DOMAINS: ["switch", "alexa"]}}, - } - ) - entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN]) - events = [ - e - for e in ( - MockLazyEventPartialState(EVENT_HOMEASSISTANT_START), - MockLazyEventPartialState(EVENT_ALEXA_SMART_HOME), - eventA, - eventB, - ) - if logbook._keep_event(self.hass, e, entities_filter) - ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) - - assert len(entries) == 2 - self.assert_entry( - entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN - ) - self.assert_entry( - entries[1], pointB, "blu", domain="sensor", entity_id=entity_id2 - ) - - def test_exclude_events_domain_glob(self): - """Test if events are filtered if domain or glob is excluded in config.""" - entity_id = "switch.bla" - entity_id2 = "sensor.blu" - entity_id3 = "sensor.excluded" - pointA = dt_util.utcnow() - pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES) - pointC = pointB + timedelta(minutes=logbook.GROUP_BY_MINUTES) - entity_attr_cache = logbook.EntityAttributeCache(self.hass) - - eventA = self.create_state_changed_event(pointA, entity_id, 10) - eventB = self.create_state_changed_event(pointB, entity_id2, 20) - eventC = self.create_state_changed_event(pointC, entity_id3, 30) - - config = logbook.CONFIG_SCHEMA( - { - ha.DOMAIN: {}, - logbook.DOMAIN: { - CONF_EXCLUDE: { - CONF_DOMAINS: ["switch", "alexa"], - CONF_ENTITY_GLOBS: "*.excluded", - } - }, - } - ) - entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN]) - events = [ - e - for e in ( - MockLazyEventPartialState(EVENT_HOMEASSISTANT_START), - MockLazyEventPartialState(EVENT_ALEXA_SMART_HOME), - eventA, - eventB, - eventC, - ) - if logbook._keep_event(self.hass, e, entities_filter) - ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) - - assert len(entries) == 2 - self.assert_entry( - entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN - ) - self.assert_entry( - entries[1], pointB, "blu", domain="sensor", entity_id=entity_id2 - ) - - def test_include_events_entity(self): - """Test if events are filtered if entity is included in config.""" - entity_id = "sensor.bla" - entity_id2 = "sensor.blu" - pointA = dt_util.utcnow() - pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES) - entity_attr_cache = logbook.EntityAttributeCache(self.hass) - - eventA = self.create_state_changed_event(pointA, entity_id, 10) - eventB = self.create_state_changed_event(pointB, entity_id2, 20) - - config = logbook.CONFIG_SCHEMA( - { - ha.DOMAIN: {}, - logbook.DOMAIN: { - CONF_INCLUDE: { - CONF_DOMAINS: ["homeassistant"], - CONF_ENTITIES: [entity_id2], - } - }, - } - ) - entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN]) - events = [ - e - for e in ( - MockLazyEventPartialState(EVENT_HOMEASSISTANT_STOP), - eventA, - eventB, - ) - if logbook._keep_event(self.hass, e, entities_filter) - ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) - - assert len(entries) == 2 - self.assert_entry( - entries[0], name="Home Assistant", message="stopped", domain=ha.DOMAIN - ) - self.assert_entry( - entries[1], pointB, "blu", domain="sensor", entity_id=entity_id2 - ) - - def test_include_events_domain(self): - """Test if events are filtered if domain is included in config.""" - assert setup_component(self.hass, "alexa", {}) - entity_id = "switch.bla" - entity_id2 = "sensor.blu" - pointA = dt_util.utcnow() - pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES) - entity_attr_cache = logbook.EntityAttributeCache(self.hass) - - event_alexa = MockLazyEventPartialState( - EVENT_ALEXA_SMART_HOME, - {"request": {"namespace": "Alexa.Discovery", "name": "Discover"}}, - ) - - eventA = self.create_state_changed_event(pointA, entity_id, 10) - eventB = self.create_state_changed_event(pointB, entity_id2, 20) - - config = logbook.CONFIG_SCHEMA( - { - ha.DOMAIN: {}, - logbook.DOMAIN: { - CONF_INCLUDE: {CONF_DOMAINS: ["homeassistant", "sensor", "alexa"]} - }, - } - ) - entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN]) - events = [ - e - for e in ( - MockLazyEventPartialState(EVENT_HOMEASSISTANT_START), - event_alexa, - eventA, - eventB, - ) - if logbook._keep_event(self.hass, e, entities_filter) - ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) - - assert len(entries) == 3 - self.assert_entry( - entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN - ) - self.assert_entry(entries[1], name="Amazon Alexa", domain="alexa") - self.assert_entry( - entries[2], pointB, "blu", domain="sensor", entity_id=entity_id2 - ) - - def test_include_events_domain_glob(self): - """Test if events are filtered if domain or glob is included in config.""" - assert setup_component(self.hass, "alexa", {}) - entity_id = "switch.bla" - entity_id2 = "sensor.blu" - entity_id3 = "switch.included" - pointA = dt_util.utcnow() - pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES) - pointC = pointB + timedelta(minutes=logbook.GROUP_BY_MINUTES) - entity_attr_cache = logbook.EntityAttributeCache(self.hass) - - event_alexa = MockLazyEventPartialState( - EVENT_ALEXA_SMART_HOME, - {"request": {"namespace": "Alexa.Discovery", "name": "Discover"}}, - ) - - eventA = self.create_state_changed_event(pointA, entity_id, 10) - eventB = self.create_state_changed_event(pointB, entity_id2, 20) - eventC = self.create_state_changed_event(pointC, entity_id3, 30) - - config = logbook.CONFIG_SCHEMA( - { - ha.DOMAIN: {}, - logbook.DOMAIN: { - CONF_INCLUDE: { - CONF_DOMAINS: ["homeassistant", "sensor", "alexa"], - CONF_ENTITY_GLOBS: ["*.included"], - } - }, - } - ) - entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN]) - events = [ - e - for e in ( - MockLazyEventPartialState(EVENT_HOMEASSISTANT_START), - event_alexa, - eventA, - eventB, - eventC, - ) - if logbook._keep_event(self.hass, e, entities_filter) - ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) - - assert len(entries) == 4 - self.assert_entry( - entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN - ) - self.assert_entry(entries[1], name="Amazon Alexa", domain="alexa") - self.assert_entry( - entries[2], pointB, "blu", domain="sensor", entity_id=entity_id2 - ) - self.assert_entry( - entries[3], pointC, "included", domain="switch", entity_id=entity_id3 - ) - - def test_include_exclude_events(self): - """Test if events are filtered if include and exclude is configured.""" - entity_id = "switch.bla" - entity_id2 = "sensor.blu" - entity_id3 = "sensor.bli" - pointA = dt_util.utcnow() - pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES) - entity_attr_cache = logbook.EntityAttributeCache(self.hass) - - eventA1 = self.create_state_changed_event(pointA, entity_id, 10) - eventA2 = self.create_state_changed_event(pointA, entity_id2, 10) - eventA3 = self.create_state_changed_event(pointA, entity_id3, 10) - eventB1 = self.create_state_changed_event(pointB, entity_id, 20) - eventB2 = self.create_state_changed_event(pointB, entity_id2, 20) - - config = logbook.CONFIG_SCHEMA( - { - ha.DOMAIN: {}, - logbook.DOMAIN: { - CONF_INCLUDE: { - CONF_DOMAINS: ["sensor", "homeassistant"], - CONF_ENTITIES: ["switch.bla"], - }, - CONF_EXCLUDE: { - CONF_DOMAINS: ["switch"], - CONF_ENTITIES: ["sensor.bli"], - }, - }, - } - ) - entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN]) - events = [ - e - for e in ( - MockLazyEventPartialState(EVENT_HOMEASSISTANT_START), - eventA1, - eventA2, - eventA3, - eventB1, - eventB2, - ) - if logbook._keep_event(self.hass, e, entities_filter) - ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) - - assert len(entries) == 5 - self.assert_entry( - entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN - ) - self.assert_entry( - entries[1], pointA, "bla", domain="switch", entity_id=entity_id - ) - self.assert_entry( - entries[2], pointA, "blu", domain="sensor", entity_id=entity_id2 - ) - self.assert_entry( - entries[3], pointB, "bla", domain="switch", entity_id=entity_id - ) - self.assert_entry( - entries[4], pointB, "blu", domain="sensor", entity_id=entity_id2 - ) - - def test_include_exclude_events_with_glob_filters(self): - """Test if events are filtered if include and exclude is configured.""" - entity_id = "switch.bla" - entity_id2 = "sensor.blu" - entity_id3 = "sensor.bli" - entity_id4 = "light.included" - entity_id5 = "switch.included" - entity_id6 = "sensor.excluded" - pointA = dt_util.utcnow() - pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES) - pointC = pointB + timedelta(minutes=logbook.GROUP_BY_MINUTES) - entity_attr_cache = logbook.EntityAttributeCache(self.hass) - - eventA1 = self.create_state_changed_event(pointA, entity_id, 10) - eventA2 = self.create_state_changed_event(pointA, entity_id2, 10) - eventA3 = self.create_state_changed_event(pointA, entity_id3, 10) - eventB1 = self.create_state_changed_event(pointB, entity_id, 20) - eventB2 = self.create_state_changed_event(pointB, entity_id2, 20) - eventC1 = self.create_state_changed_event(pointC, entity_id4, 30) - eventC2 = self.create_state_changed_event(pointC, entity_id5, 30) - eventC3 = self.create_state_changed_event(pointC, entity_id6, 30) - - config = logbook.CONFIG_SCHEMA( - { - ha.DOMAIN: {}, - logbook.DOMAIN: { - CONF_INCLUDE: { - CONF_DOMAINS: ["sensor", "homeassistant"], - CONF_ENTITIES: ["switch.bla"], - CONF_ENTITY_GLOBS: ["*.included"], - }, - CONF_EXCLUDE: { - CONF_DOMAINS: ["switch"], - CONF_ENTITY_GLOBS: ["*.excluded"], - CONF_ENTITIES: ["sensor.bli"], - }, - }, - } - ) - entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN]) - events = [ - e - for e in ( - MockLazyEventPartialState(EVENT_HOMEASSISTANT_START), - eventA1, - eventA2, - eventA3, - eventB1, - eventB2, - eventC1, - eventC2, - eventC3, - ) - if logbook._keep_event(self.hass, e, entities_filter) - ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) - - assert len(entries) == 6 - self.assert_entry( - entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN - ) - self.assert_entry( - entries[1], pointA, "bla", domain="switch", entity_id=entity_id - ) - self.assert_entry( - entries[2], pointA, "blu", domain="sensor", entity_id=entity_id2 - ) - self.assert_entry( - entries[3], pointB, "bla", domain="switch", entity_id=entity_id - ) - self.assert_entry( - entries[4], pointB, "blu", domain="sensor", entity_id=entity_id2 - ) - self.assert_entry( - entries[5], pointC, "included", domain="light", entity_id=entity_id4 - ) - def test_home_assistant_start_stop_grouped(self): """Test if HA start and stop events are grouped. @@ -1277,20 +868,7 @@ class TestComponentLogbook(unittest.TestCase): self, entry, when=None, name=None, message=None, domain=None, entity_id=None ): """Assert an entry is what is expected.""" - if when: - assert when.isoformat() == entry["when"] - - if name: - assert name == entry["name"] - - if message: - assert message == entry["message"] - - if domain: - assert domain == entry["domain"] - - if entity_id: - assert entity_id == entry["entity_id"] + return _assert_entry(entry, when, name, message, domain, entity_id) def create_state_changed_event( self, @@ -2287,22 +1865,10 @@ async def test_icon_and_state(hass, hass_client): ) hass.states.async_set("light.kitchen", STATE_OFF, {"icon": "mdi:chemical-weapon"}) - 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) + await _async_commit_and_wait(hass) client = await hass_client() - - # Today time 00:00:00 - start = dt_util.utcnow().date() - start_date = datetime(start.year, start.month, start.day) - - # Test today entries without filters - response = await client.get(f"/api/logbook/{start_date.isoformat()}") - assert response.status == 200 - response_json = await response.json() + response_json = await _async_fetch_logbook(client) assert len(response_json) == 3 assert response_json[0]["domain"] == "homeassistant" @@ -2316,6 +1882,390 @@ async def test_icon_and_state(hass, hass_client): assert response_json[2]["state"] == STATE_OFF +async def test_exclude_events_domain(hass, hass_client): + """Test if events are filtered if domain is excluded in config.""" + entity_id = "switch.bla" + entity_id2 = "sensor.blu" + + config = logbook.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + logbook.DOMAIN: {CONF_EXCLUDE: {CONF_DOMAINS: ["switch", "alexa"]}}, + } + ) + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "logbook", config) + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + hass.states.async_set(entity_id, None) + hass.states.async_set(entity_id, 10) + hass.states.async_set(entity_id2, None) + hass.states.async_set(entity_id2, 20) + + await _async_commit_and_wait(hass) + + client = await hass_client() + entries = await _async_fetch_logbook(client) + + assert len(entries) == 2 + _assert_entry( + entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN + ) + _assert_entry(entries[1], name="blu", domain="sensor", entity_id=entity_id2) + + +async def test_exclude_events_domain_glob(hass, hass_client): + """Test if events are filtered if domain or glob is excluded in config.""" + entity_id = "switch.bla" + entity_id2 = "sensor.blu" + entity_id3 = "sensor.excluded" + + config = logbook.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + logbook.DOMAIN: { + CONF_EXCLUDE: { + CONF_DOMAINS: ["switch", "alexa"], + CONF_ENTITY_GLOBS: "*.excluded", + } + }, + } + ) + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "logbook", config) + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + hass.states.async_set(entity_id, None) + hass.states.async_set(entity_id, 10) + hass.states.async_set(entity_id2, None) + hass.states.async_set(entity_id2, 20) + hass.states.async_set(entity_id3, None) + hass.states.async_set(entity_id3, 30) + + await _async_commit_and_wait(hass) + client = await hass_client() + entries = await _async_fetch_logbook(client) + + assert len(entries) == 2 + _assert_entry( + entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN + ) + _assert_entry(entries[1], name="blu", domain="sensor", entity_id=entity_id2) + + +async def test_include_events_entity(hass, hass_client): + """Test if events are filtered if entity is included in config.""" + entity_id = "sensor.bla" + entity_id2 = "sensor.blu" + + config = logbook.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + logbook.DOMAIN: { + CONF_INCLUDE: { + CONF_DOMAINS: ["homeassistant"], + CONF_ENTITIES: [entity_id2], + } + }, + } + ) + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "logbook", config) + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + hass.states.async_set(entity_id, None) + hass.states.async_set(entity_id, 10) + hass.states.async_set(entity_id2, None) + hass.states.async_set(entity_id2, 20) + + await _async_commit_and_wait(hass) + client = await hass_client() + entries = await _async_fetch_logbook(client) + + assert len(entries) == 2 + _assert_entry( + entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN + ) + _assert_entry(entries[1], name="blu", domain="sensor", entity_id=entity_id2) + + +async def test_exclude_events_entity(hass, hass_client): + """Test if events are filtered if entity is excluded in config.""" + entity_id = "sensor.bla" + entity_id2 = "sensor.blu" + + config = logbook.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + logbook.DOMAIN: {CONF_EXCLUDE: {CONF_ENTITIES: [entity_id]}}, + } + ) + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "logbook", config) + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + hass.states.async_set(entity_id, None) + hass.states.async_set(entity_id, 10) + hass.states.async_set(entity_id2, None) + hass.states.async_set(entity_id2, 20) + + await _async_commit_and_wait(hass) + client = await hass_client() + entries = await _async_fetch_logbook(client) + assert len(entries) == 2 + _assert_entry( + entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN + ) + _assert_entry(entries[1], name="blu", domain="sensor", entity_id=entity_id2) + + +async def test_include_events_domain(hass, hass_client): + """Test if events are filtered if domain is included in config.""" + assert await async_setup_component(hass, "alexa", {}) + entity_id = "switch.bla" + entity_id2 = "sensor.blu" + config = logbook.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + logbook.DOMAIN: { + CONF_INCLUDE: {CONF_DOMAINS: ["homeassistant", "sensor", "alexa"]} + }, + } + ) + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "logbook", config) + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + hass.bus.async_fire( + EVENT_ALEXA_SMART_HOME, + {"request": {"namespace": "Alexa.Discovery", "name": "Discover"}}, + ) + hass.states.async_set(entity_id, None) + hass.states.async_set(entity_id, 10) + hass.states.async_set(entity_id2, None) + hass.states.async_set(entity_id2, 20) + + await _async_commit_and_wait(hass) + client = await hass_client() + entries = await _async_fetch_logbook(client) + + assert len(entries) == 3 + _assert_entry( + entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN + ) + _assert_entry(entries[1], name="Amazon Alexa", domain="alexa") + _assert_entry(entries[2], name="blu", domain="sensor", entity_id=entity_id2) + + +async def test_include_events_domain_glob(hass, hass_client): + """Test if events are filtered if domain or glob is included in config.""" + assert await async_setup_component(hass, "alexa", {}) + entity_id = "switch.bla" + entity_id2 = "sensor.blu" + entity_id3 = "switch.included" + config = logbook.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + logbook.DOMAIN: { + CONF_INCLUDE: { + CONF_DOMAINS: ["homeassistant", "sensor", "alexa"], + CONF_ENTITY_GLOBS: ["*.included"], + } + }, + } + ) + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "logbook", config) + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + hass.bus.async_fire( + EVENT_ALEXA_SMART_HOME, + {"request": {"namespace": "Alexa.Discovery", "name": "Discover"}}, + ) + hass.states.async_set(entity_id, None) + hass.states.async_set(entity_id, 10) + hass.states.async_set(entity_id2, None) + hass.states.async_set(entity_id2, 20) + hass.states.async_set(entity_id3, None) + hass.states.async_set(entity_id3, 30) + + await _async_commit_and_wait(hass) + client = await hass_client() + entries = await _async_fetch_logbook(client) + + assert len(entries) == 4 + _assert_entry( + entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN + ) + _assert_entry(entries[1], name="Amazon Alexa", domain="alexa") + _assert_entry(entries[2], name="blu", domain="sensor", entity_id=entity_id2) + _assert_entry(entries[3], name="included", domain="switch", entity_id=entity_id3) + + +async def test_include_exclude_events(hass, hass_client): + """Test if events are filtered if include and exclude is configured.""" + entity_id = "switch.bla" + entity_id2 = "sensor.blu" + entity_id3 = "sensor.bli" + entity_id4 = "sensor.keep" + + config = logbook.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + logbook.DOMAIN: { + CONF_INCLUDE: { + CONF_DOMAINS: ["sensor", "homeassistant"], + CONF_ENTITIES: ["switch.bla"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["switch"], + CONF_ENTITIES: ["sensor.bli"], + }, + }, + } + ) + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "logbook", config) + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + hass.states.async_set(entity_id, None) + hass.states.async_set(entity_id, 10) + hass.states.async_set(entity_id2, None) + hass.states.async_set(entity_id2, 10) + hass.states.async_set(entity_id3, None) + hass.states.async_set(entity_id3, 10) + hass.states.async_set(entity_id, 20) + hass.states.async_set(entity_id2, 20) + hass.states.async_set(entity_id4, None) + hass.states.async_set(entity_id4, 10) + + await _async_commit_and_wait(hass) + client = await hass_client() + entries = await _async_fetch_logbook(client) + + assert len(entries) == 3 + _assert_entry( + entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN + ) + _assert_entry(entries[1], name="blu", domain="sensor", entity_id=entity_id2) + _assert_entry(entries[2], name="keep", domain="sensor", entity_id=entity_id4) + + +async def test_include_exclude_events_with_glob_filters(hass, hass_client): + """Test if events are filtered if include and exclude is configured.""" + entity_id = "switch.bla" + entity_id2 = "sensor.blu" + entity_id3 = "sensor.bli" + entity_id4 = "light.included" + entity_id5 = "switch.included" + entity_id6 = "sensor.excluded" + config = logbook.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + logbook.DOMAIN: { + CONF_INCLUDE: { + CONF_DOMAINS: ["sensor", "homeassistant"], + CONF_ENTITIES: ["switch.bla"], + CONF_ENTITY_GLOBS: ["*.included"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["switch"], + CONF_ENTITY_GLOBS: ["*.excluded"], + CONF_ENTITIES: ["sensor.bli"], + }, + }, + } + ) + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "logbook", config) + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + hass.states.async_set(entity_id, None) + hass.states.async_set(entity_id, 10) + hass.states.async_set(entity_id2, None) + hass.states.async_set(entity_id2, 10) + hass.states.async_set(entity_id3, None) + hass.states.async_set(entity_id3, 10) + hass.states.async_set(entity_id, 20) + hass.states.async_set(entity_id2, 20) + hass.states.async_set(entity_id4, None) + hass.states.async_set(entity_id4, 30) + hass.states.async_set(entity_id5, None) + hass.states.async_set(entity_id5, 30) + hass.states.async_set(entity_id6, None) + hass.states.async_set(entity_id6, 30) + + await _async_commit_and_wait(hass) + client = await hass_client() + entries = await _async_fetch_logbook(client) + + assert len(entries) == 3 + _assert_entry( + entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN + ) + _assert_entry(entries[1], name="blu", domain="sensor", entity_id=entity_id2) + _assert_entry(entries[2], name="included", domain="light", entity_id=entity_id4) + + +async def _async_fetch_logbook(client): + + # Today time 00:00:00 + start = dt_util.utcnow().date() + start_date = datetime(start.year, start.month, start.day) - timedelta(hours=24) + + # Test today entries without filters + end_time = start + timedelta(hours=48) + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}" + ) + assert response.status == 200 + return await response.json() + + +async def _async_commit_and_wait(hass): + 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) + await hass.async_block_till_done() + + +def _assert_entry( + entry, when=None, name=None, message=None, domain=None, entity_id=None +): + """Assert an entry is what is expected.""" + if when: + assert when.isoformat() == entry["when"] + + if name: + assert name == entry["name"] + + if message: + assert message == entry["message"] + + if domain: + assert domain == entry["domain"] + + if entity_id: + assert entity_id == entry["entity_id"] + + class MockLazyEventPartialState(ha.Event): """Minimal mock of a Lazy event.""" From ceded35a82f3a33c2a93697c006137ba0373e7e7 Mon Sep 17 00:00:00 2001 From: RichieFrame <33644730+RichieFrame@users.noreply.github.com> Date: Mon, 28 Sep 2020 09:03:21 -0500 Subject: [PATCH 410/514] Remove degree from Kelvin unit (#40574) Kelvin temperature unit does not use the degree symbol --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4d5f4db665c..0e69f8c5b13 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -389,7 +389,7 @@ CURRENCY_CENT = "¢" # Temperature units TEMP_CELSIUS = f"{DEGREE}C" TEMP_FAHRENHEIT = f"{DEGREE}F" -TEMP_KELVIN = f"{DEGREE}K" +TEMP_KELVIN = "K" # Time units TIME_MICROSECONDS = "μs" From 68d75a879b2a0e8a483cd89197e9a79ba7e8c5b4 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Mon, 28 Sep 2020 07:04:08 -0700 Subject: [PATCH 411/514] Fix Android TV 'async_get_media_image' (#40672) --- .../components/androidtv/media_player.py | 11 ++++++++++- tests/components/androidtv/test_media_player.py | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 1ea20dbeca5..e64581f9a05 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -502,14 +502,23 @@ class ADBDevice(MediaPlayerEntity): return self._unique_id @adb_decorator() + async def _adb_screencap(self): + """Take a screen capture from the device.""" + return await self.aftv.adb_screencap() + async def async_get_media_image(self): """Fetch current playing image.""" if not self._screencap or self.state in [STATE_OFF, None] or not self.available: return None, None - media_data = await self.aftv.adb_screencap() + media_data = await self._adb_screencap() if media_data: return media_data, "image/png" + + # If an exception occurred and the device is no longer available, write the state + if not self.available: + self.async_write_ha_state() + return None, None @adb_decorator() diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 456a9313091..c497a9daa48 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -1064,6 +1064,21 @@ async def test_get_image(hass, hass_ws_client): assert msg["result"]["content_type"] == "image/png" assert msg["result"]["content"] == base64.b64encode(b"image").decode("utf-8") + with patch( + "androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap", + side_effect=RuntimeError, + ): + await client.send_json( + {"id": 6, "type": "media_player_thumbnail", "entity_id": entity_id} + ) + + msg = await client.receive_json() + + # The device is unavailable, but getting the media image did not cause an exception + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_UNAVAILABLE + async def _test_service( hass, From eda68f127c93446598d0927115c6b2e265d44959 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 28 Sep 2020 16:07:17 +0200 Subject: [PATCH 412/514] Add rpi_power during onboarding on RPi (#40076) --- .../components/onboarding/manifest.json | 13 ++- homeassistant/components/onboarding/views.py | 8 ++ .../components/rpi_power/config_flow.py | 33 ++++-- tests/components/onboarding/test_views.py | 100 ++++++++++++++++++ .../components/rpi_power/test_config_flow.py | 25 ++++- 5 files changed, 168 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json index 81e88e99edb..e2fb8e084b8 100644 --- a/homeassistant/components/onboarding/manifest.json +++ b/homeassistant/components/onboarding/manifest.json @@ -2,7 +2,16 @@ "domain": "onboarding", "name": "Home Assistant Onboarding", "documentation": "https://www.home-assistant.io/integrations/onboarding", - "dependencies": ["auth", "http", "person"], - "codeowners": ["@home-assistant/core"], + "after_dependencies": [ + "hassio" + ], + "dependencies": [ + "auth", + "http", + "person" + ], + "codeowners": [ + "@home-assistant/core" + ], "quality_scale": "internal" } diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index a2a4fb15fd7..0faf099b9bf 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -159,6 +159,14 @@ class CoreConfigOnboardingView(_BaseOnboardingView): "met", context={"source": "onboarding"} ) + if ( + hass.components.hassio.is_hassio() + and "raspberrypi" in hass.components.hassio.get_core_info()["machine"] + ): + await hass.config_entries.flow.async_init( + "rpi_power", context={"source": "onboarding"} + ) + return self.json({}) diff --git a/homeassistant/components/rpi_power/config_flow.py b/homeassistant/components/rpi_power/config_flow.py index 6112bddb7d5..9924ebf0440 100644 --- a/homeassistant/components/rpi_power/config_flow.py +++ b/homeassistant/components/rpi_power/config_flow.py @@ -1,9 +1,11 @@ """Config flow for Raspberry Pi Power Supply Checker.""" +from typing import Any, Dict, Optional + from rpi_bad_power import new_under_voltage from homeassistant import config_entries from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_entry_flow +from homeassistant.helpers.config_entry_flow import DiscoveryFlowHandler from .const import DOMAIN @@ -14,9 +16,26 @@ async def _async_supported(hass: HomeAssistant) -> bool: return under_voltage is not None -config_entry_flow.register_discovery_flow( - DOMAIN, - "Raspberry Pi Power Supply Checker", - _async_supported, - config_entries.CONN_CLASS_LOCAL_POLL, -) +class RPiPowerFlow(DiscoveryFlowHandler, domain=DOMAIN): + """Discovery flow handler.""" + + VERSION = 1 + + def __init__(self) -> None: + """Set up config flow.""" + super().__init__( + DOMAIN, + "Raspberry Pi Power Supply Checker", + _async_supported, + config_entries.CONN_CLASS_LOCAL_POLL, + ) + + async def async_step_onboarding( + self, data: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Handle a flow initialized by onboarding.""" + has_devices = await self._discovery_function(self.hass) + + if not has_devices: + return self.async_abort(reason="no_devices_found") + return self.async_create_entry(title=self._title, data={}) diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 0d425642622..a1b857a52ce 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -1,5 +1,6 @@ """Test the onboarding views.""" import asyncio +import os import pytest @@ -29,6 +30,57 @@ def auth_active(hass): ) +@pytest.fixture(name="rpi") +async def rpi_fixture(hass, aioclient_mock, mock_supervisor): + """Mock core info with rpi.""" + aioclient_mock.get( + "http://127.0.0.1/core/info", + json={ + "result": "ok", + "data": {"version_latest": "1.0.0", "machine": "raspberrypi3"}, + }, + ) + assert await async_setup_component(hass, "hassio", {}) + await hass.async_block_till_done() + + +@pytest.fixture(name="no_rpi") +async def no_rpi_fixture(hass, aioclient_mock, mock_supervisor): + """Mock core info with rpi.""" + aioclient_mock.get( + "http://127.0.0.1/core/info", + json={ + "result": "ok", + "data": {"version_latest": "1.0.0", "machine": "odroid-n2"}, + }, + ) + assert await async_setup_component(hass, "hassio", {}) + await hass.async_block_till_done() + + +@pytest.fixture(name="mock_supervisor") +async def mock_supervisor_fixture(hass, aioclient_mock): + """Mock supervisor.""" + aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) + aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) + with patch.dict(os.environ, {"HASSIO": "127.0.0.1"}), patch( + "homeassistant.components.hassio.HassIO.is_connected", + return_value=True, + ), patch( + "homeassistant.components.hassio.HassIO.get_info", + return_value={}, + ), patch( + "homeassistant.components.hassio.HassIO.get_host_info", + return_value={}, + ), patch( + "homeassistant.components.hassio.HassIO.get_ingress_panels", + return_value={"panels": {}}, + ), patch.dict( + os.environ, {"HASSIO_TOKEN": "123456"} + ): + yield + + async def test_onboarding_progress(hass, hass_storage, aiohttp_client): """Test fetching progress.""" mock_storage(hass_storage, {"done": ["hello"]}) @@ -277,3 +329,51 @@ async def test_onboarding_core_sets_up_met(hass, hass_storage, hass_client): await hass.async_block_till_done() assert len(hass.states.async_entity_ids("weather")) == 1 + + +async def test_onboarding_core_sets_up_rpi_power( + hass, hass_storage, hass_client, aioclient_mock, rpi +): + """Test that the core step sets up rpi_power on RPi.""" + mock_storage(hass_storage, {"done": [const.STEP_USER]}) + await async_setup_component(hass, "persistent_notification", {}) + + assert await async_setup_component(hass, "onboarding", {}) + + client = await hass_client() + + with patch( + "homeassistant.components.rpi_power.config_flow.new_under_voltage" + ), patch("homeassistant.components.rpi_power.binary_sensor.new_under_voltage"): + resp = await client.post("/api/onboarding/core_config") + + assert resp.status == 200 + + await hass.async_block_till_done() + + rpi_power_state = hass.states.get("binary_sensor.rpi_power_status") + assert rpi_power_state + + +async def test_onboarding_core_no_rpi_power( + hass, hass_storage, hass_client, aioclient_mock, no_rpi +): + """Test that the core step do not set up rpi_power on non RPi.""" + mock_storage(hass_storage, {"done": [const.STEP_USER]}) + await async_setup_component(hass, "persistent_notification", {}) + + assert await async_setup_component(hass, "onboarding", {}) + + client = await hass_client() + + with patch( + "homeassistant.components.rpi_power.config_flow.new_under_voltage" + ), patch("homeassistant.components.rpi_power.binary_sensor.new_under_voltage"): + resp = await client.post("/api/onboarding/core_config") + + assert resp.status == 200 + + await hass.async_block_till_done() + + rpi_power_state = hass.states.get("binary_sensor.rpi_power_status") + assert not rpi_power_state diff --git a/tests/components/rpi_power/test_config_flow.py b/tests/components/rpi_power/test_config_flow.py index 70b384d6b91..090b6a6a793 100644 --- a/tests/components/rpi_power/test_config_flow.py +++ b/tests/components/rpi_power/test_config_flow.py @@ -14,7 +14,7 @@ from tests.common import patch MODULE = "homeassistant.components.rpi_power.config_flow.new_under_voltage" -async def test_setup(hass: HomeAssistant): +async def test_setup(hass: HomeAssistant) -> None: """Test setting up manually.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -29,7 +29,7 @@ async def test_setup(hass: HomeAssistant): assert result["type"] == RESULT_TYPE_CREATE_ENTRY -async def test_not_supported(hass: HomeAssistant): +async def test_not_supported(hass: HomeAssistant) -> None: """Test setting up on not supported system.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -40,3 +40,24 @@ async def test_not_supported(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "no_devices_found" + + +async def test_onboarding(hass: HomeAssistant) -> None: + """Test setting up via onboarding.""" + with patch(MODULE, return_value=MagicMock()): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "onboarding"}, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + + +async def test_onboarding_not_supported(hass: HomeAssistant) -> None: + """Test setting up via onboarding with unsupported system.""" + with patch(MODULE, return_value=None): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "onboarding"}, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "no_devices_found" From 3596eb39f23eea8114210935340959a8ccc4ca28 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 28 Sep 2020 09:14:54 -0500 Subject: [PATCH 413/514] Add support to reauthorize expired Plex tokens (#40469) --- homeassistant/components/plex/__init__.py | 27 +++++++++-- homeassistant/components/plex/config_flow.py | 42 +++++++++++----- homeassistant/components/plex/server.py | 11 ++++- homeassistant/components/plex/strings.json | 3 +- tests/components/plex/test_config_flow.py | 51 ++++++++++++++++++++ 5 files changed, 116 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 4e5abad4f79..fff6dea3bb4 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -14,8 +14,10 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ) +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( ATTR_ENTITY_ID, + CONF_SOURCE, CONF_URL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, @@ -75,7 +77,11 @@ async def async_setup_entry(hass, entry): hass.config_entries.async_update_entry(entry, options=options) plex_server = PlexServer( - hass, server_config, entry.data[CONF_SERVER_IDENTIFIER], entry.options + hass, + server_config, + entry.data[CONF_SERVER_IDENTIFIER], + entry.options, + entry.entry_id, ) try: await hass.async_add_executor_job(plex_server.connect) @@ -95,9 +101,21 @@ async def async_setup_entry(hass, entry): error, ) raise ConfigEntryNotReady from error + except plexapi.exceptions.Unauthorized: + hass.async_create_task( + hass.config_entries.flow.async_init( + PLEX_DOMAIN, + context={CONF_SOURCE: SOURCE_REAUTH}, + data={**entry.data, "config_entry_id": entry.entry_id}, + ) + ) + _LOGGER.error( + "Token not accepted, please reauthenticate Plex server '%s'", + entry.data[CONF_SERVER], + ) + return False except ( plexapi.exceptions.BadRequest, - plexapi.exceptions.Unauthorized, plexapi.exceptions.NotFound, ) as error: _LOGGER.error( @@ -207,7 +225,10 @@ async def async_unload_entry(hass, entry): async def async_options_updated(hass, entry): """Triggered by config entry options updates.""" server_id = entry.data[CONF_SERVER_IDENTIFIER] - hass.data[PLEX_DOMAIN][SERVERS][server_id].options = entry.options + + # Guard incomplete setup during reauth flows + if server_id in hass.data[PLEX_DOMAIN][SERVERS]: + hass.data[PLEX_DOMAIN][SERVERS][server_id].options = entry.options def play_on_sonos(hass, service_call): diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index ffadba63d3a..b2bf856402e 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_CLIENT_ID, CONF_HOST, CONF_PORT, + CONF_SOURCE, CONF_SSL, CONF_TOKEN, CONF_URL, @@ -70,7 +71,7 @@ async def async_discover(hass): for server_data in gdm.entries: await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + context={CONF_SOURCE: config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=server_data, ) @@ -95,6 +96,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.token = None self.client_id = None self._manual = False + self._entry_id = None async def async_step_user( self, user_input=None, errors=None @@ -209,10 +211,6 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user(errors=errors) server_id = plex_server.machine_identifier - - await self.async_set_unique_id(server_id) - self._abort_if_unique_id_configured() - url = plex_server.url_in_use token = server_config.get(CONF_TOKEN) @@ -226,16 +224,28 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL ) + data = { + CONF_SERVER: plex_server.friendly_name, + CONF_SERVER_IDENTIFIER: server_id, + PLEX_SERVER_CONFIG: entry_config, + } + + await self.async_set_unique_id(server_id) + if ( + self.context[CONF_SOURCE] # pylint: disable=no-member + == config_entries.SOURCE_REAUTH + ): + entry = self.hass.config_entries.async_get_entry(self._entry_id) + self.hass.config_entries.async_update_entry(entry, data=data) + _LOGGER.debug("Updated config entry for %s", plex_server.friendly_name) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + + self._abort_if_unique_id_configured() + _LOGGER.debug("Valid config created for %s", plex_server.friendly_name) - return self.async_create_entry( - title=plex_server.friendly_name, - data={ - CONF_SERVER: plex_server.friendly_name, - CONF_SERVER_IDENTIFIER: server_id, - PLEX_SERVER_CONFIG: entry_config, - }, - ) + return self.async_create_entry(title=plex_server.friendly_name, data=data) async def async_step_select_server(self, user_input=None): """Use selected Plex server.""" @@ -316,6 +326,12 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): server_config = {CONF_TOKEN: self.token} return await self.async_step_server_validate(server_config) + async def async_step_reauth(self, data): + """Handle a reauthorization flow request.""" + self.current_login = dict(data) + self._entry_id = self.current_login.pop("config_entry_id") + return await self.async_step_user() + class PlexOptionsFlowHandler(config_entries.OptionsFlow): """Handle Plex options.""" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index f8706eadf22..a5ac287328e 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -62,9 +62,12 @@ plexapi.X_PLEX_VERSION = X_PLEX_VERSION class PlexServer: """Manages a single Plex server connection.""" - def __init__(self, hass, server_config, known_server_id=None, options=None): + def __init__( + self, hass, server_config, known_server_id=None, options=None, entry_id=None + ): """Initialize a Plex server instance.""" self.hass = hass + self.entry_id = entry_id self._plex_account = None self._plex_server = None self._created_clients = set() @@ -270,6 +273,12 @@ class PlexServer: devices, sessions, plextv_clients = await self.hass.async_add_executor_job( self._fetch_platform_data ) + except plexapi.exceptions.Unauthorized: + _LOGGER.debug( + "Token has expired for '%s', reloading integration", self.friendly_name + ) + await self.hass.config_entries.async_reload(self.entry_id) + return except ( plexapi.exceptions.BadRequest, requests.exceptions.RequestException, diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index 2f50e2d3090..1f9226ff776 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -41,8 +41,9 @@ "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", + "reauth_successful": "Successfully reauthenticated", "token_request_timeout": "Timed out obtaining token", - "unknown": "Failed for unknown reason" + "unknown": "[%key:common::config_flow::error::unknown%]" } }, "options": { diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 6b64b2f8571..476c342f176 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -24,6 +24,7 @@ from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import ( ENTRY_STATE_LOADED, SOURCE_INTEGRATION_DISCOVERY, + SOURCE_REAUTH, ) from homeassistant.const import ( CONF_HOST, @@ -723,3 +724,53 @@ async def test_integration_discovery(hass): == mock_gdm.entries[0]["data"]["Resource-Identifier"] ) assert flow["step_id"] == "user" + + +async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket): + """Test setup and reauthorization of a Plex token.""" + await async_process_ha_core_config( + hass, + {"internal_url": "http://example.local:8123"}, + ) + + assert entry.state == ENTRY_STATE_LOADED + + with patch.object( + mock_plex_server, "clients", side_effect=plexapi.exceptions.Unauthorized + ), patch("plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized): + trigger_plex_update(mock_websocket) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state != ENTRY_STATE_LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["source"] == SOURCE_REAUTH + + flow_id = flows[0]["flow_id"] + + with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch( + "plexapi.server.PlexServer", return_value=mock_plex_server + ), patch("plexauth.PlexAuth.initiate_auth"), patch( + "plexauth.PlexAuth.token", return_value="BRAND_NEW_TOKEN" + ): + result = await hass.config_entries.flow.async_configure(flow_id, user_input={}) + assert result["type"] == "external" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "abort" + assert result["reason"] == "reauth_successful" + assert result["flow_id"] == flow_id + + assert len(hass.config_entries.flow.async_progress()) == 0 + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + assert entry.state == ENTRY_STATE_LOADED + assert entry.data[CONF_SERVER] == mock_plex_server.friendlyName + assert entry.data[CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier + assert entry.data[PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl + assert entry.data[PLEX_SERVER_CONFIG][CONF_TOKEN] == "BRAND_NEW_TOKEN" From e564af0b5bc29d3dd5a220c7edfc669a64665760 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Sep 2020 10:35:12 -0500 Subject: [PATCH 414/514] Improve performance of accessing template state (#40323) Co-authored-by: Paulus Schoutsen --- homeassistant/core.py | 9 +- homeassistant/helpers/template.py | 160 ++++++++++++++++++++++-------- tests/helpers/test_template.py | 99 ++++++++++++++++++ 3 files changed, 221 insertions(+), 47 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index bfb88ab6bfd..eb584b22b49 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -759,6 +759,7 @@ class State: last_updated: last time this object was updated. context: Context in which it was created domain: Domain of this state. + object_id: Object id of this state. """ __slots__ = [ @@ -769,6 +770,7 @@ class State: "last_updated", "context", "domain", + "object_id", ] def __init__( @@ -802,12 +804,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() - self.domain = split_entity_id(self.entity_id)[0] - - @property - def object_id(self) -> str: - """Object id of this state.""" - return split_entity_id(self.entity_id)[1] + self.domain, self.object_id = split_entity_id(self.entity_id) @property def name(self) -> str: diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 721c1407f37..6261f7b2257 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -61,6 +61,17 @@ _RESERVED_NAMES = {"contextfunction", "evalcontextfunction", "environmentfunctio _GROUP_DOMAIN_PREFIX = "group." +_COLLECTABLE_STATE_ATTRIBUTES = { + "state", + "attributes", + "last_changed", + "last_updated", + "context", + "domain", + "object_id", + "name", +} + @bind_hass def attach(hass: HomeAssistantType, obj: Any) -> None: @@ -477,9 +488,7 @@ class AllStates: def __getattr__(self, name): """Return the domain state.""" if "." in name: - if not valid_entity_id(name): - raise TemplateError(f"Invalid entity ID '{name}'") - return _get_state(self._hass, name) + return _get_state_if_valid(self._hass, name) if name in _RESERVED_NAMES: return None @@ -489,6 +498,10 @@ class AllStates: return DomainStates(self._hass, name) + # Jinja will try __getitem__ first and it avoids the need + # to call is_safe_attribute + __getitem__ = __getattr__ + def _collect_all(self) -> None: render_info = self._hass.data.get(_RENDER_INFO) if render_info is not None: @@ -529,10 +542,11 @@ class DomainStates: def __getattr__(self, name): """Return the states.""" - entity_id = f"{self._domain}.{name}" - if not valid_entity_id(entity_id): - raise TemplateError(f"Invalid entity ID '{entity_id}'") - return _get_state(self._hass, entity_id) + return _get_state_if_valid(self._hass, f"{self._domain}.{name}") + + # Jinja will try __getitem__ first and it avoids the need + # to call is_safe_attribute + __getitem__ = __getattr__ def _collect_domain(self) -> None: entity_collect = self._hass.data.get(_RENDER_INFO) @@ -571,46 +585,96 @@ class TemplateState(State): self._hass = hass self._state = state - def _access_state(self): - state = object.__getattribute__(self, "_state") - hass = object.__getattribute__(self, "_hass") - _collect_state(hass, state.entity_id) - return state + def _collect_state(self): + if _RENDER_INFO in self._hass.data: + self._hass.data[_RENDER_INFO].entities.add(self._state.entity_id) + + # Jinja will try __getitem__ first and it avoids the need + # to call is_safe_attribute + def __getitem__(self, item): + """Return a property as an attribute for jinja.""" + if item in _COLLECTABLE_STATE_ATTRIBUTES: + # _collect_state inlined here for performance + if _RENDER_INFO in self._hass.data: + self._hass.data[_RENDER_INFO].entities.add(self._state.entity_id) + return getattr(self._state, item) + if item == "entity_id": + return self._state.entity_id + if item == "state_with_unit": + return self.state_with_unit + raise KeyError + + @property + def entity_id(self): + """Wrap State.entity_id. + + Intentionally does not collect state + """ + return self._state.entity_id + + @property + def state(self): + """Wrap State.state.""" + self._collect_state() + return self._state.state + + @property + def attributes(self): + """Wrap State.attributes.""" + self._collect_state() + return self._state.attributes + + @property + def last_changed(self): + """Wrap State.last_changed.""" + self._collect_state() + return self._state.last_changed + + @property + def last_updated(self): + """Wrap State.last_updated.""" + self._collect_state() + return self._state.last_updated + + @property + def context(self): + """Wrap State.context.""" + self._collect_state() + return self._state.context + + @property + def domain(self): + """Wrap State.domain.""" + self._collect_state() + return self._state.domain + + @property + def object_id(self): + """Wrap State.object_id.""" + self._collect_state() + return self._state.object_id + + @property + def name(self): + """Wrap State.name.""" + self._collect_state() + return self._state.name @property def state_with_unit(self) -> str: """Return the state concatenated with the unit if available.""" - state = object.__getattribute__(self, "_access_state")() - unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if unit is None: - return state.state - return f"{state.state} {unit}" + self._collect_state() + unit = self._state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + return f"{self._state.state} {unit}" if unit else self._state.state def __eq__(self, other: Any) -> bool: """Ensure we collect on equality check.""" - state = object.__getattribute__(self, "_state") - hass = object.__getattribute__(self, "_hass") - _collect_state(hass, state.entity_id) - return super().__eq__(other) - - def __getattribute__(self, name): - """Return an attribute of the state.""" - # This one doesn't count as an access of the state - # since we either found it by looking direct for the ID - # or got it off an iterator. - if name == "entity_id" or name in object.__dict__: - state = object.__getattribute__(self, "_state") - return getattr(state, name) - if name in TemplateState.__dict__: - return object.__getattribute__(self, name) - state = object.__getattribute__(self, "_access_state")() - return getattr(state, name) + self._collect_state() + return self._state.__eq__(other) def __repr__(self) -> str: """Representation of Template State.""" - state = object.__getattribute__(self, "_access_state")() - rep = state.__repr__() - return f"