From c40c772f7424c1fcb335753cb4d1253c004e3bd4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Sep 2022 15:43:51 -0400 Subject: [PATCH 001/985] Finish Google brand (#79225) --- homeassistant/brands/google.json | 14 ++- homeassistant/components/nest/manifest.json | 2 +- homeassistant/generated/integrations.json | 94 ++++++++++----------- 3 files changed, 61 insertions(+), 49 deletions(-) diff --git a/homeassistant/brands/google.json b/homeassistant/brands/google.json index c50b0819827..a23c58ed8f1 100644 --- a/homeassistant/brands/google.json +++ b/homeassistant/brands/google.json @@ -1,5 +1,17 @@ { "domain": "google", "name": "Google", - "integrations": ["google", "google_sheets"] + "integrations": [ + "google_assistant", + "google_cloud", + "google_domains", + "google_maps", + "google_pubsub", + "google_sheets", + "google_translate", + "google_travel_time", + "google_wifi", + "google", + "nest" + ] } diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 72e0aed8420..90fad5cf185 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -1,6 +1,6 @@ { "domain": "nest", - "name": "Nest", + "name": "Google Nest", "config_flow": true, "dependencies": ["ffmpeg", "http", "application_credentials"], "after_dependencies": ["media_source"], diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index a6d924bbf5b..9cff574ee87 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1581,57 +1581,62 @@ "google": { "name": "Google", "integrations": { - "google": { - "config_flow": true, + "google_assistant": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Google Assistant" + }, + "google_cloud": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Google Cloud Platform" + }, + "google_domains": { + "config_flow": false, "iot_class": "cloud_polling", - "name": "Google Calendar" + "name": "Google Domains" + }, + "google_maps": { + "config_flow": false, + "iot_class": "cloud_polling", + "name": "Google Maps" + }, + "google_pubsub": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Google Pub/Sub" }, "google_sheets": { "config_flow": true, "iot_class": "cloud_polling", "name": "Google Sheets" + }, + "google_translate": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Google Translate Text-to-Speech" + }, + "google_travel_time": { + "config_flow": true, + "iot_class": "cloud_polling" + }, + "google_wifi": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Google Wifi" + }, + "google": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Google Calendar" + }, + "nest": { + "config_flow": true, + "iot_class": "cloud_push", + "name": "Google Nest" } } }, - "google_assistant": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Google Assistant" - }, - "google_cloud": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Google Cloud Platform" - }, - "google_domains": { - "config_flow": false, - "iot_class": "cloud_polling", - "name": "Google Domains" - }, - "google_maps": { - "config_flow": false, - "iot_class": "cloud_polling", - "name": "Google Maps" - }, - "google_pubsub": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Google Pub/Sub" - }, - "google_translate": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Google Translate Text-to-Speech" - }, - "google_travel_time": { - "config_flow": true, - "iot_class": "cloud_polling" - }, - "google_wifi": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Google Wifi" - }, "govee_ble": { "config_flow": true, "iot_class": "local_push", @@ -2784,11 +2789,6 @@ "iot_class": "local_push", "name": "Ness Alarm" }, - "nest": { - "config_flow": true, - "iot_class": "cloud_push", - "name": "Nest" - }, "netatmo": { "config_flow": true, "iot_class": "cloud_polling", From 99f4ce9e5afcc117a4bf610c49d512055b25ccec Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 28 Sep 2022 21:51:06 +0200 Subject: [PATCH 002/985] Bump version to 2022.11.0dev0 (#79224) --- .github/workflows/ci.yaml | 2 +- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7209a0fbf6f..07b3ee148d7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ on: env: CACHE_VERSION: 1 PIP_CACHE_VERSION: 1 - HA_SHORT_VERSION: 2022.10 + HA_SHORT_VERSION: 2022.11 # Pin latest Python patch versions to avoid issues # with runners using different versions. DEFAULT_PYTHON: 3.9.14 diff --git a/homeassistant/const.py b/homeassistant/const.py index 27d172265ce..330f5399bc2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -6,7 +6,7 @@ from typing import Final from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 -MINOR_VERSION: Final = 10 +MINOR_VERSION: Final = 11 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" diff --git a/pyproject.toml b/pyproject.toml index c76b015c84a..7838e3f7503 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.10.0.dev0" +version = "2022.11.0.dev0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From b49d499ab66218cca8ba884bbca8c8d24dd75733 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Sep 2022 10:17:29 -1000 Subject: [PATCH 003/985] Bump yalexs to 1.2.4 (#79222) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index fdd6b52f740..a816ddc06ff 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.2.3"], + "requirements": ["yalexs==1.2.4"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 8892c8a5b18..b4690f855b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2568,7 +2568,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==1.9.2 # homeassistant.components.august -yalexs==1.2.3 +yalexs==1.2.4 # homeassistant.components.yeelight yeelight==0.7.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c97e09fd37..dc6d6479f1e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1778,7 +1778,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==1.9.2 # homeassistant.components.august -yalexs==1.2.3 +yalexs==1.2.4 # homeassistant.components.yeelight yeelight==0.7.10 From 62c114e849f8dbc4082755d35d841826584162f3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Sep 2022 16:21:09 -0400 Subject: [PATCH 004/985] Add Apple brand (#79227) --- .pre-commit-config.yaml | 2 +- homeassistant/brands/apple.json | 11 +++++ homeassistant/generated/integrations.json | 51 +++++++++++++---------- script/hassfest/brand.py | 6 +-- 4 files changed, 42 insertions(+), 28 deletions(-) create mode 100644 homeassistant/brands/apple.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8442d7abecc..088099bf4e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -106,7 +106,7 @@ repos: pass_filenames: false language: script types: [text] - files: ^(homeassistant/.+/manifest\.json|pyproject\.toml|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$ + files: ^(homeassistant/.+/manifest\.json|homeassistant/brands/.+\.json|pyproject\.toml|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$ - id: hassfest name: hassfest entry: script/run-in-env.sh python3 -m script.hassfest diff --git a/homeassistant/brands/apple.json b/homeassistant/brands/apple.json new file mode 100644 index 00000000000..1a782b50900 --- /dev/null +++ b/homeassistant/brands/apple.json @@ -0,0 +1,11 @@ +{ + "domain": "apple", + "name": "Apple", + "integrations": [ + "icloud", + "ibeacon", + "apple_tv", + "homekit", + "homekit_controller" + ] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 9cff574ee87..cffcdc28b1e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -189,10 +189,34 @@ "iot_class": null, "name": "Home Assistant API" }, - "apple_tv": { - "config_flow": true, - "iot_class": "local_push", - "name": "Apple TV" + "apple": { + "name": "Apple", + "integrations": { + "icloud": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Apple iCloud" + }, + "ibeacon": { + "config_flow": true, + "iot_class": "local_push", + "name": "iBeacon Tracker" + }, + "apple_tv": { + "config_flow": true, + "iot_class": "local_push", + "name": "Apple TV" + }, + "homekit": { + "config_flow": true, + "iot_class": "local_push", + "name": "HomeKit" + }, + "homekit_controller": { + "config_flow": true, + "iot_class": "local_push" + } + } }, "application_credentials": { "config_flow": false, @@ -1811,15 +1835,6 @@ "iot_class": null, "name": "Home Assistant Alerts" }, - "homekit": { - "config_flow": true, - "iot_class": "local_push", - "name": "HomeKit" - }, - "homekit_controller": { - "config_flow": true, - "iot_class": "local_push" - }, "homematic": { "config_flow": false, "iot_class": "local_push", @@ -1919,16 +1934,6 @@ "iot_class": "cloud_polling", "name": "Jandy iAqualink" }, - "ibeacon": { - "config_flow": true, - "iot_class": "local_push", - "name": "iBeacon Tracker" - }, - "icloud": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Apple iCloud" - }, "idteck_prox": { "config_flow": false, "iot_class": "local_push", diff --git a/script/hassfest/brand.py b/script/hassfest/brand.py index 64217da1592..80e2495573e 100644 --- a/script/hassfest/brand.py +++ b/script/hassfest/brand.py @@ -51,10 +51,8 @@ def _validate_brand( f"'{sub_integration}' to 'integrations'", ) - if ( - brand.domain in integrations - and not brand.integrations - or brand.domain not in brand.integrations + if brand.domain in integrations and ( + not brand.integrations or brand.domain not in brand.integrations ): config.add_error( "brand", From e6becabe113ed76b682f2bc7e62d7fe09de21d76 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Sep 2022 16:45:35 -0400 Subject: [PATCH 005/985] Add fritz brand (#79226) --- homeassistant/brands/fritzbox.json | 5 ++++ homeassistant/generated/integrations.json | 31 +++++++++++++---------- 2 files changed, 23 insertions(+), 13 deletions(-) create mode 100644 homeassistant/brands/fritzbox.json diff --git a/homeassistant/brands/fritzbox.json b/homeassistant/brands/fritzbox.json new file mode 100644 index 00000000000..d0c0d1c1584 --- /dev/null +++ b/homeassistant/brands/fritzbox.json @@ -0,0 +1,5 @@ +{ + "domain": "fritzbox", + "name": "FRITZ!Box", + "integrations": ["fritz", "fritzbox", "fritzbox_callmonitor"] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index cffcdc28b1e..475b947d073 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1443,20 +1443,25 @@ "iot_class": "cloud_polling", "name": "Freedompro" }, - "fritz": { - "config_flow": true, - "iot_class": "local_polling", - "name": "AVM FRITZ!Box Tools" - }, "fritzbox": { - "config_flow": true, - "iot_class": "local_polling", - "name": "AVM FRITZ!SmartHome" - }, - "fritzbox_callmonitor": { - "config_flow": true, - "iot_class": "local_polling", - "name": "AVM FRITZ!Box Call Monitor" + "name": "FRITZ!Box", + "integrations": { + "fritz": { + "config_flow": true, + "iot_class": "local_polling", + "name": "AVM FRITZ!Box Tools" + }, + "fritzbox": { + "config_flow": true, + "iot_class": "local_polling", + "name": "AVM FRITZ!SmartHome" + }, + "fritzbox_callmonitor": { + "config_flow": true, + "iot_class": "local_polling", + "name": "AVM FRITZ!Box Call Monitor" + } + } }, "fronius": { "config_flow": true, From b43e19a0c1cedad136a7b10ac6a13c403bb0ac8a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Sep 2022 17:09:42 -0400 Subject: [PATCH 006/985] Add Cast + Chat to Google brand (#79231) --- homeassistant/brands/google.json | 4 +++- homeassistant/generated/integrations.json | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/homeassistant/brands/google.json b/homeassistant/brands/google.json index a23c58ed8f1..8f3340cef29 100644 --- a/homeassistant/brands/google.json +++ b/homeassistant/brands/google.json @@ -12,6 +12,8 @@ "google_travel_time", "google_wifi", "google", - "nest" + "nest", + "cast", + "hangouts" ] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 475b947d073..790d1cd9c3e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -571,11 +571,6 @@ "iot_class": "cloud_polling", "name": "Canary" }, - "cast": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Google Cast" - }, "cert_expiry": { "config_flow": true, "iot_class": "cloud_polling" @@ -1663,6 +1658,16 @@ "config_flow": true, "iot_class": "cloud_push", "name": "Google Nest" + }, + "cast": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Google Cast" + }, + "hangouts": { + "config_flow": true, + "iot_class": "cloud_push", + "name": "Google Chat" } } }, @@ -1725,11 +1730,6 @@ "iot_class": "cloud_polling", "name": "Habitica" }, - "hangouts": { - "config_flow": true, - "iot_class": "cloud_push", - "name": "Google Chat" - }, "hardware": { "config_flow": false, "iot_class": null, From e3ed4eeb76c18ad594565597a66e5db55bb5e5b7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Sep 2022 17:09:53 -0400 Subject: [PATCH 007/985] Add Denon brand (#79230) --- homeassistant/brands/denon.json | 5 ++++ homeassistant/generated/integrations.json | 31 +++++++++++++---------- script/hassfest/brand.py | 7 +++-- 3 files changed, 26 insertions(+), 17 deletions(-) create mode 100644 homeassistant/brands/denon.json diff --git a/homeassistant/brands/denon.json b/homeassistant/brands/denon.json new file mode 100644 index 00000000000..a60750e1a31 --- /dev/null +++ b/homeassistant/brands/denon.json @@ -0,0 +1,5 @@ +{ + "domain": "denon", + "name": "Denon", + "integrations": ["denon", "denonavr", "heos"] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 790d1cd9c3e..c0891bea0a2 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -805,14 +805,24 @@ "iot_class": "calculated" }, "denon": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Denon Network Receivers" - }, - "denonavr": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Denon AVR Network Receivers" + "name": "Denon", + "integrations": { + "denon": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Denon Network Receivers" + }, + "denonavr": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Denon AVR Network Receivers" + }, + "heos": { + "config_flow": true, + "iot_class": "local_push", + "name": "Denon HEOS" + } + } }, "deutsche_bahn": { "config_flow": false, @@ -1770,11 +1780,6 @@ "iot_class": "local_polling", "name": "Heatmiser" }, - "heos": { - "config_flow": true, - "iot_class": "local_push", - "name": "Denon HEOS" - }, "here_travel_time": { "config_flow": true, "iot_class": "cloud_polling", diff --git a/script/hassfest/brand.py b/script/hassfest/brand.py index 80e2495573e..c35f50599ff 100644 --- a/script/hassfest/brand.py +++ b/script/hassfest/brand.py @@ -38,7 +38,7 @@ def _validate_brand( if not brand.integrations and not brand.iot_standards: config.add_error( "brand", - f"Invalid brand file {brand.path.name}: At least one of integrations or " + f"{brand.path.name}: At least one of integrations or " "iot_standards must be non-empty", ) @@ -47,8 +47,7 @@ def _validate_brand( if sub_integration not in integrations: config.add_error( "brand", - f"Invalid brand file {brand.path.name}: Can't add non core domain " - f"'{sub_integration}' to 'integrations'", + f"{brand.path.name}: References unknown integration {sub_integration}", ) if brand.domain in integrations and ( @@ -56,7 +55,7 @@ def _validate_brand( ): config.add_error( "brand", - f"Invalid brand file {brand.path.name}: Brand '{brand.brand['domain']}' " + f"{brand.path.name}: Brand '{brand.brand['domain']}' " f"is an integration but is missing in the brand's 'integrations' list'", ) From 14c68c8692bd9c1326698094f00bfec50c2cfed4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Sep 2022 20:30:50 -0400 Subject: [PATCH 008/985] Add ubiquiti brand (#79232) --- homeassistant/brands/ubiquiti.json | 5 +++ .../components/unifi_direct/manifest.json | 2 +- .../components/unifiled/manifest.json | 2 +- homeassistant/generated/integrations.json | 45 ++++++++++--------- 4 files changed, 32 insertions(+), 22 deletions(-) create mode 100644 homeassistant/brands/ubiquiti.json diff --git a/homeassistant/brands/ubiquiti.json b/homeassistant/brands/ubiquiti.json new file mode 100644 index 00000000000..8b64cffaa7e --- /dev/null +++ b/homeassistant/brands/ubiquiti.json @@ -0,0 +1,5 @@ +{ + "domain": "ubiquiti", + "name": "Ubiquiti", + "integrations": ["unifi", "unifi_direct", "unifiled", "unifiprotect"] +} diff --git a/homeassistant/components/unifi_direct/manifest.json b/homeassistant/components/unifi_direct/manifest.json index b3ed7d2ef2f..9bfc2c8ff49 100644 --- a/homeassistant/components/unifi_direct/manifest.json +++ b/homeassistant/components/unifi_direct/manifest.json @@ -1,6 +1,6 @@ { "domain": "unifi_direct", - "name": "Ubiquiti UniFi AP", + "name": "UniFi AP", "documentation": "https://www.home-assistant.io/integrations/unifi_direct", "requirements": ["pexpect==4.6.0"], "codeowners": [], diff --git a/homeassistant/components/unifiled/manifest.json b/homeassistant/components/unifiled/manifest.json index d0716dcec3a..7f3c2b4701b 100644 --- a/homeassistant/components/unifiled/manifest.json +++ b/homeassistant/components/unifiled/manifest.json @@ -1,6 +1,6 @@ { "domain": "unifiled", - "name": "Ubiquiti UniFi LED", + "name": "UniFi LED", "documentation": "https://www.home-assistant.io/integrations/unifiled", "codeowners": ["@florisvdk"], "requirements": ["unifiled==0.11"], diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index c0891bea0a2..3e056276c16 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4591,6 +4591,31 @@ "iot_class": "cloud_push", "name": "Twitter" }, + "ubiquiti": { + "name": "Ubiquiti", + "integrations": { + "unifi": { + "config_flow": true, + "iot_class": "local_push", + "name": "UniFi Network" + }, + "unifi_direct": { + "config_flow": false, + "iot_class": "local_polling", + "name": "UniFi AP" + }, + "unifiled": { + "config_flow": false, + "iot_class": "local_polling", + "name": "UniFi LED" + }, + "unifiprotect": { + "config_flow": true, + "iot_class": "local_push", + "name": "UniFi Protect" + } + } + }, "ubus": { "config_flow": false, "iot_class": "local_polling", @@ -4611,26 +4636,6 @@ "iot_class": "cloud_polling", "name": "Ukraine Alarm" }, - "unifi": { - "config_flow": true, - "iot_class": "local_push", - "name": "UniFi Network" - }, - "unifi_direct": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Ubiquiti UniFi AP" - }, - "unifiled": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Ubiquiti UniFi LED" - }, - "unifiprotect": { - "config_flow": true, - "iot_class": "local_push", - "name": "UniFi Protect" - }, "universal": { "config_flow": false, "iot_class": "calculated", From 63f2c4ab9856187c95325662cae950f349aef3d2 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 29 Sep 2022 00:36:54 +0000 Subject: [PATCH 009/985] [ci skip] Translation update --- .../amberelectric/translations/sv.json | 5 ++ .../components/apcupsd/translations/bg.json | 18 +++++++ .../components/apcupsd/translations/ca.json | 18 +++++++ .../components/apcupsd/translations/de.json | 26 ++++++++++ .../components/apcupsd/translations/es.json | 26 ++++++++++ .../components/apcupsd/translations/et.json | 26 ++++++++++ .../components/apcupsd/translations/id.json | 26 ++++++++++ .../components/apcupsd/translations/no.json | 26 ++++++++++ .../components/apcupsd/translations/pl.json | 26 ++++++++++ .../apcupsd/translations/pt-BR.json | 26 ++++++++++ .../apcupsd/translations/zh-Hant.json | 26 ++++++++++ .../components/apple_tv/translations/ja.json | 4 +- .../components/bluetooth/translations/sv.json | 6 +++ .../components/braviatv/translations/et.json | 10 +++- .../components/braviatv/translations/pl.json | 12 +++-- .../components/braviatv/translations/sv.json | 10 +++- .../dsmr_reader/translations/et.json | 18 +++++++ .../dsmr_reader/translations/id.json | 18 +++++++ .../dsmr_reader/translations/pl.json | 18 +++++++ .../dsmr_reader/translations/pt-BR.json | 18 +++++++ .../dsmr_reader/translations/sv.json | 18 +++++++ .../components/ezviz/translations/de.json | 10 ++-- .../components/ezviz/translations/es.json | 10 ++-- .../components/ezviz/translations/id.json | 10 ++-- .../components/ezviz/translations/no.json | 10 ++-- .../components/ezviz/translations/pl.json | 10 ++-- .../components/ezviz/translations/pt-BR.json | 10 ++-- .../ezviz/translations/zh-Hant.json | 10 ++-- .../google_sheets/translations/ca.json | 1 + .../google_sheets/translations/et.json | 4 ++ .../google_sheets/translations/id.json | 4 ++ .../google_sheets/translations/pl.json | 4 ++ .../google_sheets/translations/pt-BR.json | 4 ++ .../google_sheets/translations/sv.json | 35 ++++++++++++++ .../components/guardian/translations/sv.json | 11 +++++ .../components/ibeacon/translations/sv.json | 23 +++++++++ .../integration/translations/sv.json | 6 +-- .../components/kegtron/translations/et.json | 19 ++++++++ .../components/kegtron/translations/sv.json | 22 +++++++++ .../keymitt_ble/translations/et.json | 27 +++++++++++ .../keymitt_ble/translations/sv.json | 27 +++++++++++ .../components/lametric/translations/sv.json | 3 +- .../components/lidarr/translations/sv.json | 42 ++++++++++++++++ .../litterrobot/translations/et.json | 6 +++ .../litterrobot/translations/id.json | 6 +++ .../litterrobot/translations/pl.json | 6 +++ .../litterrobot/translations/pt-BR.json | 6 +++ .../litterrobot/translations/sensor.sv.json | 3 ++ .../litterrobot/translations/sv.json | 6 +++ .../components/moon/translations/et.json | 6 +++ .../components/moon/translations/hu.json | 6 +++ .../components/moon/translations/id.json | 6 +++ .../components/moon/translations/no.json | 6 +++ .../components/moon/translations/pl.json | 6 +++ .../components/moon/translations/pt-BR.json | 6 +++ .../components/moon/translations/sv.json | 6 +++ .../nibe_heatpump/translations/et.json | 25 ++++++++++ .../nibe_heatpump/translations/sv.json | 25 ++++++++++ .../components/openuv/translations/sv.json | 10 ++++ .../components/radarr/translations/et.json | 48 +++++++++++++++++++ .../components/radarr/translations/pl.json | 48 +++++++++++++++++++ .../components/radarr/translations/sv.json | 48 +++++++++++++++++++ .../rainmachine/translations/et.json | 13 +++++ .../rainmachine/translations/pl.json | 13 +++++ .../rainmachine/translations/sv.json | 13 +++++ .../components/roomba/translations/de.json | 4 +- .../components/roomba/translations/en.json | 4 +- .../components/roomba/translations/es.json | 4 +- .../components/roomba/translations/id.json | 4 +- .../components/roomba/translations/no.json | 4 +- .../components/roomba/translations/pl.json | 4 +- .../components/roomba/translations/pt-BR.json | 4 +- .../roomba/translations/zh-Hant.json | 4 +- .../rtsp_to_webrtc/translations/pl.json | 2 +- .../components/season/translations/ca.json | 5 ++ .../components/season/translations/et.json | 6 +++ .../components/season/translations/hu.json | 6 +++ .../components/season/translations/id.json | 6 +++ .../components/season/translations/no.json | 6 +++ .../components/season/translations/pl.json | 6 +++ .../components/season/translations/pt-BR.json | 6 +++ .../components/season/translations/sv.json | 6 +++ .../components/sensor/translations/ca.json | 12 ++++- .../components/sensor/translations/de.json | 12 ++++- .../components/sensor/translations/es.json | 12 ++++- .../components/sensor/translations/et.json | 10 +++- .../components/sensor/translations/id.json | 10 +++- .../components/sensor/translations/no.json | 10 +++- .../components/sensor/translations/pl.json | 12 ++++- .../components/sensor/translations/pt-BR.json | 12 ++++- .../components/sensor/translations/ru.json | 2 + .../sensor/translations/zh-Hant.json | 12 ++++- .../components/shelly/translations/et.json | 8 ++++ .../components/shelly/translations/pl.json | 8 ++++ .../components/shelly/translations/pt-BR.json | 8 ++++ .../components/shelly/translations/sv.json | 8 ++++ .../simplisafe/translations/sv.json | 6 +++ .../components/switch/translations/sv.json | 2 +- .../switch_as_x/translations/sv.json | 2 +- .../components/switchbee/translations/sv.json | 32 +++++++++++++ .../components/tasmota/translations/sv.json | 10 ++++ .../components/tautulli/translations/bg.json | 1 + .../components/tautulli/translations/et.json | 1 + .../components/tautulli/translations/fr.json | 1 + .../components/tautulli/translations/hu.json | 1 + .../components/tautulli/translations/id.json | 1 + .../components/tautulli/translations/no.json | 1 + .../components/tautulli/translations/pl.json | 1 + .../tautulli/translations/pt-BR.json | 1 + .../components/tautulli/translations/sv.json | 1 + .../components/uptime/translations/et.json | 6 +++ .../components/uptime/translations/hu.json | 6 +++ .../components/uptime/translations/id.json | 6 +++ .../components/uptime/translations/no.json | 6 +++ .../components/uptime/translations/pl.json | 6 +++ .../components/uptime/translations/pt-BR.json | 6 +++ .../components/uptime/translations/sv.json | 6 +++ .../volvooncall/translations/sv.json | 1 + .../components/zha/translations/de.json | 2 + .../components/zha/translations/en.json | 6 +-- .../components/zha/translations/es.json | 2 + .../components/zwave_js/translations/sv.json | 6 +++ 122 files changed, 1250 insertions(+), 88 deletions(-) create mode 100644 homeassistant/components/apcupsd/translations/bg.json create mode 100644 homeassistant/components/apcupsd/translations/ca.json create mode 100644 homeassistant/components/apcupsd/translations/de.json create mode 100644 homeassistant/components/apcupsd/translations/es.json create mode 100644 homeassistant/components/apcupsd/translations/et.json create mode 100644 homeassistant/components/apcupsd/translations/id.json create mode 100644 homeassistant/components/apcupsd/translations/no.json create mode 100644 homeassistant/components/apcupsd/translations/pl.json create mode 100644 homeassistant/components/apcupsd/translations/pt-BR.json create mode 100644 homeassistant/components/apcupsd/translations/zh-Hant.json create mode 100644 homeassistant/components/dsmr_reader/translations/et.json create mode 100644 homeassistant/components/dsmr_reader/translations/id.json create mode 100644 homeassistant/components/dsmr_reader/translations/pl.json create mode 100644 homeassistant/components/dsmr_reader/translations/pt-BR.json create mode 100644 homeassistant/components/dsmr_reader/translations/sv.json create mode 100644 homeassistant/components/google_sheets/translations/sv.json create mode 100644 homeassistant/components/ibeacon/translations/sv.json create mode 100644 homeassistant/components/kegtron/translations/et.json create mode 100644 homeassistant/components/kegtron/translations/sv.json create mode 100644 homeassistant/components/keymitt_ble/translations/et.json create mode 100644 homeassistant/components/keymitt_ble/translations/sv.json create mode 100644 homeassistant/components/lidarr/translations/sv.json create mode 100644 homeassistant/components/nibe_heatpump/translations/et.json create mode 100644 homeassistant/components/nibe_heatpump/translations/sv.json create mode 100644 homeassistant/components/radarr/translations/et.json create mode 100644 homeassistant/components/radarr/translations/pl.json create mode 100644 homeassistant/components/radarr/translations/sv.json create mode 100644 homeassistant/components/switchbee/translations/sv.json diff --git a/homeassistant/components/amberelectric/translations/sv.json b/homeassistant/components/amberelectric/translations/sv.json index fdf3161483f..ec8a2deadd8 100644 --- a/homeassistant/components/amberelectric/translations/sv.json +++ b/homeassistant/components/amberelectric/translations/sv.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Ogiltig API-nyckel", + "no_site": "Ingen plats har tillhandah\u00e5llits.", + "unknown_error": "Ov\u00e4ntat fel" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/apcupsd/translations/bg.json b/homeassistant/components/apcupsd/translations/bg.json new file mode 100644 index 00000000000..cc5f200ef95 --- /dev/null +++ b/homeassistant/components/apcupsd/translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/ca.json b/homeassistant/components/apcupsd/translations/ca.json new file mode 100644 index 00000000000..414cfb55ce6 --- /dev/null +++ b/homeassistant/components/apcupsd/translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/de.json b/homeassistant/components/apcupsd/translations/de.json new file mode 100644 index 00000000000..cc410a8c84c --- /dev/null +++ b/homeassistant/components/apcupsd/translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "no_status": "Von Host wird kein Status gemeldet" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Gib den Host und den Port ein, auf dem das apcupsd-NIS bereitgestellt wird." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von APC UPS Daemon mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die APC UPS Daemon YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die YAML-Konfiguration des APC UPS Daemon wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/es.json b/homeassistant/components/apcupsd/translations/es.json new file mode 100644 index 00000000000..6f8efa27eae --- /dev/null +++ b/homeassistant/components/apcupsd/translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "no_status": "No se informa ning\u00fan estado del Host" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "description": "Introduce el host y el puerto en el que se sirve el NIS apcupsd." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de APC UPS Daemon mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de APC UPS Daemon de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de APC UPS Daemon" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/et.json b/homeassistant/components/apcupsd/translations/et.json new file mode 100644 index 00000000000..253c963950f --- /dev/null +++ b/homeassistant/components/apcupsd/translations/et.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "no_status": "Host ei ole staatust teatatud." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Sisesta host ja port millel apcupsd NIS-i teenindatakse." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "APC UPS Daemon'i konfigureerimine YAML-i abil eemaldatakse.\n\nTeie olemasolev YAML-konfiguratsioon on automaatselt kasutajaliidesesse imporditud.\n\nProbleemi lahendamiseks eemaldage APC UPS Daemon YAML-konfiguratsioon failist configuration.yaml ja k\u00e4ivitage Home Assistant uuesti.", + "title": "APC UPS Daemon YAML konfiguratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/id.json b/homeassistant/components/apcupsd/translations/id.json new file mode 100644 index 00000000000..db564f0c951 --- /dev/null +++ b/homeassistant/components/apcupsd/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "no_status": "Tidak ada status yang dilaporkan dari Host" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Masukkan host dan port tempat NIS apcupsd dilayani." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi APC UPS Daemon lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML APC UPS Daemon dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML APC UPS Daemon dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/no.json b/homeassistant/components/apcupsd/translations/no.json new file mode 100644 index 00000000000..16b7b768f32 --- /dev/null +++ b/homeassistant/components/apcupsd/translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "no_status": "Ingen status er rapportert fra Vert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "port": "Port" + }, + "description": "Angi verten og porten som apcupsd NIS blir servert p\u00e5." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av APC UPS Daemon ved hjelp av YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern APC UPS Daemon YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "APC UPS Daemon YAML-konfigurasjonen blir fjernet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/pl.json b/homeassistant/components/apcupsd/translations/pl.json new file mode 100644 index 00000000000..a7a712854ea --- /dev/null +++ b/homeassistant/components/apcupsd/translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "no_status": "Nazwa hosta lub adres IP nie zg\u0142asza \u017cadnego statusu" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + }, + "description": "Wprowad\u017a nazw\u0119 hosta i port, na kt\u00f3rym jest obs\u0142ugiwany apcupsd NIS." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja APC UPS Daemon przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla APC UPS Daemon zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/pt-BR.json b/homeassistant/components/apcupsd/translations/pt-BR.json new file mode 100644 index 00000000000..d8792506772 --- /dev/null +++ b/homeassistant/components/apcupsd/translations/pt-BR.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "no_status": "Nenhum status \u00e9 relatado de Nome do host" + }, + "error": { + "cannot_connect": "Falhou ao conectar" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta" + }, + "description": "Insira o host e a porta em que o NIS apcupsd est\u00e1 sendo servido." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do UPS Daemon da APC usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o APC UPS Daemon YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML de APC UPS Daemon est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/zh-Hant.json b/homeassistant/components/apcupsd/translations/zh-Hant.json new file mode 100644 index 00000000000..015ba0f7797 --- /dev/null +++ b/homeassistant/components/apcupsd/translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "no_status": "\u4e3b\u6a5f\u7aef \u672a\u56de\u5831\u4efb\u4f55\u72c0\u614b" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u8f38\u5165 apcupsd NIS \u670d\u52d9\u4e3b\u6a5f\u8207\u901a\u8a0a\u57e0\u3002" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 APC UPS Daemon \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 APC UPS Daemon YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "APC UPS Daemon YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/ja.json b/homeassistant/components/apple_tv/translations/ja.json index 860bf4c961b..23032f0076f 100644 --- a/homeassistant/components/apple_tv/translations/ja.json +++ b/homeassistant/components/apple_tv/translations/ja.json @@ -22,7 +22,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "`{name}` \u3068\u3044\u3046\u540d\u524d\u306eApple TV\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u307e\u3059\u3002 \n\n **\u51e6\u7406\u3092\u5b8c\u4e86\u3059\u308b\u306b\u306f\u3001\u8907\u6570\u306ePIN\u30b3\u30fc\u30c9\u306e\u5165\u529b\u304c\u5fc5\u8981\u306b\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002** \n\n\u3053\u306e\u7d71\u5408\u3067\u306f\u3001Apple TV\u306e\u96fb\u6e90\u3092\u30aa\u30d5\u306b\u3059\u308b\u3053\u3068\u306f *\u3067\u304d\u306a\u3044* \u3053\u3068\u306b\u6ce8\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002 Home Assistant\u306e\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u307f\u304c\u30aa\u30d5\u306b\u306a\u308a\u307e\u3059\uff01", + "description": "`{type}` \u30bf\u30a4\u30d7\u3067 `{name}` \u3068\u3044\u3046\u540d\u524d\u306eApple TV\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u307e\u3059\u3002 \n\n **\u51e6\u7406\u3092\u5b8c\u4e86\u3059\u308b\u306b\u306f\u3001\u8907\u6570\u306ePIN\u30b3\u30fc\u30c9\u306e\u5165\u529b\u304c\u5fc5\u8981\u306b\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002** \n\n\u3053\u306e\u7d71\u5408\u3067\u306f\u3001Apple TV\u306e\u96fb\u6e90\u3092\u30aa\u30d5\u306b\u3059\u308b\u3053\u3068\u306f *\u3067\u304d\u306a\u3044* \u3053\u3068\u306b\u6ce8\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002 Home Assistant\u306e\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u307f\u304c\u30aa\u30d5\u306b\u306a\u308a\u307e\u3059\uff01", "title": "Apple TV\u306e\u8ffd\u52a0\u3092\u78ba\u8a8d\u3059\u308b" }, "pair_no_pin": { @@ -56,7 +56,7 @@ "data": { "device_input": "\u30c7\u30d0\u30a4\u30b9" }, - "description": "\u307e\u305a\u3001\u8ffd\u52a0\u3057\u305f\u3044Apple TV\u306e\u30c7\u30d0\u30a4\u30b9\u540d(Kitchen \u3084 Bedroom\u306a\u3069)\u304bIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u3067\u30c7\u30d0\u30a4\u30b9\u304c\u81ea\u52d5\u7684\u306b\u898b\u3064\u304b\u3063\u305f\u5834\u5408\u306f\u3001\u4ee5\u4e0b\u306b\u8868\u793a\u3055\u308c\u307e\u3059\u3002\n\n\u30c7\u30d0\u30a4\u30b9\u304c\u8868\u793a\u3055\u308c\u306a\u3044\u5834\u5408\u3084\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u6307\u5b9a\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002\n\n{devices}", + "description": "\u307e\u305a\u3001\u8ffd\u52a0\u3057\u305f\u3044Apple TV\u306e\u30c7\u30d0\u30a4\u30b9\u540d(Kitchen \u3084 Bedroom\u306a\u3069)\u304bIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u3067\u30c7\u30d0\u30a4\u30b9\u304c\u81ea\u52d5\u7684\u306b\u898b\u3064\u304b\u3063\u305f\u5834\u5408\u306f\u3001\u4ee5\u4e0b\u306b\u8868\u793a\u3055\u308c\u307e\u3059\u3002\n\n\u30c7\u30d0\u30a4\u30b9\u304c\u8868\u793a\u3055\u308c\u306a\u3044\u5834\u5408\u3084\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u306eIP\u30a2\u30c9\u30ec\u30b9\u3092\u6307\u5b9a\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u65b0\u3057\u3044Apple TV\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/bluetooth/translations/sv.json b/homeassistant/components/bluetooth/translations/sv.json index 4f1e43c1490..ee2d433fc1c 100644 --- a/homeassistant/components/bluetooth/translations/sv.json +++ b/homeassistant/components/bluetooth/translations/sv.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "F\u00f6r att f\u00f6rb\u00e4ttra Bluetooths tillf\u00f6rlitlighet och prestanda rekommenderar vi starkt att du uppdaterar till version 9.0 eller senare av operativsystemet Home Assistant.", + "title": "Uppdatera till Home Assistant operativsystem 9.0 eller senare" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/braviatv/translations/et.json b/homeassistant/components/braviatv/translations/et.json index ecfc89b968d..83ee91c3206 100644 --- a/homeassistant/components/braviatv/translations/et.json +++ b/homeassistant/components/braviatv/translations/et.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", - "no_ip_control": "Teleris on IP-juhtimine keelatud v\u00f5i telerit ei toetata." + "no_ip_control": "Teleris on IP-juhtimine keelatud v\u00f5i telerit ei toetata.", + "not_bravia_device": "Seade ei ole Bravia teler." }, "error": { "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", "invalid_host": "Vigane hostinimi v\u00f5i IP aadress", "unsupported_model": "Teleri mudelit ei toetata." }, "step": { "authorize": { "data": { - "pin": "PIN kood" + "pin": "PIN kood", + "use_psk": "PSK autentimise kasutamine" }, "description": "Sisesta Sony Bravia teleris kuvatud PIN-kood.\n\n Kui PIN-koodi ei kuvata pead teleri Home Assistan'i sidumise t\u00fchistama. Mine: Seaded - > V\u00f5rk - > Kaugseadme seaded - > Kaugseadme registreerimise t\u00fchistamine.", "title": "Sony Bravia TV autoriseerimine" }, + "confirm": { + "description": "Kas alustada seadistamist?" + }, "user": { "data": { "host": "" diff --git a/homeassistant/components/braviatv/translations/pl.json b/homeassistant/components/braviatv/translations/pl.json index 83521554e21..3795bea349e 100644 --- a/homeassistant/components/braviatv/translations/pl.json +++ b/homeassistant/components/braviatv/translations/pl.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "no_ip_control": "Sterowanie IP jest wy\u0142\u0105czone w telewizorze lub telewizor nie jest obs\u0142ugiwany" + "no_ip_control": "Sterowanie IP jest wy\u0142\u0105czone w telewizorze lub telewizor nie jest obs\u0142ugiwany", + "not_bravia_device": "Urz\u0105dzenie nie jest telewizorem Bravia" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", "invalid_host": "Nieprawid\u0142owa nazwa hosta lub adres IP", "unsupported_model": "Ten model telewizora nie jest obs\u0142ugiwany" }, "step": { "authorize": { "data": { - "pin": "Kod PIN" + "pin": "Kod PIN", + "use_psk": "U\u017cyj uwierzytelniania PSK" }, - "description": "Wprowad\u017a kod PIN wy\u015bwietlany na telewizorze Sony Bravia. \n\nJe\u015bli kod PIN nie jest wy\u015bwietlany, musisz wyrejestrowa\u0107 Home Assistanta na swoim telewizorze, przejd\u017a do Ustawienia -> Sie\u0107 -> Ustawienia urz\u0105dzenia zdalnego -> Wyrejestruj urz\u0105dzenie zdalne.", + "description": "Wprowad\u017a kod PIN wy\u015bwietlany na telewizorze Sony Bravia. \n\nJe\u015bli kod PIN nie jest wy\u015bwietlany, musisz wyrejestrowa\u0107 Home Assistanta na telewizorze. Przejd\u017a do: Ustawienia - > Sie\u0107 - > Ustawienia urz\u0105dzenia zdalnego - > Wyrejestruj zdalne urz\u0105dzenie. \n\nMo\u017cesz u\u017cy\u0107 PSK (Pre-Shared-Key) zamiast kodu PIN. PSK to zdefiniowany przez u\u017cytkownika tajny klucz u\u017cywany do kontroli dost\u0119pu. Ta metoda uwierzytelniania jest zalecana jako bardziej stabilna. Aby w\u0142\u0105czy\u0107 PSK na telewizorze, przejd\u017a do: Ustawienia - > Sie\u0107 - > Konfiguracja sieci domowej - > Sterowanie IP. Nast\u0119pnie zaznacz pole \u201eU\u017cyj uwierzytelniania PSK\u201d i wprowad\u017a sw\u00f3j PSK zamiast kodu PIN.", "title": "Autoryzacja Sony Bravia TV" }, + "confirm": { + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + }, "user": { "data": { "host": "Nazwa hosta lub adres IP" diff --git a/homeassistant/components/braviatv/translations/sv.json b/homeassistant/components/braviatv/translations/sv.json index f47ee5c3a77..3467af8e997 100644 --- a/homeassistant/components/braviatv/translations/sv.json +++ b/homeassistant/components/braviatv/translations/sv.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "Den h\u00e4r TV:n \u00e4r redan konfigurerad", - "no_ip_control": "IP-kontroll \u00e4r inaktiverat p\u00e5 din TV eller s\u00e5 st\u00f6ds inte TV:n." + "no_ip_control": "IP-kontroll \u00e4r inaktiverat p\u00e5 din TV eller s\u00e5 st\u00f6ds inte TV:n.", + "not_bravia_device": "Enheten \u00e4r inte en Bravia TV." }, "error": { "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress.", "unsupported_model": "Den h\u00e4r tv modellen st\u00f6ds inte." }, "step": { "authorize": { "data": { - "pin": "Pin-kod" + "pin": "Pin-kod", + "use_psk": "Anv\u00e4nd PSK-autentisering" }, "description": "Ange PIN-koden som visas p\u00e5 Sony Bravia TV. \n\n Om PIN-koden inte visas m\u00e5ste du avregistrera Home Assistant p\u00e5 din TV, g\u00e5 till: Inst\u00e4llningar - > N\u00e4tverk - > Inst\u00e4llningar f\u00f6r fj\u00e4rrenhet - > Avregistrera fj\u00e4rrenhet.", "title": "Auktorisera Sony Bravia TV" }, + "confirm": { + "description": "Vill du starta konfigurationen?" + }, "user": { "data": { "host": "V\u00e4rdnamn eller IP-adress f\u00f6r TV" diff --git a/homeassistant/components/dsmr_reader/translations/et.json b/homeassistant/components/dsmr_reader/translations/et.json new file mode 100644 index 00000000000..39466fb688a --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/et.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks sidumine." + }, + "step": { + "confirm": { + "description": "Veenduge, et DSMR Readeris on konfigureeritud andmeallikad \"jagatud teema\"." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "DSMR Readeri konfigureerimine YAML-i abil eemaldatakse. \n\n Teie olemasolev YAML-i konfiguratsioon imporditi kasutajaliidesesse automaatselt. \n\n Eemaldage DSMR Readeri YAML-i konfiguratsioon failist configuration.yaml ja taask\u00e4ivitage selle probleemi lahendamiseks Home Assistant.", + "title": "DSMR lugeja konfiguratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/id.json b/homeassistant/components/dsmr_reader/translations/id.json new file mode 100644 index 00000000000..0e99b099b2c --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "confirm": { + "description": "Pastikan untuk mengkonfigurasi sumber data 'topik terpisah' di DSMR Reader." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi DSMR Reader lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML DSMR Reader dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML DSMR Reader dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/pl.json b/homeassistant/components/dsmr_reader/translations/pl.json new file mode 100644 index 00000000000..71cc2bdd4f7 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "confirm": { + "description": "Pami\u0119taj, aby skonfigurowa\u0107 \u017ar\u00f3d\u0142a danych \u201esplit topic\u201d w DSMR Reader." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja DSMR Reader przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla DSMR Reader zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/pt-BR.json b/homeassistant/components/dsmr_reader/translations/pt-BR.json new file mode 100644 index 00000000000..292ef5b59fa --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 est\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "confirm": { + "description": "Certifique-se de configurar as fontes de dados 'split topic' no DSMR Reader." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do DSMR Reader usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML do leitor DSMR do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o do Leitor DSMR est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/sv.json b/homeassistant/components/dsmr_reader/translations/sv.json new file mode 100644 index 00000000000..4434d1f3f96 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "confirm": { + "description": "Se till att konfigurera datak\u00e4llorna f\u00f6r \"delat \u00e4mne\" i DSMR Reader." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av DSMR Reader med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort DSMR Reader YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Konfigurationen av DSMR-l\u00e4saren tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/de.json b/homeassistant/components/ezviz/translations/de.json index 92faeff2b81..0cd59cb50b9 100644 --- a/homeassistant/components/ezviz/translations/de.json +++ b/homeassistant/components/ezviz/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "Konto wurde bereits konfiguriert", - "ezviz_cloud_account_missing": "Ezviz-Cloud-Konto fehlt. Bitte konfiguriere das Ezviz-Cloud-Konto neu", + "ezviz_cloud_account_missing": "EZVIZ-Cloud-Konto fehlt. Bitte konfiguriere das EZVIZ-Cloud-Konto neu", "unknown": "Unerwarteter Fehler" }, "error": { @@ -17,8 +17,8 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "RTSP-Anmeldeinformationen f\u00fcr Ezviz-Kamera {serial} mit IP {ip_address} eingeben", - "title": "Entdeckte Ezviz-Kamera" + "description": "RTSP-Anmeldeinformationen f\u00fcr EZVIZ-Kamera {serial} mit IP {ip_address} eingeben", + "title": "Entdeckte EZVIZ-Kamera" }, "user": { "data": { @@ -26,7 +26,7 @@ "url": "URL", "username": "Benutzername" }, - "title": "Verbinden mit Ezviz Cloud" + "title": "Verbindung zur EZVIZ Cloud" }, "user_custom_url": { "data": { @@ -35,7 +35,7 @@ "username": "Benutzername" }, "description": "URL Region manuell festlegen", - "title": "Verbinden mit benutzerdefinierter Ezviz-URL" + "title": "Verbinden mit benutzerdefinierter EZVIZ-URL" } } }, diff --git a/homeassistant/components/ezviz/translations/es.json b/homeassistant/components/ezviz/translations/es.json index a69cb8d5d24..1c7305c53f6 100644 --- a/homeassistant/components/ezviz/translations/es.json +++ b/homeassistant/components/ezviz/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "La cuenta ya est\u00e1 configurada", - "ezviz_cloud_account_missing": "Falta la cuenta de Ezviz Cloud. Por favor, vuelve a configurar la cuenta de Ezviz Cloud", + "ezviz_cloud_account_missing": "Falta la cuenta de EZVIZ Cloud. Por favor, vuelve a configurar la cuenta de EZVIZ Cloud", "unknown": "Error inesperado" }, "error": { @@ -17,8 +17,8 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Introduce las credenciales RTSP para la c\u00e1mara Ezviz {serial} con IP {ip_address}", - "title": "Descubierta c\u00e1mara Ezviz" + "description": "Introduce las credenciales RTSP para la c\u00e1mara EZVIZ {serial} con IP {ip_address}", + "title": "Descubierta c\u00e1mara EZVIZ" }, "user": { "data": { @@ -26,7 +26,7 @@ "url": "URL", "username": "Nombre de usuario" }, - "title": "Conectar con Ezviz Cloud" + "title": "Conectar con EZVIZ Cloud" }, "user_custom_url": { "data": { @@ -35,7 +35,7 @@ "username": "Nombre de usuario" }, "description": "Especificar manualmente la URL de tu regi\u00f3n", - "title": "Conectar a la URL personalizada de Ezviz" + "title": "Conectar a la URL personalizada de EZVIZ" } } }, diff --git a/homeassistant/components/ezviz/translations/id.json b/homeassistant/components/ezviz/translations/id.json index e263b00c7da..1859b1cb0bc 100644 --- a/homeassistant/components/ezviz/translations/id.json +++ b/homeassistant/components/ezviz/translations/id.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "Akun sudah dikonfigurasi", - "ezviz_cloud_account_missing": "Akun cloud Ezviz tidak tersedia. Konfigurasi ulang akun cloud Ezviz", + "ezviz_cloud_account_missing": "Akun cloud EZVIZ tidak tersedia. Konfigurasi ulang akun cloud EZVIZ", "unknown": "Kesalahan yang tidak diharapkan" }, "error": { @@ -17,8 +17,8 @@ "password": "Kata Sandi", "username": "Nama Pengguna" }, - "description": "Masukkan kredensial RTSP untuk kamera Ezviz {serial} dengan IP {ip_address}", - "title": "Kamera Ezviz yang ditemukan" + "description": "Masukkan kredensial RTSP untuk kamera EZVIZ {serial} dengan IP {ip_address}", + "title": "Kamera EZVIZ yang ditemukan" }, "user": { "data": { @@ -26,7 +26,7 @@ "url": "URL", "username": "Nama Pengguna" }, - "title": "Hubungkan ke Ezviz Cloud" + "title": "Hubungkan ke EZVIZ Cloud" }, "user_custom_url": { "data": { @@ -35,7 +35,7 @@ "username": "Nama Pengguna" }, "description": "Tentukan URL wilayah Anda secara manual", - "title": "Hubungkan ke URL Ezviz khusus" + "title": "Hubungkan ke URL EZVIZ khusus" } } }, diff --git a/homeassistant/components/ezviz/translations/no.json b/homeassistant/components/ezviz/translations/no.json index 306babef86c..a8351b205f9 100644 --- a/homeassistant/components/ezviz/translations/no.json +++ b/homeassistant/components/ezviz/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "Kontoen er allerede konfigurert", - "ezviz_cloud_account_missing": "Ezviz sky-konto mangler. Vennligst konfigurer Ezviz sky-konto p\u00e5 nytt", + "ezviz_cloud_account_missing": "EZVIZ skykonto mangler. Vennligst rekonfigurer EZVIZ skykonto", "unknown": "Uventet feil" }, "error": { @@ -17,8 +17,8 @@ "password": "Passord", "username": "Brukernavn" }, - "description": "Angi RTSP-legitimasjon for Ezviz-kameraet {serial} med IP {ip_address}", - "title": "Oppdaget Ezviz Kamera" + "description": "Skriv inn RTSP-legitimasjon for EZVIZ-kamera {serial} med IP {ip_address}", + "title": "Oppdaget EZVIZ-kamera" }, "user": { "data": { @@ -26,7 +26,7 @@ "url": "URL", "username": "Brukernavn" }, - "title": "Koble til Ezviz Cloud" + "title": "Koble til EZVIZ Cloud" }, "user_custom_url": { "data": { @@ -35,7 +35,7 @@ "username": "Brukernavn" }, "description": "Angi url-adressen for omr\u00e5det manuelt", - "title": "Koble til tilpasset Ezviz URL" + "title": "Koble til egendefinert EZVIZ URL" } } }, diff --git a/homeassistant/components/ezviz/translations/pl.json b/homeassistant/components/ezviz/translations/pl.json index a8413da6188..e59f5c3d86f 100644 --- a/homeassistant/components/ezviz/translations/pl.json +++ b/homeassistant/components/ezviz/translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "Konto jest ju\u017c skonfigurowane", - "ezviz_cloud_account_missing": "Brak konta Ezviz. Skonfiguruj ponownie konto Ezviz.", + "ezviz_cloud_account_missing": "Brak konta EZVIZ. Skonfiguruj ponownie konto EZVIZ.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { @@ -17,8 +17,8 @@ "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" }, - "description": "Wpisz dane logowania RTSP dla kamery Ezviz {serial} z IP {ip_address}", - "title": "Wykryto kamer\u0119 Ezviz" + "description": "Wpisz dane logowania RTSP dla kamery EZVIZ {serial} z IP {ip_address}", + "title": "Wykryto kamer\u0119 EZVIZ" }, "user": { "data": { @@ -26,7 +26,7 @@ "url": "URL", "username": "Nazwa u\u017cytkownika" }, - "title": "Po\u0142\u0105czenie z chmur\u0105 Ezviz" + "title": "Po\u0142\u0105czenie z chmur\u0105 EZVIZ" }, "user_custom_url": { "data": { @@ -35,7 +35,7 @@ "username": "Nazwa u\u017cytkownika" }, "description": "R\u0119cznie okre\u015bl adres URL dla swojego regionu", - "title": "Po\u0142\u0105czenie z niestandardowym adresem URL Ezviz" + "title": "Po\u0142\u0105czenie z niestandardowym adresem URL EZVIZ" } } }, diff --git a/homeassistant/components/ezviz/translations/pt-BR.json b/homeassistant/components/ezviz/translations/pt-BR.json index 371686bbf98..5b495d36d57 100644 --- a/homeassistant/components/ezviz/translations/pt-BR.json +++ b/homeassistant/components/ezviz/translations/pt-BR.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "A conta j\u00e1 foi configurada", - "ezviz_cloud_account_missing": "Conta na nuvem Ezviz ausente. Por favor, reconfigure a conta de nuvem Ezviz", + "ezviz_cloud_account_missing": "Conta na nuvem EZVIZ ausente. Por favor, reconfigure a conta de nuvem EZVIZ", "unknown": "Erro inesperado" }, "error": { @@ -17,8 +17,8 @@ "password": "Senha", "username": "Usu\u00e1rio" }, - "description": "Insira as credenciais RTSP para a c\u00e2mera Ezviz {serial} com IP {ip_address}", - "title": "C\u00e2mera Ezviz descoberta" + "description": "Insira as credenciais RTSP para a c\u00e2mera EZVIZ {serial} com IP {ip_address}", + "title": "C\u00e2mera EZVIZ descoberta" }, "user": { "data": { @@ -26,7 +26,7 @@ "url": "URL", "username": "Usu\u00e1rio" }, - "title": "Conecte-se ao Ezviz Cloud" + "title": "Conecte-se a EZVIZ Cloud" }, "user_custom_url": { "data": { @@ -35,7 +35,7 @@ "username": "Usu\u00e1rio" }, "description": "Especifique manualmente o URL da sua regi\u00e3o", - "title": "Conecte-se ao URL personalizado do Ezviz" + "title": "Conecte-se a URL personalizado do EZVIZ" } } }, diff --git a/homeassistant/components/ezviz/translations/zh-Hant.json b/homeassistant/components/ezviz/translations/zh-Hant.json index 84c5daf14c3..85c474e6484 100644 --- a/homeassistant/components/ezviz/translations/zh-Hant.json +++ b/homeassistant/components/ezviz/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "ezviz_cloud_account_missing": "\u627e\u4e0d\u5230 Ezviz \u96f2\u5e33\u865f\u3002\u8acb\u91cd\u65b0\u8a2d\u5b9a Ezviz \u96f2\u5e33\u865f", + "ezviz_cloud_account_missing": "\u627e\u4e0d\u5230 EZVIZ \u96f2\u5e33\u865f\u3002\u8acb\u91cd\u65b0\u8a2d\u5b9a EZVIZ \u96f2\u5e33\u865f", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { @@ -17,8 +17,8 @@ "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8f38\u5165 IP \u70ba {ip_address} \u7684 Ezviz \u651d\u5f71\u6a5f {serial} RTSP \u6191\u8b49", - "title": "\u81ea\u52d5\u641c\u7d22\u5230\u7684 Ezviz \u651d\u5f71\u6a5f" + "description": "\u8f38\u5165 IP \u70ba {ip_address} \u7684 EZVIZ \u651d\u5f71\u6a5f {serial} RTSP \u6191\u8b49", + "title": "\u81ea\u52d5\u641c\u7d22\u5230\u7684 EZVIZ \u651d\u5f71\u6a5f" }, "user": { "data": { @@ -26,7 +26,7 @@ "url": "\u7db2\u5740", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "title": "\u9023\u7dda\u81f3 Ezviz \u87a2\u77f3\u96f2" + "title": "\u9023\u7dda\u81f3 EZVIZ \u87a2\u77f3\u96f2" }, "user_custom_url": { "data": { @@ -35,7 +35,7 @@ "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "description": "\u624b\u52d5\u6307\u5b9a\u5340\u57df URL", - "title": "\u9023\u7dda\u81f3\u81ea\u8a02 Ezviz URL" + "title": "\u9023\u7dda\u81f3\u81ea\u8a02 EZVIZ URL" } } }, diff --git a/homeassistant/components/google_sheets/translations/ca.json b/homeassistant/components/google_sheets/translations/ca.json index 35d26781c3a..e9cf3ddeb35 100644 --- a/homeassistant/components/google_sheets/translations/ca.json +++ b/homeassistant/components/google_sheets/translations/ca.json @@ -27,6 +27,7 @@ "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" }, "reauth_confirm": { + "description": "La integraci\u00f3 Google Sheets ha de tornar a autenticar-se amb el teu compte", "title": "Reautenticaci\u00f3 de la integraci\u00f3" } } diff --git a/homeassistant/components/google_sheets/translations/et.json b/homeassistant/components/google_sheets/translations/et.json index e1e88192389..fe8433a5de8 100644 --- a/homeassistant/components/google_sheets/translations/et.json +++ b/homeassistant/components/google_sheets/translations/et.json @@ -25,6 +25,10 @@ }, "pick_implementation": { "title": "Vali tuvastusmeetod" + }, + "reauth_confirm": { + "description": "Google Sheets integratsioon peab teie konto uuesti autentima", + "title": "Taastuvasta sidumine" } } } diff --git a/homeassistant/components/google_sheets/translations/id.json b/homeassistant/components/google_sheets/translations/id.json index 474fa65b005..391a68a272f 100644 --- a/homeassistant/components/google_sheets/translations/id.json +++ b/homeassistant/components/google_sheets/translations/id.json @@ -25,6 +25,10 @@ }, "pick_implementation": { "title": "Pilih Metode Autentikasi" + }, + "reauth_confirm": { + "description": "Integrasi Google Spreadsheet perlu mengautentikasi ulang akun Anda", + "title": "Autentikasi Ulang Integrasi" } } } diff --git a/homeassistant/components/google_sheets/translations/pl.json b/homeassistant/components/google_sheets/translations/pl.json index c976b9f1910..1f0ea6a0c26 100644 --- a/homeassistant/components/google_sheets/translations/pl.json +++ b/homeassistant/components/google_sheets/translations/pl.json @@ -25,6 +25,10 @@ }, "pick_implementation": { "title": "Wybierz metod\u0119 uwierzytelniania" + }, + "reauth_confirm": { + "description": "Integracja Arkusze Google wymaga ponownego uwierzytelnienia Twojego konta", + "title": "Ponownie uwierzytelnij integracj\u0119" } } } diff --git a/homeassistant/components/google_sheets/translations/pt-BR.json b/homeassistant/components/google_sheets/translations/pt-BR.json index e8b4f23a4ed..870a7ef267e 100644 --- a/homeassistant/components/google_sheets/translations/pt-BR.json +++ b/homeassistant/components/google_sheets/translations/pt-BR.json @@ -25,6 +25,10 @@ }, "pick_implementation": { "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "description": "A integra\u00e7\u00e3o do Planilhas Google precisa autenticar novamente sua conta", + "title": "Reautenticar Integra\u00e7\u00e3o" } } } diff --git a/homeassistant/components/google_sheets/translations/sv.json b/homeassistant/components/google_sheets/translations/sv.json new file mode 100644 index 00000000000..06c59d33c2a --- /dev/null +++ b/homeassistant/components/google_sheets/translations/sv.json @@ -0,0 +1,35 @@ +{ + "application_credentials": { + "description": "F\u00f6lj [instruktionerna]({more_info_url}) f\u00f6r [OAuth-samtyckessk\u00e4rmen]({oauth_consent_url}) f\u00f6r att ge Home Assistant \u00e5tkomst till dina Google Kalkylark. Du m\u00e5ste ocks\u00e5 skapa applikationsuppgifter kopplade till ditt konto:\n 1. G\u00e5 till [Inloggningsuppgifter]({oauth_creds_url}) och klicka p\u00e5 **Skapa inloggningsuppgifter**.\n 1. V\u00e4lj **OAuth-klient-ID** i rullgardinsmenyn.\n 1. V\u00e4lj **Webbapplikation** f\u00f6r applikationstyp. \n\n" + }, + "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta.", + "create_spreadsheet_failure": "Fel vid skapande av kalkylblad, se fellogg f\u00f6r mer information", + "invalid_access_token": "Ogiltig \u00e5tkomstnyckel", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "oauth_error": "Mottog ogiltiga tokendata.", + "open_spreadsheet_failure": "Fel vid \u00f6ppning av kalkylark, se fellogg f\u00f6r detaljer", + "reauth_successful": "\u00c5terautentisering lyckades", + "timeout_connect": "Timeout uppr\u00e4ttar anslutning", + "unknown": "Ov\u00e4ntat fel" + }, + "create_entry": { + "default": "Framg\u00e5ngsrikt autentiserat och kalkylark skapat p\u00e5: {url}" + }, + "step": { + "auth": { + "title": "L\u00e4nka Google-konto" + }, + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + }, + "reauth_confirm": { + "description": "Google Sheets-integrationen m\u00e5ste autentisera ditt konto p\u00e5 nytt", + "title": "\u00c5terautenticera integration" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/sv.json b/homeassistant/components/guardian/translations/sv.json index af41cc85efe..0912dd4094b 100644 --- a/homeassistant/components/guardian/translations/sv.json +++ b/homeassistant/components/guardian/translations/sv.json @@ -29,6 +29,17 @@ } }, "title": "Tj\u00e4nsten {deprecated_service} tas bort" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Uppdatera alla automatiseringar eller skript som anv\u00e4nder denna enhet f\u00f6r att ist\u00e4llet anv\u00e4nda ` {replacement_entity_id} `.", + "title": "{old_entity_id} kommer att tas bort" + } + } + }, + "title": "{old_entity_id} kommer att tas bort" } } } \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/sv.json b/homeassistant/components/ibeacon/translations/sv.json new file mode 100644 index 00000000000..14ce1f415cc --- /dev/null +++ b/homeassistant/components/ibeacon/translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "Minst en Bluetooth-adapter eller proxy m\u00e5ste konfigureras f\u00f6r att anv\u00e4nda iBeacon Tracker.", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du konfigurera iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "Minsta RSSI" + }, + "description": "iBeacons med ett RSSI-v\u00e4rde som \u00e4r l\u00e4gre \u00e4n det l\u00e4gsta RSSI-v\u00e4rdet ignoreras. Om integrationen ser n\u00e4rliggande iBeacons kan det hj\u00e4lpa att \u00f6ka detta v\u00e4rde." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/sv.json b/homeassistant/components/integration/translations/sv.json index edba53d6310..f47ae6164d2 100644 --- a/homeassistant/components/integration/translations/sv.json +++ b/homeassistant/components/integration/translations/sv.json @@ -15,8 +15,8 @@ "unit_prefix": "Utdata kommer att skalas enligt det valda metriska prefixet.", "unit_time": "Utg\u00e5ngen kommer att skalas enligt den valda tidsenheten." }, - "description": "Skapa en sensor som ber\u00e4knar en Riemanns summa f\u00f6r att uppskatta integralen av en sensor.", - "title": "L\u00e4gg till Riemann summa integral sensor" + "description": "Skapa en sensor som ber\u00e4knar en Riemannsumma f\u00f6r att uppskatta integralen av en sensor.", + "title": "L\u00e4gg till Riemannsumma integralsensor" } } }, @@ -32,5 +32,5 @@ } } }, - "title": "Integration - Riemann summa integral sensor" + "title": "Integral - Riemannsumma integralsensor" } \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/et.json b/homeassistant/components/kegtron/translations/et.json new file mode 100644 index 00000000000..a83aceef49d --- /dev/null +++ b/homeassistant/components/kegtron/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "not_supported": "Seadet ei toetata" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/sv.json b/homeassistant/components/kegtron/translations/sv.json new file mode 100644 index 00000000000..6c6f3f5f1bb --- /dev/null +++ b/homeassistant/components/kegtron/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "not_supported": "Enheten st\u00f6ds inte" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du konfigurera {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att konfigurera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/et.json b/homeassistant/components/keymitt_ble/translations/et.json new file mode 100644 index 00000000000..9ebb2faadb9 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/et.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus", + "no_unconfigured_devices": "H\u00e4\u00e4lestamata seadmeid ei leitud.", + "unknown": "Ootamatu t\u00f5rge" + }, + "error": { + "linking": "Sidumine eba\u00f5nnestus, proovi uuesti. Kas MicroBot on sidumisre\u017eiimis?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Seadme aadress", + "name": "Nimi" + }, + "title": "MicroBot seadme seadistamine" + }, + "link": { + "description": "Vajutage MicroBot Push'i nuppu, kui LED on roosa v\u00f5i roheline, et registreeruda Home Assistant'is.", + "title": "Sidumine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/sv.json b/homeassistant/components/keymitt_ble/translations/sv.json new file mode 100644 index 00000000000..d7b16419bf0 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "no_unconfigured_devices": "Inga okonfigurerade enheter hittades.", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "linking": "Det gick inte att koppla ihop, f\u00f6rs\u00f6k igen. \u00c4r MicroBot i parningsl\u00e4ge?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Enhetsadress", + "name": "Namn" + }, + "title": "Konfigurera MicroBot-enhet" + }, + "link": { + "description": "Tryck p\u00e5 knappen p\u00e5 MicroBot Push n\u00e4r lysdioden lyser rosa eller gr\u00f6nt f\u00f6r att registrera dig med Home Assistant.", + "title": "Parkoppling" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/sv.json b/homeassistant/components/lametric/translations/sv.json index 4ea1aa31e3f..e1597b90f85 100644 --- a/homeassistant/components/lametric/translations/sv.json +++ b/homeassistant/components/lametric/translations/sv.json @@ -7,7 +7,8 @@ "link_local_address": "Lokala l\u00e4nkadresser st\u00f6ds inte", "missing_configuration": "LaMetric-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen.", "no_devices": "Den auktoriserade anv\u00e4ndaren har inga LaMetric-enheter", - "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", + "unknown": "Ov\u00e4ntat fel" }, "error": { "cannot_connect": "Det gick inte att ansluta.", diff --git a/homeassistant/components/lidarr/translations/sv.json b/homeassistant/components/lidarr/translations/sv.json new file mode 100644 index 00000000000..6e87010feae --- /dev/null +++ b/homeassistant/components/lidarr/translations/sv.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel", + "wrong_app": "Felaktig ans\u00f6kan har n\u00e5tts. F\u00f6rs\u00f6k igen.", + "zeroconf_failed": "API-nyckeln har inte hittats. Ange den manuellt" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-nyckel" + }, + "description": "Lidarr-integrationen m\u00e5ste \u00e5terautentiseras manuellt med Lidarr API", + "title": "\u00c5terautenticera integration" + }, + "user": { + "data": { + "api_key": "API-nyckel", + "url": "URL", + "verify_ssl": "Verifiera SSL-certifikat" + }, + "description": "API-nyckel kan h\u00e4mtas automatiskt om inloggningsuppgifter inte st\u00e4llts in i applikationen.\n Din API-nyckel finns i Inst\u00e4llningar > Allm\u00e4nt i Lidarr Web UI." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "Antal maximala poster att visa p\u00e5 \u00f6nskad och k\u00f6", + "upcoming_days": "Antal kommande dagar att visa i kalendern" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/et.json b/homeassistant/components/litterrobot/translations/et.json index 8bbd26ee4c4..b271a1195d9 100644 --- a/homeassistant/components/litterrobot/translations/et.json +++ b/homeassistant/components/litterrobot/translations/et.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "Vaakumseadme atribuudid on n\u00fc\u00fcd saadaval diagnostiliste anduritena.\n\nPalun kohandage k\u00f5iki automatiseerimisi v\u00f5i skripte, mis neid atribuute kasutavad.", + "title": "Litter-Roboti atribuudid on n\u00fc\u00fcd oma andurid" + } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/id.json b/homeassistant/components/litterrobot/translations/id.json index 73e19f1d439..b8d3d58a8dc 100644 --- a/homeassistant/components/litterrobot/translations/id.json +++ b/homeassistant/components/litterrobot/translations/id.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "Atribut entitas vakum sekarang tersedia sebagai sensor diagnostik.\n\nSesuaikan semua otomasi atau skrip yang mungkin Anda miliki yang menggunakan atribut ini.", + "title": "Atribut Litter-Robot sekarang menjadi sensor tersendiri" + } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/pl.json b/homeassistant/components/litterrobot/translations/pl.json index 306acce8743..aaad705c2a7 100644 --- a/homeassistant/components/litterrobot/translations/pl.json +++ b/homeassistant/components/litterrobot/translations/pl.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "Atrybuty encji s\u0105 teraz dost\u0119pne jako sensory diagnostyczne. \n\nDostosuj wszelkie automatyzacje lub skrypty korzystaj\u0105ce z tych atrybut\u00f3w.", + "title": "Atrybuty Litter-Robot s\u0105 teraz ich w\u0142asnymi sensorami" + } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/pt-BR.json b/homeassistant/components/litterrobot/translations/pt-BR.json index 9b204c74f07..dfeb0a9018f 100644 --- a/homeassistant/components/litterrobot/translations/pt-BR.json +++ b/homeassistant/components/litterrobot/translations/pt-BR.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "Os atributos da entidade de v\u00e1cuo agora est\u00e3o dispon\u00edveis como sensores de diagn\u00f3stico. \n\n Ajuste quaisquer automa\u00e7\u00f5es ou scripts que voc\u00ea possa ter que usem esses atributos.", + "title": "Os atributos do Litter-Robot agora s\u00e3o seus pr\u00f3prios sensores" + } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.sv.json b/homeassistant/components/litterrobot/translations/sensor.sv.json index c54c705b8c4..9509408a94e 100644 --- a/homeassistant/components/litterrobot/translations/sensor.sv.json +++ b/homeassistant/components/litterrobot/translations/sensor.sv.json @@ -4,6 +4,7 @@ "br": "Huven \u00e4r borttagen", "ccc": "Reningscykel klar", "ccp": "Reng\u00f6ringscykel p\u00e5g\u00e5r", + "cd": "Katt uppt\u00e4ckt", "csf": "Kattsensor fel", "csi": "Kattsensor avbruten", "cst": "Kattsensor timing", @@ -19,6 +20,8 @@ "otf": "Fel vid f\u00f6r h\u00f6gt vridmoment", "p": "Pausad", "pd": "Pinch Detect", + "pwrd": "St\u00e4nger av", + "pwru": "Startar upp", "rdy": "Redo", "scf": "Fel p\u00e5 kattsensorn vid uppstart", "sdf": "L\u00e5dan full vid uppstart", diff --git a/homeassistant/components/litterrobot/translations/sv.json b/homeassistant/components/litterrobot/translations/sv.json index e8919b760d8..fec9180f066 100644 --- a/homeassistant/components/litterrobot/translations/sv.json +++ b/homeassistant/components/litterrobot/translations/sv.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "Vakuuminneh\u00e5llet \u00e4r nu tillg\u00e4ngligt som diagnostiska sensorer.\n\nV\u00e4nligen justera eventuella automatiseringar eller skript som anv\u00e4nder dessa attribut.", + "title": "Litter-Robots attribut \u00e4r nu deras egna sensorer" + } } } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/et.json b/homeassistant/components/moon/translations/et.json index 28eccf25439..5f51cc47ab1 100644 --- a/homeassistant/components/moon/translations/et.json +++ b/homeassistant/components/moon/translations/et.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Mooni seadistamine YAML-i abil on eemaldatud.\n\nHome Assistant ei kasuta teie olemasolevat YAML-i konfiguratsiooni.\n\nEemaldage FAILIST CONFIGURATION.yaml YAML-konfiguratsioon ja taask\u00e4ivitage selle probleemi lahendamiseks Home Assistant.", + "title": "Moon YAML konfiguratsioon on eemaldatud" + } + }, "title": "Kuu" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/hu.json b/homeassistant/components/moon/translations/hu.json index 8c6a9f42071..ed7722ed484 100644 --- a/homeassistant/components/moon/translations/hu.json +++ b/homeassistant/components/moon/translations/hu.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "A Moon YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3t a Home Assistant nem haszn\u00e1lja.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Moon YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fclt" + } + }, "title": "Hold" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/id.json b/homeassistant/components/moon/translations/id.json index 42b208bfb7e..01c0ce2df14 100644 --- a/homeassistant/components/moon/translations/id.json +++ b/homeassistant/components/moon/translations/id.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Proses konfigurasi Integrasi Bulan lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Integrasi Bulan telah dihapus" + } + }, "title": "Bulan" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/no.json b/homeassistant/components/moon/translations/no.json index c86dae4f615..a4010e77787 100644 --- a/homeassistant/components/moon/translations/no.json +++ b/homeassistant/components/moon/translations/no.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Konfigurering av Moon ved hjelp av YAML er fjernet. \n\n Din eksisterende YAML-konfigurasjon brukes ikke av Home Assistant. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Moon YAML-konfigurasjonen er fjernet" + } + }, "title": "M\u00e5ne" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/pl.json b/homeassistant/components/moon/translations/pl.json index fe01b71dadf..c3ebaeca4a8 100644 --- a/homeassistant/components/moon/translations/pl.json +++ b/homeassistant/components/moon/translations/pl.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Konfiguracja Ksi\u0119\u017cyca za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistant, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Ksi\u0119\u017cyca zosta\u0142a usuni\u0119ta" + } + }, "title": "Ksi\u0119\u017cyc" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/pt-BR.json b/homeassistant/components/moon/translations/pt-BR.json index 1570f4110ec..1e2d6c5c3f0 100644 --- a/homeassistant/components/moon/translations/pt-BR.json +++ b/homeassistant/components/moon/translations/pt-BR.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o da Moon usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML da Moon foi removida" + } + }, "title": "Moon" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/sv.json b/homeassistant/components/moon/translations/sv.json index 38aaa7157e6..8c6b4e20926 100644 --- a/homeassistant/components/moon/translations/sv.json +++ b/homeassistant/components/moon/translations/sv.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Att konfigurera Moon med YAML har tagits bort. \n\n Din befintliga YAML-konfiguration anv\u00e4nds inte av Home Assistant. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Moon YAML-konfigurationen har tagits bort" + } + }, "title": "M\u00e5nen" } \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/et.json b/homeassistant/components/nibe_heatpump/translations/et.json new file mode 100644 index 00000000000..3055a863bd4 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/et.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "address": "M\u00e4\u00e4ratud on sobimatu kaug-IP-aadress. Aadress peab olema IPV4-aadress.", + "address_in_use": "Valitud kuulamisport on selles s\u00fcsteemis juba kasutusel.", + "model": "Valitud mudel ei n\u00e4i toetavat modbus40.", + "read": "Viga pumba lugemistaotlusel. Kinnitage oma \"Kaugloetav port\" v\u00f5i \"Kaug-IP-aadress\".", + "unknown": "Ootamatu t\u00f5rge", + "write": "Viga pumba kirjutamise taotlusel. Kontrollige oma `kaugkirjutusport` v\u00f5i `kaug-IP-aadress`." + }, + "step": { + "user": { + "data": { + "ip_address": "Kaug-IP-aadress", + "listening_port": "Kohalik kuulamisport", + "remote_read_port": "Kauglugemise port", + "remote_write_port": "Kaugkirjutusport" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/sv.json b/homeassistant/components/nibe_heatpump/translations/sv.json new file mode 100644 index 00000000000..4e0c9cdd7ca --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/sv.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "address": "Ogiltig fj\u00e4rr-IP-adress har angetts. Adressen m\u00e5ste vara en IPv4-adress.", + "address_in_use": "Den valda lyssningsporten anv\u00e4nds redan p\u00e5 detta system.", + "model": "Den valda modellen verkar inte st\u00f6dja modbus40", + "read": "Fel p\u00e5 l\u00e4sf\u00f6rfr\u00e5gan fr\u00e5n pumpen. Verifiera din \"Fj\u00e4rrl\u00e4sningsport\" eller \"Fj\u00e4rr-IP-adress\".", + "unknown": "Ov\u00e4ntat fel", + "write": "Fel vid skrivbeg\u00e4ran till pumpen. Verifiera din `Fj\u00e4rrskrivport` eller `Fj\u00e4rr-IP-adress`." + }, + "step": { + "user": { + "data": { + "ip_address": "Fj\u00e4rr IP-adress", + "listening_port": "Lokal lyssningsport", + "remote_read_port": "Port f\u00f6r fj\u00e4rravl\u00e4sning", + "remote_write_port": "Port f\u00f6r fj\u00e4rrskrivning" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/sv.json b/homeassistant/components/openuv/translations/sv.json index 9f1620fb980..073d160dece 100644 --- a/homeassistant/components/openuv/translations/sv.json +++ b/homeassistant/components/openuv/translations/sv.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Uppdatera eventuella automatiseringar eller skript som anv\u00e4nder den h\u00e4r tj\u00e4nsten f\u00f6r att ist\u00e4llet anv\u00e4nda tj\u00e4nsten ` {alternate_service} ` med ett av dessa enhets-ID:n som m\u00e5l: ` {alternate_targets} `.", + "title": "Tj\u00e4nsten {deprecated_service} tas bort" + }, + "deprecated_service_single_alternate_target": { + "description": "Uppdatera eventuella automatiseringar eller skript som anv\u00e4nder den h\u00e4r tj\u00e4nsten f\u00f6r att ist\u00e4llet anv\u00e4nda tj\u00e4nsten ` {alternate_service} ` med ` {alternate_targets} ` som m\u00e5l.", + "title": "Tj\u00e4nsten {deprecated_service} tas bort" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radarr/translations/et.json b/homeassistant/components/radarr/translations/et.json new file mode 100644 index 00000000000..0f91bc2f47b --- /dev/null +++ b/homeassistant/components/radarr/translations/et.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge", + "wrong_app": "Vale rakendus. Palun proovi uuesti", + "zeroconf_failed": "API v\u00f5tit ei leitud. Sisesta see k\u00e4sitsi" + }, + "step": { + "reauth_confirm": { + "description": "Radarri integratsioon tuleb k\u00e4sitsi uuesti autentida Radarri API abil.", + "title": "Taastuvasta sidumine" + }, + "user": { + "data": { + "api_key": "API v\u00f5ti", + "url": "URL", + "verify_ssl": "Kontrolli SSL serte" + }, + "description": "API-v\u00f5tme saab automaatselt alla laadida, kui rakenduses pole sisselogimismandaate m\u00e4\u00e4ratud.\n API-v\u00f5tme leiate Radarri veebikasutajaliidese jaotisest Seaded > \u00dcldine." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Radarri seadistamine YAML-i abil eemaldatakse.\n\nTeie olemasolev YAML-i konfiguratsioon imporditakse kasutajaliidesesse automaatselt.\n\nEemaldage failist configuration.yaml radarr YAML konfiguratsioon ja taask\u00e4ivitage selle probleemi lahendamiseks Home Assistant.", + "title": "Radarr YAML-i konfiguratsiooni eemaldatakse" + }, + "removed_attributes": { + "description": "M\u00f5ned murrangulised muudatused on tehtud liikumiste arvuanduri v\u00e4ljal\u00fclitamisel ettevaatusabin\u00f5ude t\u00f5ttu.\n\nSee andur v\u00f5ib p\u00f5hjustada probleeme massiivsete andmebaaside puhul. Kui soovite seda siiski kasutada, v\u00f5ite seda teha.\n\nFilmide nimed ei ole enam filmide anduri atribuutidena lisatud.\n\nTulevikus on eemaldatud. Seda ajakohastatakse, nagu kalendrielemendid peaksid olema. Kettaruum on n\u00fc\u00fcd jagatud erinevateks anduriteks, \u00fcks iga kausta jaoks.\n\nStaatus ja k\u00e4sud on eemaldatud, kuna neil ei tundu olevat t\u00f5elist v\u00e4\u00e4rtust automaatika jaoks.", + "title": "Muudatused Radarri integratsioonis" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Kuvatavate eelseisvate p\u00e4evade arv" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/pl.json b/homeassistant/components/radarr/translations/pl.json new file mode 100644 index 00000000000..ff7092eaff0 --- /dev/null +++ b/homeassistant/components/radarr/translations/pl.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d", + "wrong_app": "Osi\u0105gni\u0119to nieprawid\u0142ow\u0105 aplikacj\u0119. Spr\u00f3buj ponownie", + "zeroconf_failed": "Nie znaleziono klucza API. Prosz\u0119 wpisa\u0107 go r\u0119cznie." + }, + "step": { + "reauth_confirm": { + "description": "Integracja Radarr musi zosta\u0107 r\u0119cznie ponownie uwierzytelniona za pomoc\u0105 interfejsu API Radarr", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, + "user": { + "data": { + "api_key": "Klucz API", + "url": "URL", + "verify_ssl": "Weryfikacja certyfikatu SSL" + }, + "description": "Klucz API mo\u017ce zosta\u0107 pobrany automatycznie, je\u015bli dane logowania nie zosta\u0142y ustawione w aplikacji.\nTw\u00f3j klucz API mo\u017cesz znale\u017a\u0107 w Ustawienia > Og\u00f3lne, na swoim koncie Radarr." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Radarr przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Radarr zostanie usuni\u0119ta" + }, + "removed_attributes": { + "description": "Z powodu ostro\u017cno\u015bci wprowadzono pewne prze\u0142omowe zmiany w wy\u0142\u0105czaniu sensora liczby film\u00f3w. \n\nTen sensor mo\u017ce powodowa\u0107 problemy z ogromnymi bazami danych. Je\u015bli nadal chcesz z niego korzysta\u0107, mo\u017cesz to zrobi\u0107. \n\n\"Nazwy film\u00f3w\" nie s\u0105 ju\u017c uwzgl\u0119dniane w atrybutach sensora film\u00f3w. \n\n\"Nadchodz\u0105ce\" zosta\u0142o usuni\u0119te. Jest unowocze\u015bniany tak, jak przysta\u0142o na kalendarz. \"Miejsce na dysku\" jest teraz podzielone na r\u00f3\u017cne sensory, po jednym dla ka\u017cdego folderu. \n\n\"Status\" i \"Polecenia\" zosta\u0142y usuni\u0119te, poniewa\u017c nie wydaj\u0105 si\u0119 mie\u0107 rzeczywistej warto\u015bci dla automatyzacji.", + "title": "Zmiany w integracji Radarr" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Liczba nadchodz\u0105cych dni do wy\u015bwietlenia" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/sv.json b/homeassistant/components/radarr/translations/sv.json new file mode 100644 index 00000000000..a5f84833bd9 --- /dev/null +++ b/homeassistant/components/radarr/translations/sv.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel", + "wrong_app": "Felaktig applikation har n\u00e5tts. F\u00f6rs\u00f6k igen", + "zeroconf_failed": "API-nyckeln har inte hittats. Ange den manuellt" + }, + "step": { + "reauth_confirm": { + "description": "Radarr-integrationen m\u00e5ste autentiseras manuellt med Radarr API", + "title": "\u00c5terautenticera integration" + }, + "user": { + "data": { + "api_key": "API-nyckel", + "url": "URL", + "verify_ssl": "Verifiera SSL-certifikat" + }, + "description": "API-nyckel kan h\u00e4mtas automatiskt om inloggningsuppgifter inte st\u00e4llts in i applikationen.\n Din API-nyckel finns i Inst\u00e4llningar > Allm\u00e4nt i Radarr Web UI." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Radarr med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Radarr YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Radarr YAML-konfigurationen tas bort" + }, + "removed_attributes": { + "description": "Vissa f\u00f6r\u00e4ndringar har gjorts f\u00f6r att inaktivera r\u00e4knesensorn f\u00f6r filmer av f\u00f6rsiktighet. \n\n Denna sensor kan orsaka problem med massiva databaser. Om du fortfarande vill anv\u00e4nda den kan du g\u00f6ra det. \n\n Filmnamn ing\u00e5r inte l\u00e4ngre som attribut i filmsensorn. \n\n Kommande har tagits bort. Det h\u00e5ller p\u00e5 att moderniseras som kalenderobjekt ska vara. Diskutrymme \u00e4r nu uppdelat i olika sensorer, en f\u00f6r varje mapp. \n\n Status och kommandon har tagits bort eftersom de inte verkar ha n\u00e5got verkligt v\u00e4rde f\u00f6r automatiseringar.", + "title": "\u00c4ndringar av Radarr-integrationen" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Antal kommande dagar att visa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/et.json b/homeassistant/components/rainmachine/translations/et.json index cad1284ad3d..0a9d6e007f1 100644 --- a/homeassistant/components/rainmachine/translations/et.json +++ b/homeassistant/components/rainmachine/translations/et.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "V\u00e4rskendage automaatikaid v\u00f5i skripte, mis seda olemit kasutavad, et kasutada selle asemel '{replacement_entity_id}'.", + "title": "\u00dcksus {old_entity_id} eemaldatakse" + } + } + }, + "title": "\u00dcksus {old_entity_id} eemaldatakse" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/pl.json b/homeassistant/components/rainmachine/translations/pl.json index 665152e8e0f..e96ccd64ef0 100644 --- a/homeassistant/components/rainmachine/translations/pl.json +++ b/homeassistant/components/rainmachine/translations/pl.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Zaktualizuj wszystkie automatyzacje lub skrypty, kt\u00f3re u\u017cywaj\u0105 tej encji, aby zamiast tego u\u017cywa\u0142y `{replacement_entity_id}`.", + "title": "Encja {old_entity_id} zostanie usuni\u0119ta" + } + } + }, + "title": "Encja {old_entity_id} zostanie usuni\u0119ta" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/sv.json b/homeassistant/components/rainmachine/translations/sv.json index 10e06693207..9cf860dee34 100644 --- a/homeassistant/components/rainmachine/translations/sv.json +++ b/homeassistant/components/rainmachine/translations/sv.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Uppdatera alla automatiseringar eller skript som anv\u00e4nder denna enhet f\u00f6r att ist\u00e4llet anv\u00e4nda ` {replacement_entity_id} `.", + "title": "{old_entity_id} kommer att tas bort" + } + } + }, + "title": "{old_entity_id} kommer att tas bort" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/roomba/translations/de.json b/homeassistant/components/roomba/translations/de.json index 1717c07a735..89f280b1e94 100644 --- a/homeassistant/components/roomba/translations/de.json +++ b/homeassistant/components/roomba/translations/de.json @@ -12,14 +12,14 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "Halte die Home-Taste von {name} gedr\u00fcckt, bis das Ger\u00e4t einen Ton erzeugt (ca. zwei Sekunden) und sende die Best\u00e4tigung innerhalb von 30 Sekunden ab.", + "description": "Stelle sicher, dass die iRobot-App auf keinem Ger\u00e4t ausgef\u00fchrt wird. Halte die Home-Taste auf {name} gedr\u00fcckt, bis das Ger\u00e4t einen Ton erzeugt (etwa zwei Sekunden) und best\u00e4tige dann innerhalb von 30 Sekunden.", "title": "Passwort abrufen" }, "link_manual": { "data": { "password": "Passwort" }, - "description": "Das Passwort konnte nicht automatisch vom Ger\u00e4t abgerufen werden. Bitte die in der Dokumentation beschriebenen Schritte unter {auth_help_url} befolgen", + "description": "Das Passwort konnte nicht automatisch vom Ger\u00e4t abgerufen werden. Bitte stelle sicher, dass die iRobot-App auf keinem Ger\u00e4t ge\u00f6ffnet ist, w\u00e4hrend du versuchst, das Passwort abzurufen. Bitte befolge die Schritte in der Dokumentation unter: {auth_help_url}", "title": "Passwort eingeben" }, "manual": { diff --git a/homeassistant/components/roomba/translations/en.json b/homeassistant/components/roomba/translations/en.json index 703ccebbb11..396bca9e8ef 100644 --- a/homeassistant/components/roomba/translations/en.json +++ b/homeassistant/components/roomba/translations/en.json @@ -12,14 +12,14 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "Press and hold the Home button on {name} until the device generates a sound (about two seconds), then submit within 30 seconds.", + "description": "Make sure that the iRobot app is not running on any device. Press and hold the Home button on {name} until the device generates a sound (about two seconds), then submit within 30 seconds.", "title": "Retrieve Password" }, "link_manual": { "data": { "password": "Password" }, - "description": "The password could not be retrieved from the device automatically. Please follow the steps outlined in the documentation at: {auth_help_url}", + "description": "The password could not be retrieved from the device automatically. Please make sure that the iRobot app is not open on any device while trying to retrieve the password. Please follow the steps outlined in the documentation at: {auth_help_url}", "title": "Enter Password" }, "manual": { diff --git a/homeassistant/components/roomba/translations/es.json b/homeassistant/components/roomba/translations/es.json index 0744bf05620..6e7b3a6d2c0 100644 --- a/homeassistant/components/roomba/translations/es.json +++ b/homeassistant/components/roomba/translations/es.json @@ -12,14 +12,14 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "Mant\u00e9n pulsado el bot\u00f3n Inicio en {name} hasta que el dispositivo genere un sonido (alrededor de dos segundos), luego haz clic en enviar en los siguientes 30 segundos.", + "description": "Aseg\u00farate de que la aplicaci\u00f3n iRobot no se est\u00e9 ejecutando en ning\u00fan dispositivo. Mant\u00e9n presionado el bot\u00f3n Inicio en {name} hasta que el dispositivo genere un sonido (alrededor de dos segundos), luego pulsa Enviar dentro de los 30 segundos siguientes.", "title": "Recuperar Contrase\u00f1a" }, "link_manual": { "data": { "password": "Contrase\u00f1a" }, - "description": "La contrase\u00f1a no se pudo recuperar del dispositivo autom\u00e1ticamente. Por favor, sigue los pasos descritos en la documentaci\u00f3n en: {auth_help_url}", + "description": "La contrase\u00f1a no se pudo recuperar del dispositivo autom\u00e1ticamente. Por favor, aseg\u00farate de que la aplicaci\u00f3n iRobot no est\u00e9 abierta en ning\u00fan dispositivo mientras intentas recuperar la contrase\u00f1a. Sigue los pasos descritos en la documentaci\u00f3n en: {auth_help_url}", "title": "Introduce la contrase\u00f1a" }, "manual": { diff --git a/homeassistant/components/roomba/translations/id.json b/homeassistant/components/roomba/translations/id.json index 9b8e9ef8bbe..ff2c3acad2c 100644 --- a/homeassistant/components/roomba/translations/id.json +++ b/homeassistant/components/roomba/translations/id.json @@ -12,14 +12,14 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "Tekan dan tahan tombol Home pada {name} hingga perangkat mengeluarkan suara (sekitar dua detik), lalu kirim dalam waktu 30 detik.", + "description": "Pastikan aplikasi iRobot tidak berjalan pada semua perangkat. Tekan dan tahan tombol Home pada {name} hingga perangkat mengeluarkan suara (sekitar dua detik), lalu klik KIRIM dalam waktu 30 detik.", "title": "Ambil Kata Sandi" }, "link_manual": { "data": { "password": "Kata Sandi" }, - "description": "Kata sandi tidak dapat diambil dari perangkat secara otomatis. Ikuti langkah-langkah yang diuraikan dalam dokumentasi di: {auth_help_url}", + "description": "Kata sandi tidak dapat diambil dari perangkat secara otomatis. Pastikan aplikasi iRobot tidak terbuka di semua perangkat saat mencoba mengambil kata sandi. Ikuti langkah-langkah yang diuraikan dalam dokumentasi di: {auth_help_url}", "title": "Masukkan Kata Sandi" }, "manual": { diff --git a/homeassistant/components/roomba/translations/no.json b/homeassistant/components/roomba/translations/no.json index f378bfb127c..ccb50b901bb 100644 --- a/homeassistant/components/roomba/translations/no.json +++ b/homeassistant/components/roomba/translations/no.json @@ -12,14 +12,14 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "Trykk og hold nede Hjem-knappen p\u00e5 {name} til enheten genererer en lyd (omtrent to sekunder), og send deretter innen 30 sekunder.", + "description": "Pass p\u00e5 at iRobot-appen ikke kj\u00f8rer p\u00e5 noen enhet. Trykk og hold Hjem-knappen p\u00e5 {name} til enheten genererer en lyd (omtrent to sekunder), og send deretter inn innen 30 sekunder.", "title": "Hent passord" }, "link_manual": { "data": { "password": "Passord" }, - "description": "Passordet kan ikke hentes fra enheten automatisk. F\u00f8lg trinnene som er beskrevet i dokumentasjonen p\u00e5: {auth_help_url}", + "description": "Passordet kunne ikke hentes fra enheten automatisk. S\u00f8rg for at iRobot-appen ikke er \u00e5pen p\u00e5 noen enhet mens du pr\u00f8ver \u00e5 hente passordet. F\u00f8lg trinnene som er beskrevet i dokumentasjonen p\u00e5: {auth_help_url}", "title": "Skriv inn passord" }, "manual": { diff --git a/homeassistant/components/roomba/translations/pl.json b/homeassistant/components/roomba/translations/pl.json index a3440c97ed2..bdfe8417ba7 100644 --- a/homeassistant/components/roomba/translations/pl.json +++ b/homeassistant/components/roomba/translations/pl.json @@ -12,14 +12,14 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "Naci\u015bnij i przytrzymaj przycisk Home na {name} a\u017c urz\u0105dzenie wygeneruje d\u017awi\u0119k (oko\u0142o dwie sekundy), a nast\u0119pnie zatwierd\u017a w ci\u0105gu 30 sekund.", + "description": "Upewnij si\u0119, \u017ce aplikacja iRobot nie jest uruchomiona na \u017cadnym urz\u0105dzeniu. Naci\u015bnij i przytrzymaj przycisk Home na {name} a\u017c urz\u0105dzenie wygeneruje d\u017awi\u0119k (oko\u0142o dwie sekundy), a nast\u0119pnie zatwierd\u017a w ci\u0105gu 30 sekund.", "title": "Odzyskiwanie has\u0142a" }, "link_manual": { "data": { "password": "Has\u0142o" }, - "description": "Nie mo\u017cna automatycznie pobra\u0107 has\u0142a z urz\u0105dzenia. Post\u0119puj zgodnie z instrukcjami podanymi w dokumentacji pod adresem: {auth_help_url}", + "description": "Nie mo\u017cna automatycznie pobra\u0107 has\u0142a z urz\u0105dzenia. Upewnij si\u0119, \u017ce aplikacja iRobot nie jest otwarta na \u017cadnym urz\u0105dzeniu podczas pr\u00f3by odzyskania has\u0142a. Post\u0119puj zgodnie z instrukcjami podanymi w dokumentacji pod adresem: {auth_help_url}", "title": "Wprowad\u017a has\u0142o" }, "manual": { diff --git a/homeassistant/components/roomba/translations/pt-BR.json b/homeassistant/components/roomba/translations/pt-BR.json index 4db4e885ac7..a9642707d51 100644 --- a/homeassistant/components/roomba/translations/pt-BR.json +++ b/homeassistant/components/roomba/translations/pt-BR.json @@ -12,14 +12,14 @@ "flow_title": "{name} ( {host} )", "step": { "link": { - "description": "Pressione e segure o bot\u00e3o Home em {name} at\u00e9 que o dispositivo gere um som (cerca de dois segundos) e envie em 30 segundos.", + "description": "Certifique-se de que o aplicativo iRobot n\u00e3o esteja sendo executado em nenhum dispositivo. Pressione e segure o bot\u00e3o In\u00edcio em {name} at\u00e9 que o dispositivo gere um som (cerca de dois segundos) e envie em 30 segundos.", "title": "Recuperar Senha" }, "link_manual": { "data": { "password": "Senha" }, - "description": "A senha do dispositivo n\u00e3o p\u00f4de ser recuperada automaticamente. Siga as etapas descritas na documenta\u00e7\u00e3o em: {auth_help_url}", + "description": "A senha n\u00e3o p\u00f4de ser recuperada do dispositivo automaticamente. Certifique-se de que o aplicativo iRobot n\u00e3o esteja aberto em nenhum dispositivo ao tentar recuperar a senha. Siga as etapas descritas na documenta\u00e7\u00e3o em: {auth_help_url}", "title": "Digite a senha" }, "manual": { diff --git a/homeassistant/components/roomba/translations/zh-Hant.json b/homeassistant/components/roomba/translations/zh-Hant.json index d0c59422625..c7cf9ae7b2d 100644 --- a/homeassistant/components/roomba/translations/zh-Hant.json +++ b/homeassistant/components/roomba/translations/zh-Hant.json @@ -12,14 +12,14 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "\u8acb\u6309\u4f4f {name} \u4e0a\u7684 Home \u9375\u76f4\u5230\u88dd\u7f6e\u767c\u51fa\u8072\u97f3\uff08\u7d04\u5169\u79d2\uff09\uff0c\u7136\u5f8c\u65bc 30 \u79d2\u5167\u50b3\u9001\u3002", + "description": "\u8acb\u78ba\u5b9a\u672a\u5728\u5176\u4ed6\u88dd\u7f6e\u958b\u555f iRobot App\u3002\u8acb\u6309\u4f4f {name} \u4e0a\u7684 Home \u9375\u76f4\u5230\u88dd\u7f6e\u767c\u51fa\u8072\u97f3\uff08\u7d04\u5169\u79d2\uff09\uff0c\u7136\u5f8c\u65bc 30 \u79d2\u5167\u50b3\u9001\u3002", "title": "\u91cd\u7f6e\u5bc6\u78bc" }, "link_manual": { "data": { "password": "\u5bc6\u78bc" }, - "description": "\u5bc6\u78bc\u53ef\u81ea\u52d5\u81ea\u88dd\u7f6e\u4e0a\u53d6\u5f97\u3002\u8acb\u53c3\u95b1\u4ee5\u4e0b\u6587\u4ef6\u7684\u6b65\u9a5f\u9032\u884c\u8a2d\u5b9a\uff1a{auth_help_url}", + "description": "\u5bc6\u78bc\u53ef\u81ea\u52d5\u81ea\u88dd\u7f6e\u4e0a\u53d6\u5f97\u3002\u8acb\u78ba\u5b9a\u65bc\u53d6\u5f97\u5bc6\u78bc\u6642\uff0c\u672a\u5728\u5176\u4ed6\u88dd\u7f6e\u958b\u555f iRobot App\u3002\u8acb\u53c3\u95b1\u4ee5\u4e0b\u6587\u4ef6\u7684\u6b65\u9a5f\u9032\u884c\u8a2d\u5b9a\uff1a{auth_help_url}", "title": "\u8f38\u5165\u5bc6\u78bc" }, "manual": { diff --git a/homeassistant/components/rtsp_to_webrtc/translations/pl.json b/homeassistant/components/rtsp_to_webrtc/translations/pl.json index 25f3ffe785f..3ed933d098f 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/pl.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/pl.json @@ -12,7 +12,7 @@ }, "step": { "hassio_confirm": { - "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby \u0142\u0105czy\u0142 si\u0119 z serwerem RTSPtoWebRTC dostarczonym przez dodatek: {addon} ?", + "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby \u0142\u0105czy\u0142 si\u0119 z serwerem RTSPtoWebRTC dostarczonym przez dodatek: {addon}?", "title": "RTSPtoWebRTC poprzez dodatek Home Assistant" }, "user": { diff --git a/homeassistant/components/season/translations/ca.json b/homeassistant/components/season/translations/ca.json index b12e0e3019c..6695356f33a 100644 --- a/homeassistant/components/season/translations/ca.json +++ b/homeassistant/components/season/translations/ca.json @@ -10,5 +10,10 @@ } } } + }, + "issues": { + "removed_yaml": { + "title": "La configuraci\u00f3 YAML de Season s'ha eliminat" + } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/et.json b/homeassistant/components/season/translations/et.json index 6c11c8136e1..60abe6adf65 100644 --- a/homeassistant/components/season/translations/et.json +++ b/homeassistant/components/season/translations/et.json @@ -10,5 +10,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Season seadistamine YAML-i abil on eemaldatud. \n\n Koduassistent ei kasuta teie olemasolevat YAML-i konfiguratsiooni. \n\n Selle probleemi lahendamiseks eemaldage YAML-i konfiguratsioon failist configuration.yaml ja taask\u00e4ivitage Home Assistant.", + "title": "Sidumise Season YAML-i konfiguratsioon on eemaldatud" + } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/hu.json b/homeassistant/components/season/translations/hu.json index 11bbd17ad6c..32a3e7c77b6 100644 --- a/homeassistant/components/season/translations/hu.json +++ b/homeassistant/components/season/translations/hu.json @@ -10,5 +10,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "A Season konfigur\u00e1l\u00e1sa YAML haszn\u00e1lat\u00e1val elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3t a Home Assistant nem haszn\u00e1lja.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Season YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fclt" + } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/id.json b/homeassistant/components/season/translations/id.json index ef7de1c9d3a..0b557ccaabb 100644 --- a/homeassistant/components/season/translations/id.json +++ b/homeassistant/components/season/translations/id.json @@ -10,5 +10,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Proses konfigurasi Integrasi Musim lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Integrasi Musim telah dihapus" + } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/no.json b/homeassistant/components/season/translations/no.json index 2c177da8227..1b1c0f332e5 100644 --- a/homeassistant/components/season/translations/no.json +++ b/homeassistant/components/season/translations/no.json @@ -10,5 +10,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Konfigurering av sesong med YAML er fjernet. \n\n Din eksisterende YAML-konfigurasjon brukes ikke av Home Assistant. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Season YAML-konfigurasjonen er fjernet" + } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/pl.json b/homeassistant/components/season/translations/pl.json index bef7f92841d..21342aeb8b8 100644 --- a/homeassistant/components/season/translations/pl.json +++ b/homeassistant/components/season/translations/pl.json @@ -10,5 +10,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Konfiguracja Sezon\u00f3w za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistant, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Sezon\u00f3w zosta\u0142a usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/pt-BR.json b/homeassistant/components/season/translations/pt-BR.json index aa4f7601808..bc61719f1b1 100644 --- a/homeassistant/components/season/translations/pt-BR.json +++ b/homeassistant/components/season/translations/pt-BR.json @@ -10,5 +10,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o da Season usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML da Season foi removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/sv.json b/homeassistant/components/season/translations/sv.json index 649789e560e..5a8e28e1e07 100644 --- a/homeassistant/components/season/translations/sv.json +++ b/homeassistant/components/season/translations/sv.json @@ -10,5 +10,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Konfigurering av s\u00e4song med YAML har tagits bort. \n\n Din befintliga YAML-konfiguration anv\u00e4nds inte av Home Assistant. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "S\u00e4song YAML-konfigurationen har tagits bort" + } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/ca.json b/homeassistant/components/sensor/translations/ca.json index df4bcff1c53..2c89081e0bd 100644 --- a/homeassistant/components/sensor/translations/ca.json +++ b/homeassistant/components/sensor/translations/ca.json @@ -6,6 +6,7 @@ "is_carbon_dioxide": "Concentraci\u00f3 actual de di\u00f2xid de carboni de {entity_name}", "is_carbon_monoxide": "Concentraci\u00f3 actual de mon\u00f2xid de carboni de {entity_name}", "is_current": "Intensitat actual de {entity_name}", + "is_distance": "Dist\u00e0ncia actual de {entity_name}", "is_energy": "Energia actual de {entity_name}", "is_frequency": "Freq\u00fc\u00e8ncia actual de {entity_name}", "is_gas": "Gas actual de {entity_name}", @@ -24,11 +25,14 @@ "is_pressure": "Pressi\u00f3 actual de {entity_name}", "is_reactive_power": "Pot\u00e8ncia reactiva actual de {entity_name}", "is_signal_strength": "Pot\u00e8ncia de senyal actual de {entity_name}", + "is_speed": "Velocitat actual de {entity_name}", "is_sulphur_dioxide": "Concentraci\u00f3 actual de di\u00f2xid de sofre de {entity_name}", "is_temperature": "Temperatura actual de {entity_name}", "is_value": "Valor actual de {entity_name}", "is_volatile_organic_compounds": "Concentraci\u00f3 actual de compostos org\u00e0nics vol\u00e0tils de {entity_name}", - "is_voltage": "Voltatge actual de {entity_name}" + "is_voltage": "Voltatge actual de {entity_name}", + "is_volume": "Volum actual de {entity_name}", + "is_weight": "Pes actual de {entity_name}" }, "trigger_type": { "apparent_power": "Canvia la pot\u00e8ncia aparent de {entity_name}", @@ -36,6 +40,7 @@ "carbon_dioxide": "Canvia la concentraci\u00f3 de di\u00f2xid de carboni de {entity_name}", "carbon_monoxide": "Canvia la concentraci\u00f3 de mon\u00f2xid de carboni de {entity_name}", "current": "Canvia la intensitat de {entity_name}", + "distance": "Canvia la dist\u00e0ncia de {entity_name}", "energy": "Canvia l'energia de {entity_name}", "frequency": "Canvia la freq\u00fc\u00e8ncia de {entity_name}", "gas": "Canvia el gas de {entity_name}", @@ -54,11 +59,14 @@ "pressure": "Canvia la pressi\u00f3 de {entity_name}", "reactive_power": "Canvia la pot\u00e8ncia reactiva de {entity_name}", "signal_strength": "Canvia la pot\u00e8ncia de senyal de {entity_name}", + "speed": "Canvia la velocitat de {entity_name}", "sulphur_dioxide": "Canvia la concentraci\u00f3 de di\u00f2xid de sofre de {entity_name}", "temperature": "Canvia la temperatura de {entity_name}", "value": "Canvia el valor de {entity_name}", "volatile_organic_compounds": "Canvia la concentraci\u00f3 de compostos org\u00e0nics vol\u00e0tils de {entity_name}", - "voltage": "Canvia el voltatge de {entity_name}" + "voltage": "Canvia el voltatge de {entity_name}", + "volume": "Canvia el volum de {entity_name}", + "weight": "Canvia el pes de {entity_name}" } }, "state": { diff --git a/homeassistant/components/sensor/translations/de.json b/homeassistant/components/sensor/translations/de.json index b0cdbd198aa..378489203d9 100644 --- a/homeassistant/components/sensor/translations/de.json +++ b/homeassistant/components/sensor/translations/de.json @@ -6,6 +6,7 @@ "is_carbon_dioxide": "Aktuelle {entity_name} Kohlenstoffdioxid-Konzentration", "is_carbon_monoxide": "Aktuelle {entity_name} Kohlenstoffmonoxid-Konzentration", "is_current": "Aktueller Strom von {entity_name}", + "is_distance": "Aktuelle Entfernung zu {entity_name}", "is_energy": "Aktuelle Energie von {entity_name}", "is_frequency": "Aktuelle {entity_name} Frequenz", "is_gas": "Aktuelles {entity_name} Gas", @@ -24,11 +25,14 @@ "is_pressure": "{entity_name} Druck", "is_reactive_power": "Aktuelle Blindleistung von {entity_name}", "is_signal_strength": "Aktuelle {entity_name} Signalst\u00e4rke", + "is_speed": "Aktuelle Geschwindigkeit von {entity_name}", "is_sulphur_dioxide": "Aktuelle Schwefeldioxid-Konzentration von {entity_name}", "is_temperature": "Aktuelle {entity_name} Temperatur", "is_value": "Aktueller {entity_name} Wert", "is_volatile_organic_compounds": "Aktuelle Konzentration fl\u00fcchtiger organischer Verbindungen in {entity_name}", - "is_voltage": "Aktuelle Spannung von {entity_name}" + "is_voltage": "Aktuelle Spannung von {entity_name}", + "is_volume": "Aktuelle Lautst\u00e4rke von {entity_name}", + "is_weight": "Aktuelles Gewicht von {entity_name}" }, "trigger_type": { "apparent_power": "{entity_name} \u00c4nderungen der Scheinleistung", @@ -36,6 +40,7 @@ "carbon_dioxide": "{entity_name} Kohlenstoffdioxid-Konzentrations\u00e4nderung", "carbon_monoxide": "{entity_name} Kohlenstoffmonoxid-Konzentrations\u00e4nderung", "current": "{entity_name} Stromver\u00e4nderung", + "distance": "Abstand zu {entity_name} \u00e4ndert sich", "energy": "{entity_name} Energie\u00e4nderungen", "frequency": "{entity_name} Frequenz\u00e4nderungen", "gas": "{entity_name} Gas\u00e4nderungen", @@ -54,11 +59,14 @@ "pressure": "{entity_name} Druck\u00e4nderungen", "reactive_power": "{entity_name} Blindleistung \u00e4ndert sich", "signal_strength": "{entity_name} Signalst\u00e4rke\u00e4nderungen", + "speed": "Geschwindigkeit von {entity_name} \u00e4ndert sich", "sulphur_dioxide": "\u00c4nderung der Schwefeldioxidkonzentration bei {entity_name}", "temperature": "{entity_name} Temperatur\u00e4nderungen", "value": "{entity_name} Wert\u00e4nderungen", "volatile_organic_compounds": "{entity_name} Konzentrations\u00e4nderungen fl\u00fcchtiger organischer Verbindungen", - "voltage": "{entity_name} Spannungs\u00e4nderungen" + "voltage": "{entity_name} Spannungs\u00e4nderungen", + "volume": "Lautst\u00e4rke von {entity_name} \u00e4ndert sich", + "weight": "Das Gewicht von {entity_name} \u00e4ndert sich" } }, "state": { diff --git a/homeassistant/components/sensor/translations/es.json b/homeassistant/components/sensor/translations/es.json index d005742fee4..67008424f0d 100644 --- a/homeassistant/components/sensor/translations/es.json +++ b/homeassistant/components/sensor/translations/es.json @@ -6,6 +6,7 @@ "is_carbon_dioxide": "El nivel de la concentraci\u00f3n de di\u00f3xido de carbono actual de {entity_name}", "is_carbon_monoxide": "El nivel de la concentraci\u00f3n de mon\u00f3xido de carbono actual de {entity_name}", "is_current": "La intensidad de corriente actual de {entity_name}", + "is_distance": "Distancia actual de {entity_name}", "is_energy": "La energ\u00eda actual de {entity_name}", "is_frequency": "La frecuencia actual de {entity_name}", "is_gas": "El gas actual de {entity_name}", @@ -24,11 +25,14 @@ "is_pressure": "La presi\u00f3n actual de {entity_name}", "is_reactive_power": "La potencia reactiva actual de {entity_name}", "is_signal_strength": "La intensidad de la se\u00f1al actual de {entity_name}", + "is_speed": "Velocidad actual de {entity_name}", "is_sulphur_dioxide": "El nivel de la concentraci\u00f3n de di\u00f3xido de azufre actual de {entity_name}", "is_temperature": "La temperatura actual de {entity_name}", "is_value": "El valor actual de {entity_name}", "is_volatile_organic_compounds": "El nivel de la concentraci\u00f3n de compuestos org\u00e1nicos vol\u00e1tiles actual de {entity_name}", - "is_voltage": "El voltaje actual de {entity_name}" + "is_voltage": "El voltaje actual de {entity_name}", + "is_volume": "Volumen actual de {entity_name}", + "is_weight": "El peso actual de {entity_name}" }, "trigger_type": { "apparent_power": "La potencia aparente de {entity_name} cambia", @@ -36,6 +40,7 @@ "carbon_dioxide": "La concentraci\u00f3n de di\u00f3xido de carbono de {entity_name} cambia", "carbon_monoxide": "La concentraci\u00f3n de mon\u00f3xido de carbono de {entity_name} cambia", "current": "La intensidad de corriente de {entity_name} cambia", + "distance": "La distancia de {entity_name} cambia", "energy": "La energ\u00eda de {entity_name} cambia", "frequency": "La frecuencia de {entity_name} cambia", "gas": "El gas de {entity_name} cambia", @@ -54,11 +59,14 @@ "pressure": "La presi\u00f3n de {entity_name} cambia", "reactive_power": "La potencia reactiva de {entity_name} cambia", "signal_strength": "La intensidad de se\u00f1al de {entity_name} cambia", + "speed": "La velocidad de {entity_name} cambia", "sulphur_dioxide": "La concentraci\u00f3n de di\u00f3xido de azufre de {entity_name} cambia", "temperature": "La temperatura de {entity_name} cambia", "value": "El valor de {entity_name} cambia", "volatile_organic_compounds": "La concentraci\u00f3n de compuestos org\u00e1nicos vol\u00e1tiles de {entity_name} cambia", - "voltage": "El voltaje de {entity_name} cambia" + "voltage": "El voltaje de {entity_name} cambia", + "volume": "El volumen de {entity_name} cambia", + "weight": "El peso de {entity_name} cambia" } }, "state": { diff --git a/homeassistant/components/sensor/translations/et.json b/homeassistant/components/sensor/translations/et.json index bbc6880dcee..3519586809e 100644 --- a/homeassistant/components/sensor/translations/et.json +++ b/homeassistant/components/sensor/translations/et.json @@ -6,6 +6,7 @@ "is_carbon_dioxide": "{entity_name} praegune s\u00fcsihappegaasi tase", "is_carbon_monoxide": "{entity_name} praegune vingugaasi tase", "is_current": "Praegune {entity_name} voolutugevus", + "is_distance": "Praegune {entity_name} kaugus", "is_energy": "Praegune {entity_name} v\u00f5imsus", "is_frequency": "Praegune {entity_name} sagedus", "is_gas": "Praegune {entity_name} gaas", @@ -24,11 +25,13 @@ "is_pressure": "Praegune {entity_name} r\u00f5hk", "is_reactive_power": "Praegune {entity_name} reaktiivv\u00f5imsus", "is_signal_strength": "Praegune {entity_name} signaali tugevus", + "is_speed": "Praegune {entity_name} kiirus", "is_sulphur_dioxide": "Praegune v\u00e4\u00e4veldioksiidi kontsentratsioonitase {entity_name}", "is_temperature": "Praegune {entity_name} temperatuur", "is_value": "Praegune {entity_name} v\u00e4\u00e4rtus", "is_volatile_organic_compounds": "Praegune {entity_name} lenduvate orgaaniliste \u00fchendite kontsentratsioonitase", - "is_voltage": "Praegune {entity_name}pinge" + "is_voltage": "Praegune {entity_name}pinge", + "is_volume": "Praegune {entity_name} helitugevus" }, "trigger_type": { "apparent_power": "{entity_name} n\u00e4iv v\u00f5imsus muutub", @@ -36,6 +39,7 @@ "carbon_dioxide": "{entity_name} s\u00fcsihappegaasi tase muutus", "carbon_monoxide": "{entity_name} vingugaasi tase muutus", "current": "{entity_name} voolutugevus muutub", + "distance": "{entity_name} kaugus muutub", "energy": "{entity_name} v\u00f5imsus muutub", "frequency": "{entity_name} sagedus muutub", "gas": "{entity_name} gaasivahetus", @@ -54,11 +58,13 @@ "pressure": "{entity_name} r\u00f5hk muutub", "reactive_power": "{entity_name} reaktiivv\u00f5imsus muutub", "signal_strength": "{entity_name} signaalitugevus muutub", + "speed": "{entity_name} kiirus muutub", "sulphur_dioxide": "{entity_name} v\u00e4\u00e4veldioksiidi kontsentratsiooni muutused", "temperature": "{entity_name} temperatuur muutub", "value": "{entity_name} v\u00e4\u00e4rtus muutub", "volatile_organic_compounds": "{entity_name} lenduvate orgaaniliste \u00fchendite kontsentratsiooni muutused", - "voltage": "{entity_name} pingemuutub" + "voltage": "{entity_name} pingemuutub", + "volume": "{entity_name} helitugevus muutub" } }, "state": { diff --git a/homeassistant/components/sensor/translations/id.json b/homeassistant/components/sensor/translations/id.json index 81f1126591d..8011fed4e3b 100644 --- a/homeassistant/components/sensor/translations/id.json +++ b/homeassistant/components/sensor/translations/id.json @@ -6,6 +6,7 @@ "is_carbon_dioxide": "Level konsentasi karbondioksida {entity_name} saat ini", "is_carbon_monoxide": "Level konsentasi karbonmonoksida {entity_name} saat ini", "is_current": "Arus {entity_name} saat ini", + "is_distance": "Jarak {entity_name} saat ini", "is_energy": "Energi {entity_name} saat ini", "is_frequency": "Frekuensi {entity_name} saat ini", "is_gas": "Gas {entity_name} saat ini", @@ -24,11 +25,13 @@ "is_pressure": "Tekanan {entity_name} saat ini", "is_reactive_power": "Daya reaktif {entity_name}", "is_signal_strength": "Kekuatan sinyal {entity_name} saat ini", + "is_speed": "Kecepatan {entity_name} saat ini", "is_sulphur_dioxide": "Tingkat konsentrasi sulfur dioksida {entity_name} saat ini", "is_temperature": "Suhu {entity_name} saat ini", "is_value": "Nilai {entity_name} saat ini", "is_volatile_organic_compounds": "Tingkat konsentrasi senyawa organik volatil {entity_name} saat ini", - "is_voltage": "Tegangan {entity_name} saat ini" + "is_voltage": "Tegangan {entity_name} saat ini", + "is_volume": "Volume {entity_name} saat ini" }, "trigger_type": { "apparent_power": "Perubahan daya nyata {entity_name}", @@ -36,6 +39,7 @@ "carbon_dioxide": "Perubahan konsentrasi karbondioksida {entity_name}", "carbon_monoxide": "Perubahan konsentrasi karbonmonoksida {entity_name}", "current": "Perubahan arus {entity_name}", + "distance": "Perubahan jarak {entity_name}", "energy": "Perubahan energi {entity_name}", "frequency": "Perubahan frekuensi {entity_name}", "gas": "Perubahan gas {entity_name}", @@ -54,11 +58,13 @@ "pressure": "Perubahan tekanan {entity_name}", "reactive_power": "Perubahan daya reaktif {entity_name}", "signal_strength": "Perubahan kekuatan sinyal {entity_name}", + "speed": "Perubahan kecepatan {nama_entitas}", "sulphur_dioxide": "Perubahan konsentrasi sulfur dioksida {entity_name}", "temperature": "Perubahan suhu {entity_name}", "value": "Perubahan nilai {entity_name}", "volatile_organic_compounds": "Perubahan konsentrasi senyawa organik volatil {entity_name}", - "voltage": "Perubahan tegangan {entity_name}" + "voltage": "Perubahan tegangan {entity_name}", + "volume": "Perubahan volume {entity_name}" } }, "state": { diff --git a/homeassistant/components/sensor/translations/no.json b/homeassistant/components/sensor/translations/no.json index e5b9c70846f..3fb1c7a793d 100644 --- a/homeassistant/components/sensor/translations/no.json +++ b/homeassistant/components/sensor/translations/no.json @@ -6,6 +6,7 @@ "is_carbon_dioxide": "Gjeldende {entity_name} karbondioksidkonsentrasjonsniv\u00e5", "is_carbon_monoxide": "Gjeldende {entity_name} karbonmonoksid konsentrasjonsniv\u00e5", "is_current": "Gjeldende {entity_name} str\u00f8m", + "is_distance": "Gjeldende avstand til {entity_name}", "is_energy": "Gjeldende {entity_name} effekt", "is_frequency": "Gjeldende {entity_name} -frekvens", "is_gas": "Gjeldende {entity_name} gass", @@ -24,11 +25,13 @@ "is_pressure": "Gjeldende {entity_name} trykk", "is_reactive_power": "N\u00e5v\u00e6rende reaktiv effekt for {entity_name}", "is_signal_strength": "Gjeldende {entity_name} signalstyrke", + "is_speed": "Gjeldende hastighet {entity_name}", "is_sulphur_dioxide": "Gjeldende konsentrasjonsniv\u00e5 for svoveldioksid for {entity_name}", "is_temperature": "Gjeldende {entity_name} temperatur", "is_value": "Gjeldende {entity_name} verdi", "is_volatile_organic_compounds": "Gjeldende {entity_name} flyktige organiske forbindelser", - "is_voltage": "Gjeldende {entity_name} spenning" + "is_voltage": "Gjeldende {entity_name} spenning", + "is_volume": "Gjeldende {entity_name} -volum" }, "trigger_type": { "apparent_power": "{entity_name} tilsynelatende kraftendringer", @@ -36,6 +39,7 @@ "carbon_dioxide": "{entity_name} endringer i konsentrasjonen av karbondioksid", "carbon_monoxide": "{entity_name} endringer i konsentrasjonen av karbonmonoksid", "current": "{entity_name} gjeldende endringer", + "distance": "{entity_name} avstandsendringer", "energy": "{entity_name} effektendringer", "frequency": "{entity_name} frekvensendringer", "gas": "{entity_name} gass endres", @@ -54,11 +58,13 @@ "pressure": "{entity_name} trykk endringer", "reactive_power": "{entity_name} endringer i reaktiv effekt", "signal_strength": "{entity_name} signalstyrkeendringer", + "speed": "{entity_name} hastighetsendringer", "sulphur_dioxide": "{entity_name} svoveldioksidkonsentrasjon endres", "temperature": "{entity_name} temperaturendringer", "value": "{entity_name} verdi endringer", "volatile_organic_compounds": "{entity_name} konsentrasjon av flyktige organiske forbindelser", - "voltage": "{entity_name} spenningsendringer" + "voltage": "{entity_name} spenningsendringer", + "volume": "{entity_name} volumendringer" } }, "state": { diff --git a/homeassistant/components/sensor/translations/pl.json b/homeassistant/components/sensor/translations/pl.json index 360d7a2509b..3ac3f849a6a 100644 --- a/homeassistant/components/sensor/translations/pl.json +++ b/homeassistant/components/sensor/translations/pl.json @@ -6,6 +6,7 @@ "is_carbon_dioxide": "obecny poziom st\u0119\u017cenia dwutlenku w\u0119gla w {entity_name}", "is_carbon_monoxide": "obecny poziom st\u0119\u017cenia tlenku w\u0119gla w {entity_name}", "is_current": "obecne nat\u0119\u017cenie pr\u0105du {entity_name}", + "is_distance": "obecna odleg\u0142o\u015b\u0107 {entity_name}", "is_energy": "obecna energia {entity_name}", "is_frequency": "obecna cz\u0119stotliwo\u015b\u0107 {entity_name}", "is_gas": "obecny poziom gazu {entity_name}", @@ -24,11 +25,14 @@ "is_pressure": "obecne ci\u015bnienie {entity_name}", "is_reactive_power": "aktualna moc bierna {entity_name}", "is_signal_strength": "obecna si\u0142a sygna\u0142u {entity_name}", + "is_speed": "obecna pr\u0119dko\u015b\u0107 {entity_name}", "is_sulphur_dioxide": "obecny poziom st\u0119\u017cenia dwutlenku siarki {entity_name}", "is_temperature": "obecna temperatura {entity_name}", "is_value": "obecna warto\u015b\u0107 {entity_name}", "is_volatile_organic_compounds": "obecny poziom st\u0119\u017cenia lotnych zwi\u0105zk\u00f3w organicznych {entity_name}", - "is_voltage": "obecne napi\u0119cie {entity_name}" + "is_voltage": "obecne napi\u0119cie {entity_name}", + "is_volume": "obecna obj\u0119to\u015b\u0107 {entity_name}", + "is_weight": "obecna waga {entity_name}" }, "trigger_type": { "apparent_power": "zmieni si\u0119 moc pozorna {entity_name}", @@ -36,6 +40,7 @@ "carbon_dioxide": "{entity_name} wykryje zmian\u0119 st\u0119\u017cenia dwutlenku w\u0119gla", "carbon_monoxide": "{entity_name} wykryje zmian\u0119 st\u0119\u017cenia tlenku w\u0119gla", "current": "zmieni si\u0119 nat\u0119\u017cenie pr\u0105du w {entity_name}", + "distance": "zmieni si\u0119 odleg\u0142o\u015b\u0107 {entity_name}", "energy": "zmieni si\u0119 energia {entity_name}", "frequency": "zmieni si\u0119 cz\u0119stotliwo\u015b\u0107 w {entity_name}", "gas": "{entity_name} wykryje zmian\u0119 poziomu gazu", @@ -54,11 +59,14 @@ "pressure": "zmieni si\u0119 ci\u015bnienie {entity_name}", "reactive_power": "zmieni si\u0119 moc bierna {entity_name}", "signal_strength": "zmieni si\u0119 si\u0142a sygna\u0142u {entity_name}", + "speed": "zmieni si\u0119 pr\u0119dko\u015b\u0107 {entity_name}", "sulphur_dioxide": "{entity_name} wykryje zmian\u0119 st\u0119\u017cenia dwutlenku siarki", "temperature": "zmieni si\u0119 temperatura {entity_name}", "value": "zmieni si\u0119 warto\u015b\u0107 {entity_name}", "volatile_organic_compounds": "{entity_name} wykryje zmian\u0119 st\u0119\u017cenia lotnych zwi\u0105zk\u00f3w organicznych", - "voltage": "zmieni si\u0119 napi\u0119cie w {entity_name}" + "voltage": "zmieni si\u0119 napi\u0119cie w {entity_name}", + "volume": "zmieni si\u0119 obj\u0119to\u015b\u0107 {entity_name}", + "weight": "zmieni si\u0119 waga {entity_name}" } }, "state": { diff --git a/homeassistant/components/sensor/translations/pt-BR.json b/homeassistant/components/sensor/translations/pt-BR.json index 436a43056f1..69bdd4971c3 100644 --- a/homeassistant/components/sensor/translations/pt-BR.json +++ b/homeassistant/components/sensor/translations/pt-BR.json @@ -6,6 +6,7 @@ "is_carbon_dioxide": "N\u00edvel atual de concentra\u00e7\u00e3o de di\u00f3xido de carbono de {entity_name}", "is_carbon_monoxide": "N\u00edvel de concentra\u00e7\u00e3o de mon\u00f3xido de carbono atual de {entity_name}", "is_current": "Corrente atual de {entity_name}", + "is_distance": "Dist\u00e2ncia atual de {entity_name}", "is_energy": "Energia atual de {entity_name}", "is_frequency": "Frequ\u00eancia atual de {entity_name}", "is_gas": "G\u00e1s atual de {entity_name}", @@ -24,11 +25,14 @@ "is_pressure": "Press\u00e3o atual do(a) {entity_name}", "is_reactive_power": "Pot\u00eancia reativa atual de {entity_name}", "is_signal_strength": "For\u00e7a do sinal atual do(a) {entity_name}", + "is_speed": "Velocidade atual de {entity_name}", "is_sulphur_dioxide": "N\u00edvel atual de concentra\u00e7\u00e3o de di\u00f3xido de enxofre de {entity_name}", "is_temperature": "Temperatura atual do(a) {entity_name}", "is_value": "Valor atual de {entity_name}", "is_volatile_organic_compounds": "N\u00edvel atual de concentra\u00e7\u00e3o de compostos org\u00e2nicos vol\u00e1teis de {entity_name}", - "is_voltage": "Tens\u00e3o atual de {entity_name}" + "is_voltage": "Tens\u00e3o atual de {entity_name}", + "is_volume": "Volume atual de {entity_name}", + "is_weight": "Peso atual {entity_name}" }, "trigger_type": { "apparent_power": "Mudan\u00e7as de poder aparentes de {entity_name}", @@ -36,6 +40,7 @@ "carbon_dioxide": "Mudan\u00e7as na concentra\u00e7\u00e3o de di\u00f3xido de carbono de {entity_name}", "carbon_monoxide": "Altera\u00e7\u00f5es na concentra\u00e7\u00e3o de mon\u00f3xido de carbono de {entity_name}", "current": "Mudan\u00e7a na corrente de {entity_name}", + "distance": "Mudan\u00e7as da dist\u00e2ncia de {entity_name}", "energy": "Mudan\u00e7as na energia de {entity_name}", "frequency": "Altera\u00e7\u00f5es de frequ\u00eancia de {entity_name}", "gas": "Mudan\u00e7as de g\u00e1s de {entity_name}", @@ -54,11 +59,14 @@ "pressure": "{entity_name} mudan\u00e7as de press\u00e3o", "reactive_power": "Altera\u00e7\u00f5es de pot\u00eancia reativa de {entity_name}", "signal_strength": "{entity_name} muda a for\u00e7a do sinal", + "speed": "Mudan\u00e7as de velocidade de {entity_name}", "sulphur_dioxide": "Altera\u00e7\u00f5es na concentra\u00e7\u00e3o de di\u00f3xido de enxofre de {entity_name}", "temperature": "{entity_name} mudan\u00e7as de temperatura", "value": "{entity_name} mudan\u00e7as de valor", "volatile_organic_compounds": "Altera\u00e7\u00f5es na concentra\u00e7\u00e3o de compostos org\u00e2nicos vol\u00e1teis de {entity_name}", - "voltage": "Mudan\u00e7as de voltagem de {entity_name}" + "voltage": "Mudan\u00e7as de voltagem de {entity_name}", + "volume": "Altera\u00e7\u00f5es de volume de {entity_name}", + "weight": "Altera\u00e7\u00f5es de peso {entity_name}" } }, "state": { diff --git a/homeassistant/components/sensor/translations/ru.json b/homeassistant/components/sensor/translations/ru.json index 1efde71a7c6..af4d66b631f 100644 --- a/homeassistant/components/sensor/translations/ru.json +++ b/homeassistant/components/sensor/translations/ru.json @@ -6,6 +6,7 @@ "is_carbon_dioxide": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0443\u0433\u043b\u0435\u043a\u0438\u0441\u043b\u043e\u0433\u043e \u0433\u0430\u0437\u0430", "is_carbon_monoxide": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0443\u0433\u0430\u0440\u043d\u043e\u0433\u043e \u0433\u0430\u0437\u0430", "is_current": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0438\u043b\u044b \u0442\u043e\u043a\u0430", + "is_distance": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_energy": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", "is_frequency": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_gas": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", @@ -36,6 +37,7 @@ "carbon_dioxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "carbon_monoxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "current": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0438\u043b\u044b \u0442\u043e\u043a\u0430", + "distance": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0440\u0430\u0441\u0441\u0442\u043e\u044f\u043d\u0438\u0435", "energy": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", "frequency": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "gas": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435 \u0433\u0430\u0437\u0430", diff --git a/homeassistant/components/sensor/translations/zh-Hant.json b/homeassistant/components/sensor/translations/zh-Hant.json index eb0bbfc50f9..2f513ac59e9 100644 --- a/homeassistant/components/sensor/translations/zh-Hant.json +++ b/homeassistant/components/sensor/translations/zh-Hant.json @@ -6,6 +6,7 @@ "is_carbon_dioxide": "\u76ee\u524d {entity_name} \u4e8c\u6c27\u5316\u78b3\u6fc3\u5ea6\u72c0\u614b", "is_carbon_monoxide": "\u76ee\u524d {entity_name} \u4e00\u6c27\u5316\u78b3\u6fc3\u5ea6\u72c0\u614b", "is_current": "\u76ee\u524d{entity_name}\u96fb\u6d41", + "is_distance": "\u76ee\u524d{entity_name}\u8ddd\u96e2", "is_energy": "\u76ee\u524d{entity_name}\u96fb\u529b", "is_frequency": "\u76ee\u524d{entity_name}\u983b\u7387", "is_gas": "\u76ee\u524d{entity_name}\u6c23\u9ad4", @@ -24,11 +25,14 @@ "is_pressure": "\u76ee\u524d{entity_name}\u58d3\u529b", "is_reactive_power": "\u76ee\u524d{entity_name}\u7121\u6548\u529f\u7387", "is_signal_strength": "\u76ee\u524d{entity_name}\u8a0a\u865f\u5f37\u5ea6", + "is_speed": "\u76ee\u524d{entity_name}\u901f\u5ea6", "is_sulphur_dioxide": "\u76ee\u524d {entity_name} \u4e8c\u6c27\u5316\u786b\u6fc3\u5ea6\u72c0\u614b", "is_temperature": "\u76ee\u524d{entity_name}\u6eab\u5ea6", "is_value": "\u76ee\u524d{entity_name}\u503c", "is_volatile_organic_compounds": "\u76ee\u524d {entity_name} \u63ee\u767c\u6027\u6709\u6a5f\u7269\u6fc3\u5ea6\u72c0\u614b", - "is_voltage": "\u76ee\u524d{entity_name}\u96fb\u58d3" + "is_voltage": "\u76ee\u524d{entity_name}\u96fb\u58d3", + "is_volume": "\u76ee\u524d{entity_name}\u9ad4\u7a4d", + "is_weight": "\u76ee\u524d{entity_name}\u91cd\u91cf" }, "trigger_type": { "apparent_power": "{entity_name}\u8996\u5728\u529f\u7387\u8b8a\u66f4", @@ -36,6 +40,7 @@ "carbon_dioxide": "{entity_name} \u4e8c\u6c27\u5316\u78b3\u6fc3\u5ea6\u8b8a\u5316", "carbon_monoxide": "{entity_name} \u4e00\u6c27\u5316\u78b3\u6fc3\u5ea6\u8b8a\u5316", "current": "\u76ee\u524d{entity_name}\u96fb\u6d41\u8b8a\u66f4", + "distance": "{entity_name}\u8ddd\u96e2\u8b8a\u66f4", "energy": "\u76ee\u524d{entity_name}\u96fb\u529b\u8b8a\u66f4", "frequency": "{entity_name}\u983b\u7387\u8b8a\u66f4", "gas": "{entity_name}\u6c23\u9ad4\u8b8a\u66f4", @@ -54,11 +59,14 @@ "pressure": "{entity_name}\u58d3\u529b\u8b8a\u66f4", "reactive_power": "{entity_name}\u7121\u6548\u529f\u7387\u8b8a\u66f4", "signal_strength": "{entity_name}\u8a0a\u865f\u5f37\u5ea6\u8b8a\u66f4", + "speed": "{entity_name}\u901f\u5ea6\u8b8a\u66f4", "sulphur_dioxide": "{entity_name} \u4e8c\u6c27\u5316\u786b\u6fc3\u5ea6\u8b8a\u5316", "temperature": "{entity_name}\u6eab\u5ea6\u8b8a\u66f4", "value": "{entity_name}\u503c\u8b8a\u66f4", "volatile_organic_compounds": "{entity_name} \u63ee\u767c\u6027\u6709\u6a5f\u7269\u6fc3\u5ea6\u8b8a\u5316", - "voltage": "\u76ee\u524d{entity_name}\u96fb\u58d3\u8b8a\u66f4" + "voltage": "\u76ee\u524d{entity_name}\u96fb\u58d3\u8b8a\u66f4", + "volume": "{entity_name}\u9ad4\u7a4d\u8b8a\u66f4", + "weight": "{entity_name}\u91cd\u91cf\u8b8a\u66f4" } }, "state": { diff --git a/homeassistant/components/shelly/translations/et.json b/homeassistant/components/shelly/translations/et.json index e248ee9eba4..bc68caeb9bb 100644 --- a/homeassistant/components/shelly/translations/et.json +++ b/homeassistant/components/shelly/translations/et.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "reauth_unsuccessful": "Taasautentimine eba\u00f5nnestus, eemaldage integratsioon ja seadistage see uuesti.", "unsupported_firmware": "Seade kasutab toetuseta p\u00fcsivara versiooni." }, "error": { @@ -21,6 +23,12 @@ "username": "Kasutajanimi" } }, + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + }, "user": { "data": { "host": "" diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json index c9c7496d13e..dd7d9d10486 100644 --- a/homeassistant/components/shelly/translations/pl.json +++ b/homeassistant/components/shelly/translations/pl.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "reauth_unsuccessful": "B\u0142\u0105d ponownego uwierzytelnienia, usu\u0144 integracj\u0119 i skonfiguruj j\u0105 ponownie", "unsupported_firmware": "Urz\u0105dzenie u\u017cywa nieobs\u0142ugiwanej wersji firmware" }, "error": { @@ -21,6 +23,12 @@ "username": "Nazwa u\u017cytkownika" } }, + "reauth_confirm": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + }, "user": { "data": { "host": "Nazwa hosta lub adres IP" diff --git a/homeassistant/components/shelly/translations/pt-BR.json b/homeassistant/components/shelly/translations/pt-BR.json index 125334b8d28..6fff6c1ec3c 100644 --- a/homeassistant/components/shelly/translations/pt-BR.json +++ b/homeassistant/components/shelly/translations/pt-BR.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "reauth_unsuccessful": "A reautentica\u00e7\u00e3o falhou. Remova a integra\u00e7\u00e3o e configure-a novamente.", "unsupported_firmware": "O dispositivo est\u00e1 usando uma vers\u00e3o de firmware n\u00e3o compat\u00edvel." }, "error": { @@ -21,6 +23,12 @@ "username": "Usu\u00e1rio" } }, + "reauth_confirm": { + "data": { + "password": "Senha", + "username": "Nome de usu\u00e1rio" + } + }, "user": { "data": { "host": "Nome do host" diff --git a/homeassistant/components/shelly/translations/sv.json b/homeassistant/components/shelly/translations/sv.json index 62262c8558c..fb5a480f9e7 100644 --- a/homeassistant/components/shelly/translations/sv.json +++ b/homeassistant/components/shelly/translations/sv.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades", + "reauth_unsuccessful": "\u00c5terautentiseringen misslyckades. Ta bort integrationen och konfigurera den igen.", "unsupported_firmware": "Enheten anv\u00e4nder en firmwareversion som inte st\u00f6ds." }, "error": { @@ -21,6 +23,12 @@ "username": "Anv\u00e4ndarnamn" } }, + "reauth_confirm": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + }, "user": { "data": { "host": "V\u00e4rd" diff --git a/homeassistant/components/simplisafe/translations/sv.json b/homeassistant/components/simplisafe/translations/sv.json index 61e00432950..a2d75f36697 100644 --- a/homeassistant/components/simplisafe/translations/sv.json +++ b/homeassistant/components/simplisafe/translations/sv.json @@ -39,6 +39,12 @@ } } }, + "issues": { + "deprecated_service": { + "description": "Uppdatera alla automatiseringar eller skript som anv\u00e4nder den h\u00e4r tj\u00e4nsten f\u00f6r att ist\u00e4llet anv\u00e4nda tj\u00e4nsten ` {alternate_service} ` med ett m\u00e5lenhets-ID p\u00e5 ` {alternate_target} `. Klicka sedan p\u00e5 SKICKA nedan f\u00f6r att markera problemet som l\u00f6st.", + "title": "Tj\u00e4nsten {deprecated_service} tas bort" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/switch/translations/sv.json b/homeassistant/components/switch/translations/sv.json index 6a87682ae5d..d9456d76c61 100644 --- a/homeassistant/components/switch/translations/sv.json +++ b/homeassistant/components/switch/translations/sv.json @@ -21,5 +21,5 @@ "on": "P\u00e5" } }, - "title": "Kontakt" + "title": "Brytare" } \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/sv.json b/homeassistant/components/switch_as_x/translations/sv.json index 95ea5abe410..9ff8e548255 100644 --- a/homeassistant/components/switch_as_x/translations/sv.json +++ b/homeassistant/components/switch_as_x/translations/sv.json @@ -10,5 +10,5 @@ } } }, - "title": "Kontakt som X" + "title": "\u00c4ndra enhetstyp f\u00f6r en c" } \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/sv.json b/homeassistant/components/switchbee/translations/sv.json new file mode 100644 index 00000000000..42d3330f48e --- /dev/null +++ b/homeassistant/components/switchbee/translations/sv.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "switch_as_light": "Initiera omkopplare som ljusenheter", + "username": "Anv\u00e4ndarnamn" + }, + "description": "Konfigurera SwitchBee-integrationen med Home Assistant." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "Enheter att inkludera" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/sv.json b/homeassistant/components/tasmota/translations/sv.json index df8bff40946..e33005865c2 100644 --- a/homeassistant/components/tasmota/translations/sv.json +++ b/homeassistant/components/tasmota/translations/sv.json @@ -16,5 +16,15 @@ "description": "Vill du konfigurera Tasmota?" } } + }, + "issues": { + "topic_duplicated": { + "description": "Flera Tasmota-enheter delar \u00e4mnet {topic} . \n\n Tasmota-enheter med detta problem: {offenders} .", + "title": "Flera Tasmota-enheter delar samma \u00e4mne" + }, + "topic_no_prefix": { + "description": "Tasmota-enhet {name} med IP {ip} inkluderar inte ` %prefix% ` i hela \u00e4mnet. \n\n Entiteter f\u00f6r denna enhet \u00e4r inaktiverade tills konfigurationen har korrigerats.", + "title": "Tasmota-enheten {name} har ett ogiltigt MQTT-\u00e4mne" + } } } \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/bg.json b/homeassistant/components/tautulli/translations/bg.json index fb4e836f98d..8f8e92fc429 100644 --- a/homeassistant/components/tautulli/translations/bg.json +++ b/homeassistant/components/tautulli/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, diff --git a/homeassistant/components/tautulli/translations/et.json b/homeassistant/components/tautulli/translations/et.json index bc690db7a28..30ef733c976 100644 --- a/homeassistant/components/tautulli/translations/et.json +++ b/homeassistant/components/tautulli/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", "reauth_successful": "Taastuvastamine \u00f5nnestus", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." }, diff --git a/homeassistant/components/tautulli/translations/fr.json b/homeassistant/components/tautulli/translations/fr.json index 05b66af0972..ad9c327c551 100644 --- a/homeassistant/components/tautulli/translations/fr.json +++ b/homeassistant/components/tautulli/translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, diff --git a/homeassistant/components/tautulli/translations/hu.json b/homeassistant/components/tautulli/translations/hu.json index b654081ecd1..7d31ad678f2 100644 --- a/homeassistant/components/tautulli/translations/hu.json +++ b/homeassistant/components/tautulli/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, diff --git a/homeassistant/components/tautulli/translations/id.json b/homeassistant/components/tautulli/translations/id.json index c2042dacffa..18669b36f29 100644 --- a/homeassistant/components/tautulli/translations/id.json +++ b/homeassistant/components/tautulli/translations/id.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Layanan sudah dikonfigurasi", "reauth_successful": "Autentikasi ulang berhasil", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, diff --git a/homeassistant/components/tautulli/translations/no.json b/homeassistant/components/tautulli/translations/no.json index cd6667b26fe..00a5c8c943a 100644 --- a/homeassistant/components/tautulli/translations/no.json +++ b/homeassistant/components/tautulli/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Tjenesten er allerede konfigurert", "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, diff --git a/homeassistant/components/tautulli/translations/pl.json b/homeassistant/components/tautulli/translations/pl.json index 25684ac6b3c..6dac9a79617 100644 --- a/homeassistant/components/tautulli/translations/pl.json +++ b/homeassistant/components/tautulli/translations/pl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, diff --git a/homeassistant/components/tautulli/translations/pt-BR.json b/homeassistant/components/tautulli/translations/pt-BR.json index 45c5b508f96..e5732024e3a 100644 --- a/homeassistant/components/tautulli/translations/pt-BR.json +++ b/homeassistant/components/tautulli/translations/pt-BR.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, diff --git a/homeassistant/components/tautulli/translations/sv.json b/homeassistant/components/tautulli/translations/sv.json index abcbe307998..1d4ad84cf58 100644 --- a/homeassistant/components/tautulli/translations/sv.json +++ b/homeassistant/components/tautulli/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", "reauth_successful": "\u00c5terautentisering lyckades", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, diff --git a/homeassistant/components/uptime/translations/et.json b/homeassistant/components/uptime/translations/et.json index f1b40328dab..71ef4a5c265 100644 --- a/homeassistant/components/uptime/translations/et.json +++ b/homeassistant/components/uptime/translations/et.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Uptime konfigureerimine YAML-i abil on eemaldatud. \n\n Koduassistent ei kasuta teie olemasolevat YAML-i konfiguratsiooni. \n\n Selle probleemi lahendamiseks eemaldage YAML-i konfiguratsioon failist configuration.yaml ja taask\u00e4ivitage Home Assistant.", + "title": "Uptime YAML-i konfiguratsioon on eemaldatud" + } + }, "title": "T\u00f6\u00f6aeg" } \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/hu.json b/homeassistant/components/uptime/translations/hu.json index 241c28b48ea..bae1e5edabc 100644 --- a/homeassistant/components/uptime/translations/hu.json +++ b/homeassistant/components/uptime/translations/hu.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Az Uptime YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3t a Home Assistant nem haszn\u00e1lja.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "Az Uptime YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fclt" + } + }, "title": "Uptime" } \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/id.json b/homeassistant/components/uptime/translations/id.json index bf6ea606f2b..33b92602016 100644 --- a/homeassistant/components/uptime/translations/id.json +++ b/homeassistant/components/uptime/translations/id.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Proses konfigurasi Integrasi Uptime lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Integrasi Uptime telah dihapus" + } + }, "title": "Uptime" } \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/no.json b/homeassistant/components/uptime/translations/no.json index 9ac16f0a20c..fa9103dff3c 100644 --- a/homeassistant/components/uptime/translations/no.json +++ b/homeassistant/components/uptime/translations/no.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Konfigurering av Oppetid med YAML er fjernet. \n\n Din eksisterende YAML-konfigurasjon brukes ikke av Home Assistant. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Oppetid YAML-konfigurasjonen er fjernet" + } + }, "title": "Oppetid" } \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/pl.json b/homeassistant/components/uptime/translations/pl.json index bca14b2f14c..2e5f40c3bd7 100644 --- a/homeassistant/components/uptime/translations/pl.json +++ b/homeassistant/components/uptime/translations/pl.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Konfiguracja Uptime za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Uptime zosta\u0142a usuni\u0119ta" + } + }, "title": "Uptime" } \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/pt-BR.json b/homeassistant/components/uptime/translations/pt-BR.json index d3dddae8233..ae89c128e08 100644 --- a/homeassistant/components/uptime/translations/pt-BR.json +++ b/homeassistant/components/uptime/translations/pt-BR.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do Uptime usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Uptime foi removida" + } + }, "title": "Tempo de atividade" } \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/sv.json b/homeassistant/components/uptime/translations/sv.json index 0d9e03ec575..48ca71741a5 100644 --- a/homeassistant/components/uptime/translations/sv.json +++ b/homeassistant/components/uptime/translations/sv.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Konfigurering av Uptime med YAML har tagits bort. \n\n Din befintliga YAML-konfiguration anv\u00e4nds inte av Home Assistant. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Uptime YAML-konfigurationen har tagits bort" + } + }, "title": "Upptid" } \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/sv.json b/homeassistant/components/volvooncall/translations/sv.json index 03658c137df..48d56656c6a 100644 --- a/homeassistant/components/volvooncall/translations/sv.json +++ b/homeassistant/components/volvooncall/translations/sv.json @@ -15,6 +15,7 @@ "password": "L\u00f6senord", "region": "Region", "scandinavian_miles": "Anv\u00e4nd Skandinaviska mil", + "unit_system": "Enhetssystem", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 60ea4fcc615..a2c7ad5956f 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -116,6 +116,8 @@ }, "device_automation": { "action_type": { + "issue_all_led_effect": "Ausgabeeffekt f\u00fcr alle LEDs", + "issue_individual_led_effect": "Ausgabeeffekt f\u00fcr einzelne LED", "squawk": "Kreischen", "warn": "Warnen" }, diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index adf89983256..c75fa14628d 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -116,10 +116,10 @@ }, "device_automation": { "action_type": { - "squawk": "Squawk", - "warn": "Warn", "issue_all_led_effect": "Issue effect for all LEDs", - "issue_individual_led_effect": "Issue effect for individual LED" + "issue_individual_led_effect": "Issue effect for individual LED", + "squawk": "Squawk", + "warn": "Warn" }, "trigger_subtype": { "both_buttons": "Both buttons", diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 7aa42172d05..080814991db 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -116,6 +116,8 @@ }, "device_automation": { "action_type": { + "issue_all_led_effect": "Efecto de emisi\u00f3n para todos los LEDs", + "issue_individual_led_effect": "Efecto de emisi\u00f3n para LED individual", "squawk": "Squawk", "warn": "Advertir" }, diff --git a/homeassistant/components/zwave_js/translations/sv.json b/homeassistant/components/zwave_js/translations/sv.json index b619c54026b..448069933d9 100644 --- a/homeassistant/components/zwave_js/translations/sv.json +++ b/homeassistant/components/zwave_js/translations/sv.json @@ -91,6 +91,12 @@ "zwave_js.value_updated.value": "V\u00e4rdef\u00f6r\u00e4ndring p\u00e5 ett Z-Wave JS-v\u00e4rde" } }, + "issues": { + "invalid_server_version": { + "description": "Den version av Z-Wave JS Server du f\u00f6r n\u00e4rvarande k\u00f6r \u00e4r f\u00f6r gammal f\u00f6r den h\u00e4r versionen av Home Assistant. Uppdatera Z-Wave JS Server till den senaste versionen f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Nyare version av Z-Wave JS Server beh\u00f6vs" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Det gick inte att h\u00e4mta Z-Wave JS-till\u00e4ggsuppt\u00e4cktsinformation.", From 3884b4b6bf1476740327c18c849b86d4823ebdb4 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 28 Sep 2022 21:24:04 -0400 Subject: [PATCH 010/985] Bump zwave-js-server-python to 0.42.0 (#79020) --- homeassistant/components/zwave_js/__init__.py | 7 +- homeassistant/components/zwave_js/api.py | 4 +- homeassistant/components/zwave_js/const.py | 4 + .../components/zwave_js/device_action.py | 4 +- .../components/zwave_js/diagnostics.py | 6 +- .../zwave_js/discovery_data_template.py | 4 +- homeassistant/components/zwave_js/entity.py | 6 +- homeassistant/components/zwave_js/helpers.py | 4 +- .../components/zwave_js/manifest.json | 2 +- homeassistant/components/zwave_js/services.py | 10 +- .../zwave_js/triggers/value_updated.py | 6 +- homeassistant/const.py | 1 + homeassistant/helpers/aiohttp_client.py | 6 +- homeassistant/helpers/httpx_client.py | 6 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_api.py | 58 ++-- tests/components/zwave_js/test_climate.py | 100 ------ tests/components/zwave_js/test_cover.py | 202 ------------ tests/components/zwave_js/test_fan.py | 63 ---- tests/components/zwave_js/test_humidifier.py | 145 --------- tests/components/zwave_js/test_init.py | 10 +- tests/components/zwave_js/test_light.py | 115 ------- tests/components/zwave_js/test_lock.py | 67 ---- tests/components/zwave_js/test_number.py | 25 -- tests/components/zwave_js/test_select.py | 39 --- tests/components/zwave_js/test_services.py | 289 ------------------ tests/components/zwave_js/test_siren.py | 24 +- tests/components/zwave_js/test_switch.py | 46 --- 29 files changed, 93 insertions(+), 1164 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index d676a40d32f..f8828e8cdd0 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -88,6 +88,7 @@ from .const import ( DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, LOGGER, + USER_AGENT, ZWAVE_JS_NOTIFICATION_EVENT, ZWAVE_JS_VALUE_NOTIFICATION_EVENT, ZWAVE_JS_VALUE_UPDATED_EVENT, @@ -129,7 +130,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if use_addon := entry.data.get(CONF_USE_ADDON): await async_ensure_addon_running(hass, entry) - client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) + client = ZwaveClient( + entry.data[CONF_URL], + async_get_clientsession(hass), + additional_user_agent_components=USER_AGENT, + ) # connect and throw error if connection failed try: diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 98078c8457f..7ceca062ee4 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -68,6 +68,7 @@ from .const import ( DATA_CLIENT, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, + USER_AGENT, ) from .helpers import ( async_enable_statistics, @@ -466,7 +467,7 @@ async def websocket_network_status( ) return controller = driver.controller - await controller.async_get_state() + controller.update(await controller.async_get_state()) client_version_info = client.version assert client_version_info # When client is connected version info is set. data = { @@ -2064,6 +2065,7 @@ class FirmwareUploadView(HomeAssistantView): uploaded_file.filename, await hass.async_add_executor_job(uploaded_file.file.read), async_get_clientsession(hass), + additional_user_agent_components=USER_AGENT, target=target, ) except BaseZwaveJSServerError as err: diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index ff1a97d6ecc..3967709ccc8 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -1,6 +1,10 @@ """Constants for the Z-Wave JS integration.""" import logging +from homeassistant.const import APPLICATION_NAME, __version__ as HA_VERSION + +USER_AGENT = {APPLICATION_NAME: HA_VERSION} + CONF_ADDON_DEVICE = "device" CONF_ADDON_EMULATE_HARDWARE = "emulate_hardware" CONF_ADDON_LOG_LEVEL = "log_level" diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index 728b376228e..004e4cc2aae 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -9,7 +9,7 @@ import voluptuous as vol from zwave_js_server.const import CommandClass from zwave_js_server.const.command_class.lock import ATTR_CODE_SLOT, ATTR_USERCODE from zwave_js_server.const.command_class.meter import CC_SPECIFIC_METER_TYPE -from zwave_js_server.model.value import get_value_id +from zwave_js_server.model.value import get_value_id_str from zwave_js_server.util.command_class.meter import get_meter_type from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN @@ -341,7 +341,7 @@ async def async_get_action_capabilities( } if action_type == SERVICE_SET_CONFIG_PARAMETER: - value_id = get_value_id( + value_id = get_value_id_str( node, CommandClass.CONFIGURATION, config[ATTR_CONFIG_PARAMETER], diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index f9a30528863..ef34a2f12de 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DATA_CLIENT, DOMAIN +from .const import DATA_CLIENT, DOMAIN, USER_AGENT from .helpers import ( get_home_and_node_id_from_device_entry, get_state_key_from_unique_id, @@ -138,7 +138,9 @@ async def async_get_config_entry_diagnostics( ) -> list[dict]: """Return diagnostics for a config entry.""" msgs: list[dict] = async_redact_data( - await dump_msgs(config_entry.data[CONF_URL], async_get_clientsession(hass)), + await dump_msgs( + config_entry.data[CONF_URL], async_get_clientsession(hass), USER_AGENT + ), KEYS_TO_REDACT, ) handshake_msgs = msgs[:-1] diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 74847c3f4da..9ae1cd36d13 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -80,7 +80,7 @@ from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import ( ConfigurationValue as ZwaveConfigurationValue, Value as ZwaveValue, - get_value_id, + get_value_id_str, ) from zwave_js_server.util.command_class.meter import get_meter_scale_type from zwave_js_server.util.command_class.multilevel_sensor import ( @@ -263,7 +263,7 @@ class BaseDiscoverySchemaDataTemplate: node: ZwaveNode, value_id_obj: ZwaveValueID ) -> ZwaveValue | ZwaveConfigurationValue | None: """Get a ZwaveValue from a node using a ZwaveValueDict.""" - value_id = get_value_id( + value_id = get_value_id_str( node, value_id_obj.command_class, value_id_obj.property_, diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 621316a166f..53946a07982 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -3,7 +3,7 @@ from __future__ import annotations from zwave_js_server.const import NodeStatus from zwave_js_server.model.driver import Driver -from zwave_js_server.model.value import Value as ZwaveValue, get_value_id +from zwave_js_server.model.value import Value as ZwaveValue, get_value_id_str from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback @@ -242,7 +242,7 @@ class ZWaveBaseEntity(Entity): endpoint = self.info.primary_value.endpoint # lookup value by value_id - value_id = get_value_id( + value_id = get_value_id_str( self.info.node, command_class, value_property, @@ -256,7 +256,7 @@ class ZWaveBaseEntity(Entity): if return_value is None and check_all_endpoints: for endpoint_idx in self.info.node.endpoints: if endpoint_idx != self.info.primary_value.endpoint: - value_id = get_value_id( + value_id = get_value_id_str( self.info.node, command_class, value_property, diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 6175b7db353..6949f3654a5 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -14,7 +14,7 @@ from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import ( ConfigurationValue, Value as ZwaveValue, - get_value_id, + get_value_id_str, ) from homeassistant.components.group import expand_entity_ids @@ -317,7 +317,7 @@ def get_zwave_value_from_config(node: ZwaveNode, config: ConfigType) -> ZwaveVal property_key = None if config.get(ATTR_PROPERTY_KEY): property_key = config[ATTR_PROPERTY_KEY] - value_id = get_value_id( + value_id = get_value_id_str( node, config[ATTR_COMMAND_CLASS], config[ATTR_PROPERTY], diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 7c569301831..9880d5bb5d1 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.41.1"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.42.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index d60532fcf75..63a9071ffb6 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -12,7 +12,7 @@ from zwave_js_server.const import CommandClass, CommandStatus from zwave_js_server.exceptions import SetValueFailed from zwave_js_server.model.endpoint import Endpoint from zwave_js_server.model.node import Node as ZwaveNode -from zwave_js_server.model.value import ValueDataType, get_value_id +from zwave_js_server.model.value import ValueDataType, get_value_id_str from zwave_js_server.util.multicast import async_multicast_set_value from zwave_js_server.util.node import ( async_bulk_set_partial_config_parameters, @@ -497,7 +497,7 @@ class ZWaveServices: coros = [] for node in nodes: - value_id = get_value_id( + value_id = get_value_id_str( node, command_class, property_, @@ -582,13 +582,15 @@ class ZWaveServices: first_node = next( node for node in client.driver.controller.nodes.values() - if get_value_id(node, command_class, property_, endpoint, property_key) + if get_value_id_str( + node, command_class, property_, endpoint, property_key + ) in node.values ) # If value has a string type but the new value is not a string, we need to # convert it to one - value_id = get_value_id( + value_id = get_value_id_str( first_node, command_class, property_, endpoint, property_key ) if ( diff --git a/homeassistant/components/zwave_js/triggers/value_updated.py b/homeassistant/components/zwave_js/triggers/value_updated.py index 780d1251911..655d1f9070e 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -5,7 +5,7 @@ import functools import voluptuous as vol from zwave_js_server.const import CommandClass -from zwave_js_server.model.value import Value, get_value_id +from zwave_js_server.model.value import Value, get_value_id_str from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM, MATCH_ALL from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback @@ -164,7 +164,9 @@ async def async_attach_trigger( device_identifier = get_device_id(driver, node) device = dev_reg.async_get_device({device_identifier}) assert device - value_id = get_value_id(node, command_class, property_, endpoint, property_key) + value_id = get_value_id_str( + node, command_class, property_, endpoint, property_key + ) value = node.values[value_id] # We need to store the current value and device for the callback unsubs.append( diff --git a/homeassistant/const.py b/homeassistant/const.py index 330f5399bc2..3c0b2a4051c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,6 +5,7 @@ from typing import Final from .backports.enum import StrEnum +APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 PATCH_VERSION: Final = "0.dev0" diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index f44b59ff077..2558b5d0896 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -16,7 +16,7 @@ from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout import async_timeout from homeassistant import config_entries -from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ +from homeassistant.const import APPLICATION_NAME, EVENT_HOMEASSISTANT_CLOSE, __version__ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.loader import bind_hass from homeassistant.util import ssl as ssl_util @@ -32,8 +32,8 @@ DATA_CONNECTOR = "aiohttp_connector" DATA_CONNECTOR_NOTVERIFY = "aiohttp_connector_notverify" DATA_CLIENTSESSION = "aiohttp_clientsession" DATA_CLIENTSESSION_NOTVERIFY = "aiohttp_clientsession_notverify" -SERVER_SOFTWARE = "HomeAssistant/{0} aiohttp/{1} Python/{2[0]}.{2[1]}".format( - __version__, aiohttp.__version__, sys.version_info +SERVER_SOFTWARE = "{0}/{1} aiohttp/{2} Python/{3[0]}.{3[1]}".format( + APPLICATION_NAME, __version__, aiohttp.__version__, sys.version_info ) WARN_CLOSE_MSG = "closes the Home Assistant aiohttp session" diff --git a/homeassistant/helpers/httpx_client.py b/homeassistant/helpers/httpx_client.py index e2ebbd31dac..b932a1cd795 100644 --- a/homeassistant/helpers/httpx_client.py +++ b/homeassistant/helpers/httpx_client.py @@ -7,7 +7,7 @@ from typing import Any import httpx -from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ +from homeassistant.const import APPLICATION_NAME, EVENT_HOMEASSISTANT_CLOSE, __version__ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.loader import bind_hass @@ -15,8 +15,8 @@ from .frame import warn_use DATA_ASYNC_CLIENT = "httpx_async_client" DATA_ASYNC_CLIENT_NOVERIFY = "httpx_async_client_noverify" -SERVER_SOFTWARE = "HomeAssistant/{0} httpx/{1} Python/{2[0]}.{2[1]}".format( - __version__, httpx.__version__, sys.version_info +SERVER_SOFTWARE = "{0}/{1} httpx/{2} Python/{3[0]}.{3[1]}".format( + APPLICATION_NAME, __version__, httpx.__version__, sys.version_info ) USER_AGENT = "User-Agent" diff --git a/requirements_all.txt b/requirements_all.txt index b4690f855b7..e02de390571 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2619,7 +2619,7 @@ zigpy==0.50.3 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.41.1 +zwave-js-server-python==0.42.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc6d6479f1e..75ff02697e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1811,7 +1811,7 @@ zigpy-znp==0.8.2 zigpy==0.50.3 # homeassistant.components.zwave_js -zwave-js-server-python==0.41.1 +zwave-js-server-python==0.42.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.6 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index ed7c23aef79..b55f4941a49 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -86,13 +86,18 @@ def get_device(hass, node): return dev_reg.async_get_device({device_id}) -async def test_network_status(hass, multisensor_6, integration, hass_ws_client): +async def test_network_status( + hass, multisensor_6, controller_state, integration, hass_ws_client +): """Test the network status websocket command.""" entry = integration ws_client = await hass_ws_client(hass) # Try API call with entry ID - with patch("zwave_js_server.model.controller.Controller.async_get_state"): + with patch( + "zwave_js_server.model.controller.Controller.async_get_state", + return_value=controller_state["controller"], + ): await ws_client.send_json( { ID: 1, @@ -113,7 +118,10 @@ async def test_network_status(hass, multisensor_6, integration, hass_ws_client): identifiers={(DOMAIN, "3245146787-52")}, ) assert device - with patch("zwave_js_server.model.controller.Controller.async_get_state"): + with patch( + "zwave_js_server.model.controller.Controller.async_get_state", + return_value=controller_state["controller"], + ): await ws_client.send_json( { ID: 2, @@ -2585,27 +2593,10 @@ async def test_set_config_parameter( assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, - "propertyName": "Group 2: Send battery reports", "propertyKey": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 @@ -2633,27 +2624,10 @@ async def test_set_config_parameter( assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, - "propertyName": "Group 2: Send battery reports", "propertyKey": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 @@ -2842,13 +2816,19 @@ async def test_firmware_upload_view( device = get_device(hass, multisensor_6) with patch( "homeassistant.components.zwave_js.api.begin_firmware_update", - ) as mock_cmd: + ) as mock_cmd, patch.dict( + "homeassistant.components.zwave_js.api.USER_AGENT", + {"HomeAssistant": "0.0.0"}, + ): resp = await client.post( f"/api/zwave_js/firmware/upload/{device.id}", data={"file": firmware_file, "target": "15"}, ) assert mock_cmd.call_args[0][1:4] == (multisensor_6, "file", bytes(10)) - assert mock_cmd.call_args[1] == {"target": 15} + assert mock_cmd.call_args[1] == { + "target": 15, + "additional_user_agent_components": {"HomeAssistant": "0.0.0"}, + } assert json.loads(await resp.text()) is None diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index e0b8dbe569f..4b4519c07b9 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -86,21 +86,9 @@ async def test_thermostat_v2( assert args["command"] == "node.set_value" assert args["nodeId"] == 13 assert args["valueId"] == { - "commandClassName": "Thermostat Mode", "commandClass": 64, "endpoint": 1, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 31, - "label": "Thermostat mode", - "states": {"0": "Off", "1": "Heat", "2": "Cool", "3": "Auto"}, - }, - "value": 1, } assert args["value"] == 2 @@ -123,42 +111,19 @@ async def test_thermostat_v2( assert args["command"] == "node.set_value" assert args["nodeId"] == 13 assert args["valueId"] == { - "commandClassName": "Thermostat Mode", "commandClass": 64, "endpoint": 1, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 31, - "label": "Thermostat mode", - "states": {"0": "Off", "1": "Heat", "2": "Cool", "3": "Auto"}, - }, - "value": 1, } assert args["value"] == 2 args = client.async_send_command.call_args_list[1][0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 13 assert args["valueId"] == { - "commandClassName": "Thermostat Setpoint", "commandClass": 67, "endpoint": 1, "property": "setpoint", "propertyKey": 1, - "propertyName": "setpoint", - "propertyKeyName": "Heating", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "unit": "°F", - "ccSpecific": {"setpointType": 1}, - }, - "value": 72, } assert args["value"] == 77 @@ -232,21 +197,10 @@ async def test_thermostat_v2( assert args["command"] == "node.set_value" assert args["nodeId"] == 13 assert args["valueId"] == { - "commandClassName": "Thermostat Setpoint", "commandClass": 67, "endpoint": 1, "property": "setpoint", "propertyKey": 1, - "propertyName": "setpoint", - "propertyKeyName": "Heating", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "unit": "°F", - "ccSpecific": {"setpointType": 1}, - }, - "value": 72, } assert args["value"] == 77 @@ -254,21 +208,10 @@ async def test_thermostat_v2( assert args["command"] == "node.set_value" assert args["nodeId"] == 13 assert args["valueId"] == { - "commandClassName": "Thermostat Setpoint", "commandClass": 67, "endpoint": 1, "property": "setpoint", "propertyKey": 2, - "propertyName": "setpoint", - "propertyKeyName": "Cooling", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "unit": "°F", - "ccSpecific": {"setpointType": 2}, - }, - "value": 73, } assert args["value"] == 86 @@ -306,20 +249,7 @@ async def test_thermostat_v2( assert args["valueId"] == { "endpoint": 1, "commandClass": 68, - "commandClassName": "Thermostat Fan Mode", "property": "mode", - "propertyName": "mode", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "states": {"0": "Auto low", "1": "Low"}, - "label": "Thermostat fan mode", - }, - "value": 0, } assert args["value"] == 1 @@ -408,20 +338,8 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat assert args["valueId"] == { "endpoint": 0, "commandClass": 67, - "commandClassName": "Thermostat Setpoint", "property": "setpoint", - "propertyName": "setpoint", "propertyKey": 1, - "propertyKeyName": "Heating", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "unit": "\u00b0C", - "ccSpecific": {"setpointType": 1}, - }, - "value": 14, } assert args["value"] == 21.5 @@ -627,27 +545,9 @@ async def test_preset_and_no_setpoint( assert args["command"] == "node.set_value" assert args["nodeId"] == 8 assert args["valueId"] == { - "commandClassName": "Thermostat Mode", "commandClass": 64, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "ccVersion": 3, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 31, - "label": "Thermostat mode", - "states": { - "0": "Off", - "1": "Heat", - "11": "Energy heat", - "15": "Full power", - }, - }, - "value": 1, } assert args["value"] == 15 diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index 0958e259ab0..54f71fa00d3 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -50,20 +50,9 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 6 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "label": "Target value", - }, } assert args["value"] == 50 @@ -82,20 +71,9 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 6 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "label": "Target value", - }, } assert args["value"] == 0 @@ -114,20 +92,9 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 6 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "label": "Target value", - }, } assert args["value"] @@ -145,18 +112,9 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert open_args["command"] == "node.set_value" assert open_args["nodeId"] == 6 assert open_args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "Open", - "propertyName": "Open", - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Perform a level change (Open)", - "ccSpecific": {"switchType": 3}, - }, } assert not open_args["value"] @@ -164,18 +122,9 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert close_args["command"] == "node.set_value" assert close_args["nodeId"] == 6 assert close_args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "Close", - "propertyName": "Close", - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Perform a level change (Close)", - "ccSpecific": {"switchType": 3}, - }, } assert not close_args["value"] @@ -215,20 +164,9 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 6 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "label": "Target value", - }, } assert args["value"] == 0 @@ -247,18 +185,9 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert open_args["command"] == "node.set_value" assert open_args["nodeId"] == 6 assert open_args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "Open", - "propertyName": "Open", - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Perform a level change (Open)", - "ccSpecific": {"switchType": 3}, - }, } assert not open_args["value"] @@ -266,18 +195,9 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert close_args["command"] == "node.set_value" assert close_args["nodeId"] == 6 assert close_args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "Close", - "propertyName": "Close", - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Perform a level change (Close)", - "ccSpecific": {"switchType": 3}, - }, } assert not close_args["value"] @@ -332,21 +252,8 @@ async def test_fibaro_FGR222_shutter_cover( assert args["valueId"] == { "endpoint": 0, "commandClass": 145, - "commandClassName": "Manufacturer Proprietary", "property": "fibaro", "propertyKey": "venetianBlindsTilt", - "propertyName": "fibaro", - "propertyKeyName": "venetianBlindsTilt", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Venetian blinds tilt", - "min": 0, - "max": 99, - }, - "value": 0, } assert args["value"] == 99 @@ -366,21 +273,8 @@ async def test_fibaro_FGR222_shutter_cover( assert args["valueId"] == { "endpoint": 0, "commandClass": 145, - "commandClassName": "Manufacturer Proprietary", "property": "fibaro", "propertyKey": "venetianBlindsTilt", - "propertyName": "fibaro", - "propertyKeyName": "venetianBlindsTilt", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Venetian blinds tilt", - "min": 0, - "max": 99, - }, - "value": 0, } assert args["value"] == 0 @@ -411,23 +305,9 @@ async def test_aeotec_nano_shutter_cover( assert args["command"] == "node.set_value" assert args["nodeId"] == 3 assert args["valueId"] == { - "ccVersion": 4, - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "value": 0, - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "valueChangeOptions": ["transitionDuration"], - "readable": True, - "writeable": True, - "label": "Target value", - }, } assert args["value"] @@ -445,20 +325,9 @@ async def test_aeotec_nano_shutter_cover( assert open_args["command"] == "node.set_value" assert open_args["nodeId"] == 3 assert open_args["valueId"] == { - "ccVersion": 4, - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "On", - "propertyName": "On", - "value": False, - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Perform a level change (On)", - "ccSpecific": {"switchType": 1}, - }, } assert not open_args["value"] @@ -466,20 +335,9 @@ async def test_aeotec_nano_shutter_cover( assert close_args["command"] == "node.set_value" assert close_args["nodeId"] == 3 assert close_args["valueId"] == { - "ccVersion": 4, - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "Off", - "propertyName": "Off", - "value": False, - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Perform a level change (Off)", - "ccSpecific": {"switchType": 1}, - }, } assert not close_args["value"] @@ -520,23 +378,9 @@ async def test_aeotec_nano_shutter_cover( assert args["command"] == "node.set_value" assert args["nodeId"] == 3 assert args["valueId"] == { - "ccVersion": 4, - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "value": 0, - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - "label": "Target value", - }, } assert args["value"] == 0 @@ -555,20 +399,9 @@ async def test_aeotec_nano_shutter_cover( assert open_args["command"] == "node.set_value" assert open_args["nodeId"] == 3 assert open_args["valueId"] == { - "ccVersion": 4, - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "On", - "propertyName": "On", - "value": False, - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Perform a level change (On)", - "ccSpecific": {"switchType": 1}, - }, } assert not open_args["value"] @@ -576,20 +409,9 @@ async def test_aeotec_nano_shutter_cover( assert close_args["command"] == "node.set_value" assert close_args["nodeId"] == 3 assert close_args["valueId"] == { - "ccVersion": 4, - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "Off", - "propertyName": "Off", - "value": False, - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Perform a level change (Off)", - "ccSpecific": {"switchType": 1}, - }, } assert not close_args["value"] @@ -631,21 +453,9 @@ async def test_motor_barrier_cover(hass, client, gdc_zw062, integration): assert args["nodeId"] == 12 assert args["value"] == 255 assert args["valueId"] == { - "ccVersion": 0, "commandClass": 102, - "commandClassName": "Barrier Operator", "endpoint": 0, - "metadata": { - "label": "Target Barrier State", - "max": 255, - "min": 0, - "readable": True, - "states": {"0": "Closed", "255": "Open"}, - "type": "number", - "writeable": True, - }, "property": "targetState", - "propertyName": "targetState", } # state doesn't change until currentState value update is received @@ -665,21 +475,9 @@ async def test_motor_barrier_cover(hass, client, gdc_zw062, integration): assert args["nodeId"] == 12 assert args["value"] == 0 assert args["valueId"] == { - "ccVersion": 0, "commandClass": 102, - "commandClassName": "Barrier Operator", "endpoint": 0, - "metadata": { - "label": "Target Barrier State", - "max": 255, - "min": 0, - "readable": True, - "states": {"0": "Closed", "255": "Open"}, - "type": "number", - "writeable": True, - }, "property": "targetState", - "propertyName": "targetState", } # state doesn't change until currentState value update is received diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py index 97b4a0074bc..2e200d9e4e9 100644 --- a/tests/components/zwave_js/test_fan.py +++ b/tests/components/zwave_js/test_fan.py @@ -54,19 +54,9 @@ async def test_generic_fan(hass, client, fan_generic, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 17 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - }, } assert args["value"] == 66 @@ -96,19 +86,9 @@ async def test_generic_fan(hass, client, fan_generic, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 17 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - }, } assert args["value"] == 255 @@ -127,19 +107,9 @@ async def test_generic_fan(hass, client, fan_generic, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 17 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - }, } assert args["value"] == 0 @@ -602,22 +572,9 @@ async def test_thermostat_fan(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 3, - "commandClassName": "Thermostat Fan Mode", "commandClass": CommandClass.THERMOSTAT_FAN_MODE.value, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "label": "Thermostat fan mode", - "max": 255, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "states": {"0": "Auto low", "1": "Low", "6": "Circulation"}, - }, - "value": 0, } assert args["value"] == 1 @@ -647,19 +604,9 @@ async def test_thermostat_fan(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 3, - "commandClassName": "Thermostat Fan Mode", "commandClass": CommandClass.THERMOSTAT_FAN_MODE.value, "endpoint": 0, "property": "off", - "propertyName": "off", - "metadata": { - "label": "Thermostat fan turned off", - "type": "boolean", - "readable": True, - "writeable": True, - }, - "value": False, } assert args["value"] @@ -678,19 +625,9 @@ async def test_thermostat_fan(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 3, - "commandClassName": "Thermostat Fan Mode", "commandClass": CommandClass.THERMOSTAT_FAN_MODE.value, "endpoint": 0, "property": "off", - "propertyName": "off", - "metadata": { - "label": "Thermostat fan turned off", - "type": "boolean", - "readable": True, - "writeable": True, - }, - "value": False, } assert not args["value"] diff --git a/tests/components/zwave_js/test_humidifier.py b/tests/components/zwave_js/test_humidifier.py index 37280ff5ad4..7952f639fe6 100644 --- a/tests/components/zwave_js/test_humidifier.py +++ b/tests/components/zwave_js/test_humidifier.py @@ -56,24 +56,10 @@ async def test_humidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 1, - "commandClassName": "Humidity Control Setpoint", "commandClass": CommandClass.HUMIDITY_CONTROL_SETPOINT, "endpoint": 0, "property": "setpoint", "propertyKey": 1, - "propertyName": "setpoint", - "propertyKeyName": "Humidifier", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "unit": "%", - "min": 10, - "max": 70, - "ccSpecific": {"setpointType": 1}, - }, - "value": 35, } assert args["value"] == 41 @@ -186,22 +172,9 @@ async def test_humidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 2, - "commandClassName": "Humidity Control Mode", "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Humidity control mode", - "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, - }, - "value": int(HumidityControlMode.HUMIDIFY), } assert args["value"] == int(HumidityControlMode.OFF) @@ -239,22 +212,9 @@ async def test_humidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 2, - "commandClassName": "Humidity Control Mode", "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Humidity control mode", - "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, - }, - "value": int(HumidityControlMode.AUTO), } assert args["value"] == int(HumidityControlMode.DEHUMIDIFY) @@ -416,22 +376,9 @@ async def test_humidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 2, - "commandClassName": "Humidity Control Mode", "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Humidity control mode", - "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, - }, - "value": int(HumidityControlMode.DEHUMIDIFY), } assert args["value"] == int(HumidityControlMode.AUTO) @@ -469,22 +416,9 @@ async def test_humidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 2, - "commandClassName": "Humidity Control Mode", "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Humidity control mode", - "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, - }, - "value": int(HumidityControlMode.OFF), } assert args["value"] == int(HumidityControlMode.HUMIDIFY) @@ -570,22 +504,9 @@ async def test_humidifier_missing_mode( assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 2, - "commandClassName": "Humidity Control Mode", "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Humidity control mode", - "states": {"0": "Off", "1": "Humidify", "3": "Auto"}, - }, - "value": int(HumidityControlMode.AUTO), } assert args["value"] == int(HumidityControlMode.OFF) @@ -623,24 +544,10 @@ async def test_dehumidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 1, - "commandClassName": "Humidity Control Setpoint", "commandClass": CommandClass.HUMIDITY_CONTROL_SETPOINT, "endpoint": 0, "property": "setpoint", "propertyKey": 2, - "propertyName": "setpoint", - "propertyKeyName": "De-humidifier", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "unit": "%", - "min": 30, - "max": 90, - "ccSpecific": {"setpointType": 2}, - }, - "value": 60, } assert args["value"] == 41 @@ -753,22 +660,9 @@ async def test_dehumidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 2, - "commandClassName": "Humidity Control Mode", "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Humidity control mode", - "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, - }, - "value": int(HumidityControlMode.DEHUMIDIFY), } assert args["value"] == int(HumidityControlMode.OFF) @@ -806,22 +700,9 @@ async def test_dehumidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 2, - "commandClassName": "Humidity Control Mode", "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Humidity control mode", - "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, - }, - "value": int(HumidityControlMode.AUTO), } assert args["value"] == int(HumidityControlMode.HUMIDIFY) @@ -983,22 +864,9 @@ async def test_dehumidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 2, - "commandClassName": "Humidity Control Mode", "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Humidity control mode", - "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, - }, - "value": int(HumidityControlMode.HUMIDIFY), } assert args["value"] == int(HumidityControlMode.AUTO) @@ -1036,21 +904,8 @@ async def test_dehumidifier(hass, client, climate_adc_t3000, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 68 assert args["valueId"] == { - "ccVersion": 2, - "commandClassName": "Humidity Control Mode", "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, "endpoint": 0, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Humidity control mode", - "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, - }, - "value": int(HumidityControlMode.OFF), } assert args["value"] == int(HumidityControlMode.DEHUMIDIFY) diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index c69c5a09f89..4f58c87febb 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -8,6 +8,7 @@ from zwave_js_server.client import Client from zwave_js_server.event import Event from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion from zwave_js_server.model.node import Node +from zwave_js_server.model.version import VersionInfo from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.zwave_js import DOMAIN @@ -667,6 +668,7 @@ async def test_update_addon( backup_calls, update_addon_side_effect, create_backup_side_effect, + version_state, ): """Test update the Z-Wave JS add-on during entry setup.""" device = "/test" @@ -677,7 +679,9 @@ async def test_update_addon( addon_info.return_value["update_available"] = update_available create_backup.side_effect = create_backup_side_effect update_addon.side_effect = update_addon_side_effect - client.connect.side_effect = InvalidServerVersion("Invalid version") + client.connect.side_effect = InvalidServerVersion( + VersionInfo("a", "b", 1, 1, 1), 1, "Invalid version" + ) entry = MockConfigEntry( domain=DOMAIN, title="Z-Wave JS", @@ -703,7 +707,9 @@ async def test_issue_registry(hass, client, version_state): device = "/test" network_key = "abc123" - client.connect.side_effect = InvalidServerVersion("Invalid version") + client.connect.side_effect = InvalidServerVersion( + VersionInfo("a", "b", 1, 1, 1), 1, "Invalid version" + ) entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index d2434f01963..cd7d9e91459 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -60,20 +60,9 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 39 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, } assert args["value"] == 255 @@ -92,20 +81,9 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 39 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, } assert args["value"] == 255 assert args["options"]["transitionDuration"] == "10s" @@ -164,20 +142,9 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 39 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, } assert args["value"] == 50 assert args["options"]["transitionDuration"] == "default" @@ -201,20 +168,9 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 39 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, } assert args["value"] == 50 assert args["options"]["transitionDuration"] == "20s" @@ -233,12 +189,9 @@ async def test_light(hass, client, bulb_6_multi_color, integration): args = client.async_send_command.call_args_list[0][0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 39 - assert args["valueId"]["commandClassName"] == "Color Switch" assert args["valueId"]["commandClass"] == 51 assert args["valueId"]["endpoint"] == 0 - assert args["valueId"]["metadata"]["label"] == "Target Color" assert args["valueId"]["property"] == "targetColor" - assert args["valueId"]["propertyName"] == "targetColor" assert args["value"] == { "blue": 255, "coldWhite": 0, @@ -332,12 +285,9 @@ async def test_light(hass, client, bulb_6_multi_color, integration): args = client.async_send_command.call_args_list[0][0][0] # red 0 assert args["command"] == "node.set_value" assert args["nodeId"] == 39 - assert args["valueId"]["commandClassName"] == "Color Switch" assert args["valueId"]["commandClass"] == 51 assert args["valueId"]["endpoint"] == 0 - assert args["valueId"]["metadata"]["label"] == "Target Color" assert args["valueId"]["property"] == "targetColor" - assert args["valueId"]["propertyName"] == "targetColor" assert args["value"] == { "blue": 0, "coldWhite": 235, @@ -438,20 +388,9 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 39 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, } assert args["value"] == 0 @@ -493,20 +432,9 @@ async def test_rgbw_light(hass, client, zen_31, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 94 assert args["valueId"] == { - "commandClassName": "Color Switch", "commandClass": 51, "endpoint": 1, "property": "targetColor", - "propertyName": "targetColor", - "ccVersion": 0, - "metadata": { - "label": "Target Color", - "type": "any", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, - "value": {"blue": 70, "green": 159, "red": 255, "warmWhite": 141}, } assert args["value"] == {"blue": 0, "green": 0, "red": 0, "warmWhite": 128} @@ -514,22 +442,9 @@ async def test_rgbw_light(hass, client, zen_31, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 94 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, "endpoint": 1, "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 0, - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, - "value": 59, } assert args["value"] == 255 @@ -565,19 +480,9 @@ async def test_black_is_off(hass, client, express_controls_ezmultipli, integrati assert args["command"] == "node.set_value" assert args["nodeId"] == node.node_id assert args["valueId"] == { - "commandClassName": "Color Switch", "commandClass": 51, "endpoint": 0, "property": "targetColor", - "propertyName": "targetColor", - "ccVersion": 1, - "metadata": { - "label": "Target Color", - "type": "any", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, } assert args["value"] == {"red": 255, "green": 255, "blue": 255} @@ -656,19 +561,9 @@ async def test_black_is_off(hass, client, express_controls_ezmultipli, integrati assert args["command"] == "node.set_value" assert args["nodeId"] == node.node_id assert args["valueId"] == { - "commandClassName": "Color Switch", "commandClass": 51, "endpoint": 0, "property": "targetColor", - "propertyName": "targetColor", - "ccVersion": 1, - "metadata": { - "label": "Target Color", - "type": "any", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, } assert args["value"] == {"red": 0, "green": 0, "blue": 0} @@ -686,19 +581,9 @@ async def test_black_is_off(hass, client, express_controls_ezmultipli, integrati assert args["command"] == "node.set_value" assert args["nodeId"] == node.node_id assert args["valueId"] == { - "commandClassName": "Color Switch", "commandClass": 51, "endpoint": 0, "property": "targetColor", - "propertyName": "targetColor", - "ccVersion": 1, - "metadata": { - "label": "Target Color", - "type": "any", - "readable": True, - "writeable": True, - "valueChangeOptions": ["transitionDuration"], - }, } assert args["value"] == {"red": 0, "green": 255, "blue": 0} diff --git a/tests/components/zwave_js/test_lock.py b/tests/components/zwave_js/test_lock.py index 2bf4cff8b5b..5f35a568f37 100644 --- a/tests/components/zwave_js/test_lock.py +++ b/tests/components/zwave_js/test_lock.py @@ -44,29 +44,9 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 20 assert args["valueId"] == { - "commandClassName": "Door Lock", "commandClass": 98, "endpoint": 0, "property": "targetMode", - "propertyName": "targetMode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Target lock mode", - "states": { - "0": "Unsecured", - "1": "UnsecuredWithTimeout", - "16": "InsideUnsecured", - "17": "InsideUnsecuredWithTimeout", - "32": "OutsideUnsecured", - "33": "OutsideUnsecuredWithTimeout", - "254": "Unknown", - "255": "Secured", - }, - }, } assert args["value"] == 255 @@ -109,29 +89,9 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 20 assert args["valueId"] == { - "commandClassName": "Door Lock", "commandClass": 98, "endpoint": 0, "property": "targetMode", - "propertyName": "targetMode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Target lock mode", - "states": { - "0": "Unsecured", - "1": "UnsecuredWithTimeout", - "16": "InsideUnsecured", - "17": "InsideUnsecuredWithTimeout", - "32": "OutsideUnsecured", - "33": "OutsideUnsecuredWithTimeout", - "254": "Unknown", - "255": "Secured", - }, - }, } assert args["value"] == 0 @@ -154,22 +114,10 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 20 assert args["valueId"] == { - "commandClassName": "User Code", "commandClass": 99, "endpoint": 0, "property": "userCode", - "propertyName": "userCode", "propertyKey": 1, - "propertyKeyName": "1", - "metadata": { - "type": "string", - "readable": True, - "writeable": True, - "minLength": 4, - "maxLength": 10, - "label": "User Code (1)", - }, - "value": "**********", } assert args["value"] == "1234" @@ -188,25 +136,10 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 20 assert args["valueId"] == { - "commandClassName": "User Code", "commandClass": 99, "endpoint": 0, "property": "userIdStatus", - "propertyName": "userIdStatus", "propertyKey": 1, - "propertyKeyName": "1", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "User ID status (1)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled", - }, - }, - "value": 1, } assert args["value"] == 0 diff --git a/tests/components/zwave_js/test_number.py b/tests/components/zwave_js/test_number.py index e987bfbebc6..e36bd081b18 100644 --- a/tests/components/zwave_js/test_number.py +++ b/tests/components/zwave_js/test_number.py @@ -31,21 +31,9 @@ async def test_number(hass, client, aeotec_radiator_thermostat, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 4 assert args["valueId"] == { - "commandClassName": "Multilevel Switch", "commandClass": 38, - "ccVersion": 1, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "label": "Target value", - "max": 99, - "min": 0, - "type": "number", - "readable": True, - "writeable": True, - "label": "Target value", - }, } assert args["value"] == 30.0 @@ -101,20 +89,7 @@ async def test_volume_number(hass, client, aeotec_zw164_siren, integration): assert args["valueId"] == { "endpoint": 2, "commandClass": 121, - "commandClassName": "Sound Switch", "property": "defaultVolume", - "propertyName": "defaultVolume", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Default volume", - "min": 0, - "max": 100, - "unit": "%", - }, - "value": 100, } assert args["value"] == 30 diff --git a/tests/components/zwave_js/test_select.py b/tests/components/zwave_js/test_select.py index e5b415d1341..1cf5fb54304 100644 --- a/tests/components/zwave_js/test_select.py +++ b/tests/components/zwave_js/test_select.py @@ -81,19 +81,7 @@ async def test_default_tone_select( assert args["valueId"] == { "endpoint": 2, "commandClass": 121, - "commandClassName": "Sound Switch", "property": "defaultToneId", - "propertyName": "defaultToneId", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Default tone ID", - "min": 0, - "max": 254, - }, - "value": 17, } assert args["value"] == 30 @@ -164,22 +152,7 @@ async def test_protection_select( assert args["valueId"] == { "endpoint": 0, "commandClass": 117, - "commandClassName": "Protection", "property": "local", - "propertyName": "local", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Local protection state", - "states": { - "0": "Unprotected", - "1": "ProtectedBySequence", - "2": "NoOperationPossible", - }, - }, - "value": 0, } assert args["value"] == 1 @@ -264,19 +237,7 @@ async def test_multilevel_switch_select(hass, client, fortrezz_ssa1_siren, integ assert args["valueId"] == { "endpoint": 0, "commandClass": 38, - "commandClassName": "Multilevel Switch", "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "min": 0, - "max": 99, - }, } assert args["value"] == 33 diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index b5ef083bf64..6e425bff042 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -78,27 +78,10 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, - "propertyName": "Group 2: Send battery reports", "propertyKey": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 @@ -122,27 +105,10 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, - "propertyName": "Group 2: Send battery reports", "propertyKey": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 @@ -165,27 +131,10 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, - "propertyName": "Group 2: Send battery reports", "propertyKey": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 @@ -208,27 +157,10 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 41, - "propertyName": "Temperature Threshold (Unit)", "propertyKey": 15, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 3, - "min": 1, - "max": 2, - "default": 1, - "format": 0, - "allowManualEntry": False, - "states": {"1": "Celsius", "2": "Fahrenheit"}, - "label": "Temperature Threshold (Unit)", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 2 @@ -254,27 +186,10 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 41, - "propertyName": "Temperature Threshold (Unit)", "propertyKey": 15, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 3, - "min": 1, - "max": 2, - "default": 1, - "format": 0, - "allowManualEntry": False, - "states": {"1": "Celsius", "2": "Fahrenheit"}, - "label": "Temperature Threshold (Unit)", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 2 @@ -298,27 +213,10 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, - "propertyName": "Group 2: Send battery reports", "propertyKey": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 @@ -344,27 +242,10 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, - "propertyName": "Group 2: Send battery reports", "propertyKey": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 @@ -431,27 +312,10 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, - "propertyName": "Group 2: Send battery reports", "propertyKey": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 @@ -477,27 +341,10 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, - "propertyName": "Group 2: Send battery reports", "propertyKey": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 @@ -551,32 +398,7 @@ async def test_set_config_parameter_gather( assert args["valueId"] == { "endpoint": 0, "commandClass": 112, - "commandClassName": "Configuration", "property": 1, - "propertyName": "Temperature Reporting Threshold", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "description": "Reporting threshold for changes in the ambient temperature", - "label": "Temperature Reporting Threshold", - "default": 2, - "min": 0, - "max": 4, - "states": { - "0": "Disabled", - "1": "0.5\u00b0 F", - "2": "1.0\u00b0 F", - "3": "1.5\u00b0 F", - "4": "2.0\u00b0 F", - }, - "valueSize": 1, - "format": 0, - "allowManualEntry": False, - "isFromConfig": True, - }, - "value": 1, } assert args["value"] == 1 @@ -847,26 +669,9 @@ async def test_refresh_value( assert args["command"] == "node.poll_value" assert args["nodeId"] == 26 assert args["valueId"] == { - "commandClassName": "Thermostat Mode", "commandClass": 64, "endpoint": 1, "property": "mode", - "propertyName": "mode", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "min": 0, - "max": 255, - "label": "Thermostat mode", - "states": { - "0": "Off", - "1": "Heat", - "2": "Cool", - }, - }, - "value": 2, - "ccVersion": 0, } client.async_send_command.reset_mock() @@ -950,20 +755,9 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 5 assert args["valueId"] == { - "commandClassName": "Protection", "commandClass": 117, "endpoint": 0, "property": "local", - "propertyName": "local", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Local protection state", - "states": {"0": "Unprotected", "2": "NoOperationPossible"}, - }, - "value": 0, } assert args["value"] == 2 @@ -988,20 +782,9 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 5 assert args["valueId"] == { - "commandClassName": "Protection", "commandClass": 117, "endpoint": 0, "property": "local", - "propertyName": "local", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Local protection state", - "states": {"0": "Unprotected", "2": "NoOperationPossible"}, - }, - "value": 0, } assert args["value"] == 2 @@ -1029,20 +812,9 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 5 assert args["valueId"] == { - "commandClassName": "Protection", "commandClass": 117, "endpoint": 0, "property": "local", - "propertyName": "local", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Local protection state", - "states": {"0": "Unprotected", "2": "NoOperationPossible"}, - }, - "value": 0, } assert args["value"] == 2 @@ -1069,20 +841,9 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 5 assert args["valueId"] == { - "commandClassName": "Protection", "commandClass": 117, "endpoint": 0, "property": "local", - "propertyName": "local", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Local protection state", - "states": {"0": "Unprotected", "2": "NoOperationPossible"}, - }, - "value": 0, } assert args["value"] == 2 @@ -1111,20 +872,9 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): assert args["command"] == "node.set_value" assert args["nodeId"] == 5 assert args["valueId"] == { - "commandClassName": "Protection", "commandClass": 117, "endpoint": 0, "property": "local", - "propertyName": "local", - "ccVersion": 2, - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "label": "Local protection state", - "states": {"0": "Unprotected", "2": "NoOperationPossible"}, - }, - "value": 0, } assert args["value"] == 2 @@ -1170,22 +920,10 @@ async def test_set_value_string( assert args["command"] == "node.set_value" assert args["nodeId"] == lock_schlage_be469.node_id assert args["valueId"] == { - "commandClassName": "User Code", "commandClass": 99, "endpoint": 0, "property": "userCode", - "propertyName": "userCode", "propertyKey": 1, - "propertyKeyName": "1", - "metadata": { - "type": "string", - "readable": True, - "writeable": True, - "minLength": 4, - "maxLength": 10, - "label": "User Code (1)", - }, - "value": "**********", } assert args["value"] == "12345" @@ -1212,17 +950,7 @@ async def test_set_value_options(hass, client, aeon_smart_switch_6, integration) assert args["valueId"] == { "endpoint": 0, "commandClass": 37, - "commandClassName": "Binary Switch", "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - }, } assert args["value"] == 2 assert args["options"] == {"transitionDuration": 1} @@ -1263,27 +991,10 @@ async def test_set_value_gather( assert args["command"] == "node.set_value" assert args["nodeId"] == 52 assert args["valueId"] == { - "commandClassName": "Configuration", "commandClass": 112, "endpoint": 0, "property": 102, "propertyKey": 1, - "propertyName": "Group 2: Send battery reports", - "metadata": { - "type": "number", - "readable": True, - "writeable": True, - "valueSize": 4, - "min": 0, - "max": 1, - "default": 1, - "format": 0, - "allowManualEntry": True, - "label": "Group 2: Send battery reports", - "description": "Include battery information in periodic reports to Group 2", - "isFromConfig": True, - }, - "value": 0, } assert args["value"] == 1 diff --git a/tests/components/zwave_js/test_siren.py b/tests/components/zwave_js/test_siren.py index 3284526aa0f..75d43c545bd 100644 --- a/tests/components/zwave_js/test_siren.py +++ b/tests/components/zwave_js/test_siren.py @@ -115,7 +115,11 @@ async def test_siren(hass, client, aeotec_zw164_siren, integration): args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == node.node_id - assert args["valueId"] == TONE_ID_VALUE_ID + assert args["valueId"] == { + "endpoint": 2, + "commandClass": 121, + "property": "toneId", + } assert args["value"] == 255 client.async_send_command.reset_mock() @@ -159,7 +163,11 @@ async def test_siren(hass, client, aeotec_zw164_siren, integration): args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == node.node_id - assert args["valueId"] == {**TONE_ID_VALUE_ID, "value": 255} + assert args["valueId"] == { + "endpoint": 2, + "commandClass": 121, + "property": "toneId", + } assert args["value"] == 1 assert args["options"] == {"volume": 50} @@ -181,7 +189,11 @@ async def test_siren(hass, client, aeotec_zw164_siren, integration): args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == node.node_id - assert args["valueId"] == {**TONE_ID_VALUE_ID, "value": 255} + assert args["valueId"] == { + "endpoint": 2, + "commandClass": 121, + "property": "toneId", + } assert args["value"] == 1 assert args["options"] == {"volume": 50} @@ -199,7 +211,11 @@ async def test_siren(hass, client, aeotec_zw164_siren, integration): args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == node.node_id - assert args["valueId"] == {**TONE_ID_VALUE_ID, "value": 255} + assert args["valueId"] == { + "endpoint": 2, + "commandClass": 121, + "property": "toneId", + } assert args["value"] == 0 client.async_send_command.reset_mock() diff --git a/tests/components/zwave_js/test_switch.py b/tests/components/zwave_js/test_switch.py index ea6e27d9b72..b84ab32f618 100644 --- a/tests/components/zwave_js/test_switch.py +++ b/tests/components/zwave_js/test_switch.py @@ -25,18 +25,9 @@ async def test_switch(hass, hank_binary_switch, integration, client): assert args["command"] == "node.set_value" assert args["nodeId"] == 32 assert args["valueId"] == { - "commandClassName": "Binary Switch", "commandClass": 37, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Target value", - }, - "value": False, } assert args["value"] is True @@ -72,18 +63,9 @@ async def test_switch(hass, hank_binary_switch, integration, client): assert args["command"] == "node.set_value" assert args["nodeId"] == 32 assert args["valueId"] == { - "commandClassName": "Binary Switch", "commandClass": 37, "endpoint": 0, "property": "targetValue", - "propertyName": "targetValue", - "metadata": { - "type": "boolean", - "readable": True, - "writeable": True, - "label": "Target value", - }, - "value": False, } assert args["value"] is False @@ -108,24 +90,10 @@ async def test_barrier_signaling_switch(hass, gdc_zw062, integration, client): assert args["nodeId"] == 12 assert args["value"] == 0 assert args["valueId"] == { - "ccVersion": 0, "commandClass": 102, - "commandClassName": "Barrier Operator", "endpoint": 0, - "metadata": { - "label": "Signaling State (Visual)", - "max": 255, - "min": 0, - "readable": True, - "states": {"0": "Off", "255": "On"}, - "type": "number", - "writeable": True, - }, "property": "signalingState", "propertyKey": 2, - "propertyKeyName": "2", - "propertyName": "signalingState", - "value": 255, } # state change is optimistic and writes state @@ -149,24 +117,10 @@ async def test_barrier_signaling_switch(hass, gdc_zw062, integration, client): assert args["nodeId"] == 12 assert args["value"] == 255 assert args["valueId"] == { - "ccVersion": 0, "commandClass": 102, - "commandClassName": "Barrier Operator", "endpoint": 0, - "metadata": { - "label": "Signaling State (Visual)", - "max": 255, - "min": 0, - "readable": True, - "states": {"0": "Off", "255": "On"}, - "type": "number", - "writeable": True, - }, "property": "signalingState", "propertyKey": 2, - "propertyKeyName": "2", - "propertyName": "signalingState", - "value": 255, } # state change is optimistic and writes state From 768b83139fe9704c35017f7dd8f9322cb38bc0c5 Mon Sep 17 00:00:00 2001 From: HarvsG <11440490+HarvsG@users.noreply.github.com> Date: Thu, 29 Sep 2022 01:39:15 +0000 Subject: [PATCH 011/985] Add to issue registry if user has mirrored entries for breaking in #67631 (#79208) Co-authored-by: Diogo Gomes --- .../components/bayesian/binary_sensor.py | 17 ++++ homeassistant/components/bayesian/repairs.py | 39 ++++++++ .../components/bayesian/translations/en.json | 8 ++ .../components/bayesian/test_binary_sensor.py | 92 +++++++++++++++++++ 4 files changed, 156 insertions(+) create mode 100644 homeassistant/components/bayesian/repairs.py create mode 100644 homeassistant/components/bayesian/translations/en.json diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 73ebcc8b37e..0e943b2d0ad 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -34,6 +34,7 @@ from homeassistant.helpers.template import result_as_boolean from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DOMAIN, PLATFORMS +from .repairs import raise_mirrored_entries ATTR_OBSERVATIONS = "observations" ATTR_OCCURRED_OBSERVATION_ENTITIES = "occurred_observation_entities" @@ -245,6 +246,22 @@ class BayesianBinarySensor(BinarySensorEntity): self.probability = self._calculate_new_probability() self._attr_is_on = bool(self.probability >= self._probability_threshold) + # detect mirrored entries + for entity, observations in self.observations_by_entity.items(): + raise_mirrored_entries( + self.hass, observations, text=f"{self._attr_name}/{entity}" + ) + + all_template_observations = [] + for value in self.observations_by_template.values(): + all_template_observations.append(value[0]) + if len(all_template_observations) == 2: + raise_mirrored_entries( + self.hass, + all_template_observations, + text=f"{self._attr_name}/{all_template_observations[0]['value_template']}", + ) + @callback def _recalculate_and_write_state(self): self.probability = self._calculate_new_probability() diff --git a/homeassistant/components/bayesian/repairs.py b/homeassistant/components/bayesian/repairs.py new file mode 100644 index 00000000000..a1391f8c550 --- /dev/null +++ b/homeassistant/components/bayesian/repairs.py @@ -0,0 +1,39 @@ +"""Helpers for generating repairs.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry + +from . import DOMAIN + + +def raise_mirrored_entries(hass: HomeAssistant, observations, text: str = "") -> None: + """If there are mirrored entries, the user is probably using a workaround for a patched bug.""" + if len(observations) != 2: + return + true_sums_1: bool = ( + round( + observations[0]["prob_given_true"] + observations[1]["prob_given_true"], 1 + ) + == 1.0 + ) + false_sums_1: bool = ( + round( + observations[0]["prob_given_false"] + observations[1]["prob_given_false"], 1 + ) + == 1.0 + ) + same_states: bool = observations[0]["platform"] == observations[1]["platform"] + if true_sums_1 & false_sums_1 & same_states: + issue_registry.async_create_issue( + hass, + DOMAIN, + "mirrored_entry/" + text, + breaks_in_ha_version="2022.10.0", + is_fixable=False, + is_persistent=False, + severity=issue_registry.IssueSeverity.WARNING, + translation_key="manual_migration", + translation_placeholders={"entity": text}, + learn_more_url="https://github.com/home-assistant/core/pull/67631", + ) diff --git a/homeassistant/components/bayesian/translations/en.json b/homeassistant/components/bayesian/translations/en.json new file mode 100644 index 00000000000..ae9e5645f73 --- /dev/null +++ b/homeassistant/components/bayesian/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "manual_migration": { + "description": "The Bayesian integration now also updates the probability if the observed `to_state`, `above`, `below`, or `value_template` evaluates to `False` rather than only `True`. So it is no longer required to have duplicate, complementary entries for each binary state. Please remove the mirrored entry for `{entity}`.", + "title": "Manual YAML fix required for Bayesian" + } + } +} diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 357cacb4214..0344e2b9445 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, callback from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.issue_registry import async_get from homeassistant.setup import async_setup_component from tests.common import get_fixture_path @@ -184,6 +185,8 @@ async def test_sensor_numeric_state(hass): assert state.state == "off" + assert len(async_get(hass).issues) == 0 + async def test_sensor_state(hass): """Test sensor on state platform observations.""" @@ -341,6 +344,7 @@ async def test_threshold(hass): assert round(abs(1.0 - state.attributes.get("probability")), 7) == 0 assert state.state == "on" + assert len(async_get(hass).issues) == 0 async def test_multiple_observations(hass): @@ -495,6 +499,94 @@ async def test_multiple_numeric_observations(hass): assert state.attributes.get("observations")[1]["platform"] == "numeric_state" +async def test_mirrored_observations(hass): + """Test whether mirrored entries are detected and appropriate issues are created.""" + + config = { + "binary_sensor": { + "platform": "bayesian", + "name": "Test_Binary", + "observations": [ + { + "platform": "state", + "entity_id": "binary_sensor.test_monitored", + "to_state": "on", + "prob_given_true": 0.8, + "prob_given_false": 0.4, + }, + { + "platform": "state", + "entity_id": "binary_sensor.test_monitored", + "to_state": "off", + "prob_given_true": 0.2, + "prob_given_false": 0.59, + }, + { + "platform": "numeric_state", + "entity_id": "sensor.test_monitored1", + "above": 5, + "prob_given_true": 0.7, + "prob_given_false": 0.4, + }, + { + "platform": "numeric_state", + "entity_id": "sensor.test_monitored1", + "below": 5, + "prob_given_true": 0.3, + "prob_given_false": 0.6, + }, + { + "platform": "template", + "value_template": "{{states('sensor.test_monitored2') == 'off'}}", + "prob_given_true": 0.79, + "prob_given_false": 0.4, + }, + { + "platform": "template", + "value_template": "{{states('sensor.test_monitored2') == 'on'}}", + "prob_given_true": 0.2, + "prob_given_false": 0.6, + }, + { + "platform": "state", + "entity_id": "sensor.colour", + "to_state": "blue", + "prob_given_true": 0.33, + "prob_given_false": 0.8, + }, + { + "platform": "state", + "entity_id": "sensor.colour", + "to_state": "green", + "prob_given_true": 0.3, + "prob_given_false": 0.15, + }, + { + "platform": "state", + "entity_id": "sensor.colour", + "to_state": "red", + "prob_given_true": 0.4, + "prob_given_false": 0.05, + }, + ], + "prior": 0.1, + } + } + assert len(async_get(hass).issues) == 0 + assert await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + hass.states.async_set("sensor.test_monitored2", "on") + await hass.async_block_till_done() + + assert len(async_get(hass).issues) == 3 + assert ( + async_get(hass).issues[ + ("bayesian", "mirrored_entry/Test_Binary/sensor.test_monitored1") + ] + is not None + ) + + async def test_probability_updates(hass): """Test probability update function.""" prob_given_true = [0.3, 0.6, 0.8] From 473d7c484d2bf830944162e04d40c86beb9a8b2d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 29 Sep 2022 03:45:15 +0200 Subject: [PATCH 012/985] Improve deCONZ sensor classes (#79137) --- homeassistant/components/deconz/sensor.py | 334 +++++++++++----------- 1 file changed, 163 insertions(+), 171 deletions(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 09d248756fc..1b24098f717 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -1,12 +1,15 @@ """Support for deCONZ sensors.""" + from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime +from typing import Generic, TypeVar from pydeconz.interfaces.sensors import SensorResources from pydeconz.models.event import EventType +from pydeconz.models.sensor import SensorBase as PydeconzSensorBase from pydeconz.models.sensor.air_quality import AirQuality from pydeconz.models.sensor.consumption import Consumption from pydeconz.models.sensor.daylight import DAYLIGHT_STATUS, Daylight @@ -67,171 +70,163 @@ ATTR_DAYLIGHT = "daylight" ATTR_EVENT_ID = "event_id" +T = TypeVar( + "T", + AirQuality, + Consumption, + Daylight, + GenericStatus, + Humidity, + LightLevel, + Power, + Pressure, + Temperature, + Time, + PydeconzSensorBase, +) + + @dataclass -class DeconzSensorDescriptionMixin: +class DeconzSensorDescriptionMixin(Generic[T]): """Required values when describing secondary sensor attributes.""" + isinstance_fn: Callable[[T], bool] update_key: str - value_fn: Callable[[SensorResources], float | int | str | None] + value_fn: Callable[[T], datetime | StateType] @dataclass class DeconzSensorDescription( - SensorEntityDescription, - DeconzSensorDescriptionMixin, + SensorEntityDescription, DeconzSensorDescriptionMixin[T], Generic[T] ): """Class describing deCONZ binary sensor entities.""" - suffix: str = "" + common: bool = False + name_suffix: str = "" + old_unique_id_suffix: str = "" -ENTITY_DESCRIPTIONS = { - AirQuality: [ - DeconzSensorDescription( - key="air_quality", - value_fn=lambda device: device.air_quality - if isinstance(device, AirQuality) - else None, - update_key="airquality", - state_class=SensorStateClass.MEASUREMENT, - ), - DeconzSensorDescription( - key="air_quality_ppb", - value_fn=lambda device: device.air_quality_ppb - if isinstance(device, AirQuality) - else None, - suffix="PPB", - update_key="airqualityppb", - device_class=SensorDeviceClass.AQI, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, - ), - ], - Consumption: [ - DeconzSensorDescription( - key="consumption", - value_fn=lambda device: device.scaled_consumption - if isinstance(device, Consumption) and isinstance(device.consumption, int) - else None, - update_key="consumption", - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - ) - ], - Daylight: [ - DeconzSensorDescription( - key="daylight_status", - value_fn=lambda device: DAYLIGHT_STATUS[device.daylight_status] - if isinstance(device, Daylight) - else None, - update_key="status", - icon="mdi:white-balance-sunny", - entity_registry_enabled_default=False, - ) - ], - GenericStatus: [ - DeconzSensorDescription( - key="status", - value_fn=lambda device: device.status - if isinstance(device, GenericStatus) - else None, - update_key="status", - ) - ], - Humidity: [ - DeconzSensorDescription( - key="humidity", - value_fn=lambda device: device.scaled_humidity - if isinstance(device, Humidity) and isinstance(device.humidity, int) - else None, - update_key="humidity", - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - ) - ], - LightLevel: [ - DeconzSensorDescription( - key="light_level", - value_fn=lambda device: device.scaled_light_level - if isinstance(device, LightLevel) and isinstance(device.light_level, int) - else None, - update_key="lightlevel", - device_class=SensorDeviceClass.ILLUMINANCE, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=LIGHT_LUX, - ) - ], - Power: [ - DeconzSensorDescription( - key="power", - value_fn=lambda device: device.power if isinstance(device, Power) else None, - update_key="power", - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=POWER_WATT, - ) - ], - Pressure: [ - DeconzSensorDescription( - key="pressure", - value_fn=lambda device: device.pressure - if isinstance(device, Pressure) - else None, - update_key="pressure", - device_class=SensorDeviceClass.PRESSURE, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PRESSURE_HPA, - ) - ], - Temperature: [ - DeconzSensorDescription( - key="temperature", - value_fn=lambda device: device.scaled_temperature - if isinstance(device, Temperature) and isinstance(device.temperature, int) - else None, - update_key="temperature", - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=TEMP_CELSIUS, - ) - ], - Time: [ - DeconzSensorDescription( - key="last_set", - value_fn=lambda device: device.last_set - if isinstance(device, Time) - else None, - update_key="lastset", - device_class=SensorDeviceClass.TIMESTAMP, - state_class=SensorStateClass.TOTAL_INCREASING, - ) - ], -} - - -COMMON_SENSOR_DESCRIPTIONS = [ - DeconzSensorDescription( - key="battery", - value_fn=lambda device: device.battery, - suffix="Battery", - update_key="battery", - device_class=SensorDeviceClass.BATTERY, +ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( + DeconzSensorDescription[AirQuality]( + key="air_quality", + isinstance_fn=lambda device: isinstance(device, AirQuality), + value_fn=lambda device: device.air_quality, + update_key="airquality", + state_class=SensorStateClass.MEASUREMENT, + ), + DeconzSensorDescription[AirQuality]( + key="air_quality_ppb", + isinstance_fn=lambda device: isinstance(device, AirQuality), + value_fn=lambda device: device.air_quality_ppb, + update_key="airqualityppb", + name_suffix="PPB", + old_unique_id_suffix="ppb", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + ), + DeconzSensorDescription[Consumption]( + key="consumption", + isinstance_fn=lambda device: isinstance(device, Consumption), + value_fn=lambda device: device.scaled_consumption, + update_key="consumption", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DeconzSensorDescription[Daylight]( + key="daylight_status", + isinstance_fn=lambda device: isinstance(device, Daylight), + value_fn=lambda device: DAYLIGHT_STATUS[device.daylight_status], + update_key="status", + icon="mdi:white-balance-sunny", + entity_registry_enabled_default=False, + ), + DeconzSensorDescription[GenericStatus]( + key="status", + isinstance_fn=lambda device: isinstance(device, GenericStatus), + value_fn=lambda device: device.status, + update_key="status", + ), + DeconzSensorDescription[Humidity]( + key="humidity", + isinstance_fn=lambda device: isinstance(device, Humidity), + value_fn=lambda device: device.scaled_humidity, + update_key="humidity", + device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, - entity_category=EntityCategory.DIAGNOSTIC, ), - DeconzSensorDescription( - key="internal_temperature", - value_fn=lambda device: device.internal_temperature, - suffix="Temperature", + DeconzSensorDescription[LightLevel]( + key="light_level", + isinstance_fn=lambda device: isinstance(device, LightLevel), + value_fn=lambda device: device.scaled_light_level, + update_key="lightlevel", + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=LIGHT_LUX, + ), + DeconzSensorDescription[Power]( + key="power", + isinstance_fn=lambda device: isinstance(device, Power), + value_fn=lambda device: device.power, + update_key="power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + ), + DeconzSensorDescription[Pressure]( + key="pressure", + isinstance_fn=lambda device: isinstance(device, Pressure), + value_fn=lambda device: device.pressure, + update_key="pressure", + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PRESSURE_HPA, + ), + DeconzSensorDescription[Temperature]( + key="temperature", + isinstance_fn=lambda device: isinstance(device, Temperature), + value_fn=lambda device: device.scaled_temperature, update_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), -] + DeconzSensorDescription[Time]( + key="last_set", + isinstance_fn=lambda device: isinstance(device, Time), + value_fn=lambda device: dt_util.parse_datetime(device.last_set), + update_key="lastset", + device_class=SensorDeviceClass.TIMESTAMP, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DeconzSensorDescription[SensorResources]( + key="battery", + isinstance_fn=lambda device: isinstance(device, PydeconzSensorBase), + value_fn=lambda device: device.battery, + update_key="battery", + common=True, + name_suffix="Battery", + old_unique_id_suffix="battery", + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DeconzSensorDescription[SensorResources]( + key="internal_temperature", + isinstance_fn=lambda device: isinstance(device, PydeconzSensorBase), + value_fn=lambda device: device.internal_temperature, + update_key="temperature", + common=True, + name_suffix="Temperature", + old_unique_id_suffix="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + ), +) @callback @@ -248,8 +243,8 @@ def async_update_unique_id( if ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, new_unique_id): return - if description.suffix: - unique_id = f'{unique_id.split("-", 1)[0]}-{description.suffix.lower()}' + if description.old_unique_id_suffix: + unique_id = f'{unique_id.split("-", 1)[0]}-{description.old_unique_id_suffix}' if entity_id := ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, unique_id): ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) @@ -265,7 +260,9 @@ async def async_setup_entry( gateway.entities[DOMAIN] = set() known_device_entities: dict[str, set[str]] = { - description.key: set() for description in COMMON_SENSOR_DESCRIPTIONS + description.key: set() + for description in ENTITY_DESCRIPTIONS + if description.common } @callback @@ -274,17 +271,15 @@ async def async_setup_entry( sensor = gateway.api.sensors[sensor_id] entities: list[DeconzSensor] = [] - for description in ( - ENTITY_DESCRIPTIONS.get(type(sensor), []) + COMMON_SENSOR_DESCRIPTIONS - ): + for description in ENTITY_DESCRIPTIONS: + if not description.isinstance_fn(sensor): + continue + no_sensor_data = False - if ( - not hasattr(sensor, description.key) - or description.value_fn(sensor) is None - ): + if description.value_fn(sensor) is None: no_sensor_data = True - if description in COMMON_SENSOR_DESCRIPTIONS: + if description.common: if ( sensor.type.startswith("CLIP") or (no_sensor_data and description.key != "battery") @@ -296,7 +291,10 @@ async def async_setup_entry( continue known_device_entities[description.key].add(unique_id) if no_sensor_data and description.key == "battery": - DeconzBatteryTracker(sensor_id, gateway, async_add_entities) + async_update_unique_id(hass, sensor.unique_id, description) + DeconzBatteryTracker( + sensor_id, gateway, description, async_add_entities + ) continue if no_sensor_data: @@ -327,9 +325,10 @@ class DeconzSensor(DeconzDevice[SensorResources], SensorEntity): ) -> None: """Initialize deCONZ sensor.""" self.entity_description = description + self.unique_id_suffix = description.key self._update_key = description.update_key - if description.suffix: - self._name_suffix = description.suffix + if description.name_suffix: + self._name_suffix = description.name_suffix super().__init__(device, gateway) if ( @@ -338,18 +337,9 @@ class DeconzSensor(DeconzDevice[SensorResources], SensorEntity): ): self._update_keys.update({"on", "state"}) - @property - def unique_id(self) -> str: - """Return a unique identifier for this device.""" - return f"{self._device.unique_id}-{self.entity_description.key}" - @property def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" - if self.entity_description.device_class is SensorDeviceClass.TIMESTAMP: - value = self.entity_description.value_fn(self._device) - assert isinstance(value, str) - return dt_util.parse_datetime(value) return self.entity_description.value_fn(self._device) @property @@ -399,19 +389,21 @@ class DeconzBatteryTracker: self, sensor_id: str, gateway: DeconzGateway, + description: DeconzSensorDescription, async_add_entities: AddEntitiesCallback, ) -> None: """Set up tracker.""" self.sensor = gateway.api.sensors[sensor_id] self.gateway = gateway + self.description = description self.async_add_entities = async_add_entities self.unsubscribe = self.sensor.subscribe(self.async_update_callback) @callback def async_update_callback(self) -> None: """Update the device's state.""" - if "battery" in self.sensor.changed_keys: + if self.description.update_key in self.sensor.changed_keys: self.unsubscribe() - desc = COMMON_SENSOR_DESCRIPTIONS[0] - async_update_unique_id(self.gateway.hass, self.sensor.unique_id, desc) - self.async_add_entities([DeconzSensor(self.sensor, self.gateway, desc)]) + self.async_add_entities( + [DeconzSensor(self.sensor, self.gateway, self.description)] + ) From 45ecddb9aa1b015e6276523c7a871603ba455d2c Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Thu, 29 Sep 2022 03:50:30 +0200 Subject: [PATCH 013/985] Remove route sensor of here_travel_time (#79211) --- homeassistant/components/here_travel_time/__init__.py | 2 -- homeassistant/components/here_travel_time/const.py | 1 - homeassistant/components/here_travel_time/model.py | 1 - homeassistant/components/here_travel_time/sensor.py | 6 ------ tests/components/here_travel_time/test_sensor.py | 4 ---- 5 files changed, 14 deletions(-) diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index 549cc08356c..8f63060b683 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -33,7 +33,6 @@ from .const import ( ATTR_DURATION_IN_TRAFFIC, ATTR_ORIGIN, ATTR_ORIGIN_NAME, - ATTR_ROUTE, CONF_ARRIVAL_TIME, CONF_DEPARTURE_TIME, CONF_DESTINATION_ENTITY_ID, @@ -190,7 +189,6 @@ class HereTravelTimeDataUpdateCoordinator(DataUpdateCoordinator): ATTR_DURATION: round(summary["baseTime"] / 60), # type: ignore[misc] ATTR_DURATION_IN_TRAFFIC: round(traffic_time / 60), ATTR_DISTANCE: distance, - ATTR_ROUTE: response.route_short, ATTR_ORIGIN: ",".join(origin), ATTR_DESTINATION: ",".join(destination), ATTR_ORIGIN_NAME: waypoint[0]["mappedRoadName"], diff --git a/homeassistant/components/here_travel_time/const.py b/homeassistant/components/here_travel_time/const.py index 4e9b8beaf12..b79e7b4e4cd 100644 --- a/homeassistant/components/here_travel_time/const.py +++ b/homeassistant/components/here_travel_time/const.py @@ -69,7 +69,6 @@ UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] ATTR_DURATION = "duration" ATTR_DISTANCE = "distance" -ATTR_ROUTE = "route" ATTR_ORIGIN = "origin" ATTR_DESTINATION = "destination" diff --git a/homeassistant/components/here_travel_time/model.py b/homeassistant/components/here_travel_time/model.py index 65673a1e8b6..7310ac24e77 100644 --- a/homeassistant/components/here_travel_time/model.py +++ b/homeassistant/components/here_travel_time/model.py @@ -13,7 +13,6 @@ class HERERoutingData(TypedDict): ATTR_DURATION: float ATTR_DURATION_IN_TRAFFIC: float ATTR_DISTANCE: float - ATTR_ROUTE: str ATTR_ORIGIN: str ATTR_DESTINATION: str ATTR_ORIGIN_NAME: str diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 74a9ae357e1..537c32e423b 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -38,7 +38,6 @@ from .const import ( ATTR_DURATION_IN_TRAFFIC, ATTR_ORIGIN, ATTR_ORIGIN_NAME, - ATTR_ROUTE, DOMAIN, ICON_CAR, ICONS, @@ -64,11 +63,6 @@ def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...] state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TIME_MINUTES, ), - SensorEntityDescription( - name="Route", - icon="mdi:directions", - key=ATTR_ROUTE, - ), ) diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 60b1f5fcced..316563b20cf 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -179,10 +179,6 @@ async def test_sensor( hass.states.get("sensor.test_distance").attributes.get(ATTR_UNIT_OF_MEASUREMENT) == expected_distance_unit ) - assert hass.states.get("sensor.test_route").state == ( - "US-29 - K St NW; US-29 - Whitehurst Fwy; " - "I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd" - ) assert ( hass.states.get("sensor.test_duration_in_traffic").state == expected_duration_in_traffic From 1a9bcafbd209e166e43a66fb3609b3c14eb9da36 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 28 Sep 2022 18:54:12 -0700 Subject: [PATCH 014/985] Clean up Add spotify support to forked-daapd (#79213) --- .../forked_daapd/test_browse_media.py | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/components/forked_daapd/test_browse_media.py b/tests/components/forked_daapd/test_browse_media.py index ff26b6f9315..bedb541b803 100644 --- a/tests/components/forked_daapd/test_browse_media.py +++ b/tests/components/forked_daapd/test_browse_media.py @@ -229,7 +229,7 @@ async def test_async_browse_spotify(hass, hass_ws_client, config_entry): assert await async_setup_component(hass, spotify.DOMAIN, {}) await hass.async_block_till_done() config_entry.add_to_hass(hass) - await config_entry.async_setup(hass) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() with patch( "homeassistant.components.forked_daapd.media_player.spotify_async_browse_media" @@ -273,6 +273,52 @@ async def test_async_browse_spotify(hass, hass_ws_client, config_entry): assert msg["success"] +async def test_async_browse_media_source(hass, hass_ws_client, config_entry): + """Test browsing media_source.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + with patch( + "homeassistant.components.forked_daapd.media_player.media_source.async_browse_media" + ) as mock_media_source_browse: + children = [ + BrowseMedia( + title="Test mp3", + media_class=MediaClass.MUSIC, + media_content_id="media-source://test_dir/test.mp3", + media_content_type="audio/aac", + can_play=False, + can_expand=True, + ) + ] + mock_media_source_browse.return_value = BrowseMedia( + title="Audio Folder", + media_class=MediaClass.DIRECTORY, + media_content_id="media-source://audio_folder", + media_content_type=MediaType.APP, + can_play=False, + can_expand=True, + children=children, + ) + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": TEST_MASTER_ENTITY_NAME, + "media_content_type": MediaType.APP, + "media_content_id": "media-source://audio_folder", + } + ) + msg = await client.receive_json() + # Assert WebSocket response + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + async def test_async_browse_image(hass, hass_client, config_entry): """Test browse media images.""" From 504ce8e93aa90b3491f2292cb8a3210631aefc39 Mon Sep 17 00:00:00 2001 From: PeteRager <76050312+PeteRager@users.noreply.github.com> Date: Wed, 28 Sep 2022 22:23:11 -0400 Subject: [PATCH 015/985] Set nest entities as unavailable on lost connection (#78773) * NEST - Issues with lost internet connectivity #70479 Update Climate and Sensor entities to be unavailable when the device connectivity trait indicates the device is offline. The prior behavior, the last known values would be displayed indefinitely if the device lost internet connectivity. This was creating the illusion that the device was still connected. With this change, the Home Assistant entities will become unavailable when the device loses connectivity. * Update formatting * Add doc strings, fix indentation * Fix doc strings * Update test_climate_sdm.py * Update test_climate_sdm.py * Update test_sensor_sdm.py * Update test_sensor_sdm.py * more formatting fixes * Place availability logic in mixin 1. Consolidate repeated code into mixin and apply mixin to Climate and Sensor entities 2. Return true instead of super.available() 3. No unit test changes required to maintain code coverage * Define self._device is mixin to make linter happier * Remove logger used for debugging * restore whitespace * Fix test due to underlying merge change * Update availability_mixin.py * Move availability logic into device_info * Update sensor_sdm.py --- homeassistant/components/nest/climate_sdm.py | 5 ++ homeassistant/components/nest/const.py | 2 + homeassistant/components/nest/device_info.py | 13 +++- homeassistant/components/nest/sensor_sdm.py | 5 ++ tests/components/nest/test_climate_sdm.py | 66 +++++++++++++++++++- tests/components/nest/test_sensor_sdm.py | 53 ++++++++++++++++ 6 files changed, 141 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index e40db60d5ed..3113cb2dd40 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -117,6 +117,11 @@ class ThermostatEntity(ClimateEntity): """Return device specific attributes.""" return self._device_info.device_info + @property + def available(self) -> bool: + """Return device availability.""" + return self._device_info.available + async def async_added_to_hass(self) -> None: """Run when entity is added to register update signal handler.""" self._attr_supported_features = self._get_supported_features() diff --git a/homeassistant/components/nest/const.py b/homeassistant/components/nest/const.py index 64c27c1643b..853e778977d 100644 --- a/homeassistant/components/nest/const.py +++ b/homeassistant/components/nest/const.py @@ -14,6 +14,8 @@ CONF_SUBSCRIBER_ID = "subscriber_id" CONF_SUBSCRIBER_ID_IMPORTED = "subscriber_id_imported" CONF_CLOUD_PROJECT_ID = "cloud_project_id" +CONNECTIVITY_TRAIT_OFFLINE = "OFFLINE" + SIGNAL_NEST_UPDATE = "nest_update" # For the Google Nest Device Access API diff --git a/homeassistant/components/nest/device_info.py b/homeassistant/components/nest/device_info.py index 2d2b01d3849..e269b76fcc4 100644 --- a/homeassistant/components/nest/device_info.py +++ b/homeassistant/components/nest/device_info.py @@ -5,13 +5,13 @@ from __future__ import annotations from collections.abc import Mapping from google_nest_sdm.device import Device -from google_nest_sdm.device_traits import InfoTrait +from google_nest_sdm.device_traits import ConnectivityTrait, InfoTrait from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo -from .const import DATA_DEVICE_MANAGER, DOMAIN +from .const import CONNECTIVITY_TRAIT_OFFLINE, DATA_DEVICE_MANAGER, DOMAIN DEVICE_TYPE_MAP: dict[str, str] = { "sdm.devices.types.CAMERA": "Camera", @@ -30,6 +30,15 @@ class NestDeviceInfo: """Initialize the DeviceInfo.""" self._device = device + @property + def available(self) -> bool: + """Return device availability.""" + if ConnectivityTrait.NAME in self._device.traits: + trait: ConnectivityTrait = self._device.traits[ConnectivityTrait.NAME] + if trait.status == CONNECTIVITY_TRAIT_OFFLINE: + return False + return True + @property def device_info(self) -> DeviceInfo: """Return device specific attributes.""" diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index 11edc9f3506..b36e9103196 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -62,6 +62,11 @@ class SensorBase(SensorEntity): self._attr_unique_id = f"{device.name}-{self.device_class}" self._attr_device_info = self._device_info.device_info + @property + def available(self) -> bool: + """Return the device availability.""" + return self._device_info.available + async def async_added_to_hass(self) -> None: """Run when entity is added to register update signal handler.""" self.async_on_remove( diff --git a/tests/components/nest/test_climate_sdm.py b/tests/components/nest/test_climate_sdm.py index 440855f6ab7..4ac58171fcd 100644 --- a/tests/components/nest/test_climate_sdm.py +++ b/tests/components/nest/test_climate_sdm.py @@ -34,7 +34,11 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) -from homeassistant.const import ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE +from homeassistant.const import ( + ATTR_SUPPORTED_FEATURES, + ATTR_TEMPERATURE, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -1442,3 +1446,63 @@ async def test_thermostat_hvac_mode_failure( with pytest.raises(HomeAssistantError): await common.async_set_preset_mode(hass, PRESET_ECO) await hass.async_block_till_done() + + +async def test_thermostat_available( + hass: HomeAssistant, setup_platform: PlatformSetup, create_device: CreateDevice +): + """Test a thermostat that is available.""" + create_device.create( + { + "sdm.devices.traits.ThermostatHvac": { + "status": "COOLING", + }, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "COOL", + }, + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 29.9, + }, + "sdm.devices.traits.ThermostatTemperatureSetpoint": { + "coolCelsius": 28.0, + }, + "sdm.devices.traits.Connectivity": {"status": "ONLINE"}, + }, + ) + await setup_platform() + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVACMode.COOL + + +async def test_thermostat_unavailable( + hass: HomeAssistant, setup_platform: PlatformSetup, create_device: CreateDevice +): + """Test a thermostat that is unavailable.""" + create_device.create( + { + "sdm.devices.traits.ThermostatHvac": { + "status": "COOLING", + }, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "COOL", + }, + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 29.9, + }, + "sdm.devices.traits.ThermostatTemperatureSetpoint": { + "coolCelsius": 28.0, + }, + "sdm.devices.traits.Connectivity": {"status": "OFFLINE"}, + }, + ) + await setup_platform() + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == STATE_UNAVAILABLE diff --git a/tests/components/nest/test_sensor_sdm.py b/tests/components/nest/test_sensor_sdm.py index d1a89317959..c3698cf4123 100644 --- a/tests/components/nest/test_sensor_sdm.py +++ b/tests/components/nest/test_sensor_sdm.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, + STATE_UNAVAILABLE, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -90,6 +91,58 @@ async def test_thermostat_device( assert device.identifiers == {("nest", DEVICE_ID)} +async def test_thermostat_device_available( + hass: HomeAssistant, create_device: CreateDevice, setup_platform: PlatformSetup +): + """Test a thermostat with temperature and humidity sensors that is Online.""" + create_device.create( + { + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 25.1, + }, + "sdm.devices.traits.Humidity": { + "ambientHumidityPercent": 35.0, + }, + "sdm.devices.traits.Connectivity": {"status": "ONLINE"}, + } + ) + await setup_platform() + + temperature = hass.states.get("sensor.my_sensor_temperature") + assert temperature is not None + assert temperature.state == "25.1" + + humidity = hass.states.get("sensor.my_sensor_humidity") + assert humidity is not None + assert humidity.state == "35" + + +async def test_thermostat_device_unavailable( + hass: HomeAssistant, create_device: CreateDevice, setup_platform: PlatformSetup +): + """Test a thermostat with temperature and humidity sensors that is Offline.""" + create_device.create( + { + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 25.1, + }, + "sdm.devices.traits.Humidity": { + "ambientHumidityPercent": 35.0, + }, + "sdm.devices.traits.Connectivity": {"status": "OFFLINE"}, + } + ) + await setup_platform() + + temperature = hass.states.get("sensor.my_sensor_temperature") + assert temperature is not None + assert temperature.state == STATE_UNAVAILABLE + + humidity = hass.states.get("sensor.my_sensor_humidity") + assert humidity is not None + assert humidity.state == STATE_UNAVAILABLE + + async def test_no_devices(hass: HomeAssistant, setup_platform: PlatformSetup): """Test no devices returned by the api.""" await setup_platform() From 79713d637a11cd02572b9cec940925283f04f274 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 28 Sep 2022 19:46:27 -0700 Subject: [PATCH 016/985] Use Owntone name for forked-daapd (#79214) Co-authored-by: Paulus Schoutsen --- homeassistant/components/forked_daapd/const.py | 2 +- .../components/forked_daapd/manifest.json | 2 +- homeassistant/components/forked_daapd/strings.json | 14 +++++++------- homeassistant/generated/integrations.json | 2 +- tests/components/forked_daapd/test_browse_media.py | 2 +- tests/components/forked_daapd/test_media_player.py | 9 ++++----- 6 files changed, 15 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/forked_daapd/const.py b/homeassistant/components/forked_daapd/const.py index 60e31bc707d..f74fe0b049d 100644 --- a/homeassistant/components/forked_daapd/const.py +++ b/homeassistant/components/forked_daapd/const.py @@ -30,7 +30,7 @@ DEFAULT_TTS_PAUSE_TIME = 1.2 DEFAULT_TTS_VOLUME = 0.8 DEFAULT_UNMUTE_VOLUME = 0.6 DOMAIN = "forked_daapd" # key for hass.data -FD_NAME = "forked-daapd" +FD_NAME = "Owntone" HASS_DATA_REMOVE_LISTENERS_KEY = "REMOVE_LISTENERS" HASS_DATA_UPDATER_KEY = "UPDATER" KNOWN_PIPES = {"librespot-java"} diff --git a/homeassistant/components/forked_daapd/manifest.json b/homeassistant/components/forked_daapd/manifest.json index 14d2132a165..8cd2822156f 100644 --- a/homeassistant/components/forked_daapd/manifest.json +++ b/homeassistant/components/forked_daapd/manifest.json @@ -1,6 +1,6 @@ { "domain": "forked_daapd", - "name": "forked-daapd", + "name": "Owntone", "documentation": "https://www.home-assistant.io/integrations/forked_daapd", "codeowners": ["@uvjustin"], "requirements": ["pyforked-daapd==0.1.11", "pylibrespot-java==0.1.0"], diff --git a/homeassistant/components/forked_daapd/strings.json b/homeassistant/components/forked_daapd/strings.json index 671538210ff..76a03abeb4b 100644 --- a/homeassistant/components/forked_daapd/strings.json +++ b/homeassistant/components/forked_daapd/strings.json @@ -3,7 +3,7 @@ "flow_title": "{name} ({host})", "step": { "user": { - "title": "Set up forked-daapd device", + "title": "Set up Owntone device", "data": { "name": "Friendly name", "host": "[%key:common::config_flow::data::host%]", @@ -13,23 +13,23 @@ } }, "error": { - "forbidden": "Unable to connect. Please check your forked-daapd network permissions.", - "websocket_not_enabled": "forked-daapd server websocket not enabled.", + "forbidden": "Unable to connect. Please check your Owntone network permissions.", + "websocket_not_enabled": "Owntone server websocket not enabled.", "wrong_host_or_port": "Unable to connect. Please check host and port.", "wrong_password": "Incorrect password.", - "wrong_server_type": "The forked-daapd integration requires a forked-daapd server with version >= 27.0.", + "wrong_server_type": "The Owntone integration requires an Owntone server with version >= 27.0.", "unknown_error": "[%key:common::config_flow::error::unknown%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "not_forked_daapd": "Device is not a forked-daapd server." + "not_forked_daapd": "Device is not an Owntone server." } }, "options": { "step": { "init": { - "title": "Configure forked-daapd options", - "description": "Set various options for the forked-daapd integration.", + "title": "Configure Owntone options", + "description": "Set various options for the Owntone integration.", "data": { "librespot_java_port": "Port for librespot-java pipe control (if used)", "max_playlists": "Max number of playlists used as sources", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 3e056276c16..8943f00be34 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1411,7 +1411,7 @@ "forked_daapd": { "config_flow": true, "iot_class": "local_push", - "name": "forked-daapd" + "name": "Owntone" }, "fortios": { "config_flow": false, diff --git a/tests/components/forked_daapd/test_browse_media.py b/tests/components/forked_daapd/test_browse_media.py index bedb541b803..23fc9fcf6eb 100644 --- a/tests/components/forked_daapd/test_browse_media.py +++ b/tests/components/forked_daapd/test_browse_media.py @@ -12,7 +12,7 @@ from homeassistant.components.spotify.const import ( from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.setup import async_setup_component -TEST_MASTER_ENTITY_NAME = "media_player.forked_daapd_server" +TEST_MASTER_ENTITY_NAME = "media_player.owntone_server" async def test_async_browse_media(hass, hass_ws_client, config_entry): diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index 589f176db14..ae5e29bee47 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -66,10 +66,9 @@ from homeassistant.const import ( from tests.common import async_mock_signal -TEST_MASTER_ENTITY_NAME = "media_player.forked_daapd_server" +TEST_MASTER_ENTITY_NAME = "media_player.owntone_server" TEST_ZONE_ENTITY_NAMES = [ - "media_player.forked_daapd_output_" + x - for x in ("kitchen", "computer", "daapd_fifo") + "media_player.owntone_output_" + x for x in ("kitchen", "computer", "daapd_fifo") ] OPTIONS_DATA = { @@ -354,7 +353,7 @@ def test_master_state(hass, mock_api_object): """Test master state attributes.""" state = hass.states.get(TEST_MASTER_ENTITY_NAME) assert state.state == STATE_PAUSED - assert state.attributes[ATTR_FRIENDLY_NAME] == "forked-daapd server" + assert state.attributes[ATTR_FRIENDLY_NAME] == "Owntone server" assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORTED_FEATURES assert not state.attributes[ATTR_MEDIA_VOLUME_MUTED] assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.2 @@ -413,7 +412,7 @@ async def test_zone(hass, mock_api_object): """Test zone attributes and methods.""" zone_entity_name = TEST_ZONE_ENTITY_NAMES[0] state = hass.states.get(zone_entity_name) - assert state.attributes[ATTR_FRIENDLY_NAME] == "forked-daapd output (kitchen)" + assert state.attributes[ATTR_FRIENDLY_NAME] == "Owntone output (kitchen)" assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORTED_FEATURES_ZONE assert state.state == STATE_ON assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.5 From 21693b0022be231ca563906d90490310edec1f7f Mon Sep 17 00:00:00 2001 From: Kevin Addeman Date: Thu, 29 Sep 2022 01:51:45 -0400 Subject: [PATCH 017/985] Bump pylutron_caseta to 0.16.0 (#79243) --- homeassistant/components/lutron_caseta/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index 88849391e24..ad933dc0a69 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -2,7 +2,7 @@ "domain": "lutron_caseta", "name": "Lutron Cas\u00e9ta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", - "requirements": ["pylutron-caseta==0.15.2"], + "requirements": ["pylutron-caseta==0.16.0"], "config_flow": true, "zeroconf": ["_leap._tcp.local."], "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index e02de390571..498c02ba14b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1685,7 +1685,7 @@ pylitejet==0.3.0 pylitterbot==2022.9.6 # homeassistant.components.lutron_caseta -pylutron-caseta==0.15.2 +pylutron-caseta==0.16.0 # homeassistant.components.lutron pylutron==0.2.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75ff02697e9..44c99d4bb3d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1186,7 +1186,7 @@ pylitejet==0.3.0 pylitterbot==2022.9.6 # homeassistant.components.lutron_caseta -pylutron-caseta==0.15.2 +pylutron-caseta==0.16.0 # homeassistant.components.mailgun pymailgunner==1.4 From 0fa0ab855b75311de7f1e609bfe44a49c233b845 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 29 Sep 2022 09:19:20 +0200 Subject: [PATCH 018/985] Use SensorDeviceClass.SPEED in metoffice (#79263) --- homeassistant/components/metoffice/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index dd8ceefad23..c553904a895 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -83,15 +83,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="wind_speed", name="Wind speed", - device_class=None, native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", entity_registry_enabled_default=True, ), SensorEntityDescription( key="wind_direction", name="Wind direction", - device_class=None, native_unit_of_measurement=None, icon="mdi:compass-outline", entity_registry_enabled_default=False, @@ -99,8 +98,8 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="wind_gust", name="Wind gust", - device_class=None, native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", entity_registry_enabled_default=False, ), From f9d36fe493f3ac6a59f25866c1614788b1e4a122 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 29 Sep 2022 09:20:13 +0200 Subject: [PATCH 019/985] Use SensorDeviceClass.SPEED in rfxtrx (#79261) Use SensorDeviceClass.VOLUME in rfxtrx --- homeassistant/components/rfxtrx/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index b4d4d65295c..563b166e0aa 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -206,12 +206,14 @@ SENSOR_TYPES = ( name="Wind average speed", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SPEED_METERS_PER_SECOND, + device_class=SensorDeviceClass.SPEED, ), RfxtrxSensorEntityDescription( key="Wind gust", name="Wind gust", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SPEED_METERS_PER_SECOND, + device_class=SensorDeviceClass.SPEED, ), RfxtrxSensorEntityDescription( key="Rain total", From a0357767ef0b1349064ee9564e64554af5519528 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 29 Sep 2022 09:33:31 +0200 Subject: [PATCH 020/985] Fix late comments to deCONZ sensors from #79137 (#79272) * Fix late comments from #79137 * Fix comment --- homeassistant/components/deconz/sensor.py | 67 +++++++++++------------ 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 1b24098f717..90e19aee11d 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -90,18 +90,15 @@ T = TypeVar( class DeconzSensorDescriptionMixin(Generic[T]): """Required values when describing secondary sensor attributes.""" - isinstance_fn: Callable[[T], bool] update_key: str value_fn: Callable[[T], datetime | StateType] @dataclass -class DeconzSensorDescription( - SensorEntityDescription, DeconzSensorDescriptionMixin[T], Generic[T] -): +class DeconzSensorDescription(SensorEntityDescription, DeconzSensorDescriptionMixin[T]): """Class describing deCONZ binary sensor entities.""" - common: bool = False + instance_check: type[T] | None = None name_suffix: str = "" old_unique_id_suffix: str = "" @@ -109,16 +106,16 @@ class DeconzSensorDescription( ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( DeconzSensorDescription[AirQuality]( key="air_quality", - isinstance_fn=lambda device: isinstance(device, AirQuality), - value_fn=lambda device: device.air_quality, update_key="airquality", + value_fn=lambda device: device.air_quality, + instance_check=AirQuality, state_class=SensorStateClass.MEASUREMENT, ), DeconzSensorDescription[AirQuality]( key="air_quality_ppb", - isinstance_fn=lambda device: isinstance(device, AirQuality), - value_fn=lambda device: device.air_quality_ppb, update_key="airqualityppb", + value_fn=lambda device: device.air_quality_ppb, + instance_check=AirQuality, name_suffix="PPB", old_unique_id_suffix="ppb", device_class=SensorDeviceClass.AQI, @@ -127,86 +124,84 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[Consumption]( key="consumption", - isinstance_fn=lambda device: isinstance(device, Consumption), - value_fn=lambda device: device.scaled_consumption, update_key="consumption", + value_fn=lambda device: device.scaled_consumption, + instance_check=Consumption, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), DeconzSensorDescription[Daylight]( key="daylight_status", - isinstance_fn=lambda device: isinstance(device, Daylight), - value_fn=lambda device: DAYLIGHT_STATUS[device.daylight_status], update_key="status", + value_fn=lambda device: DAYLIGHT_STATUS[device.daylight_status], + instance_check=Daylight, icon="mdi:white-balance-sunny", entity_registry_enabled_default=False, ), DeconzSensorDescription[GenericStatus]( key="status", - isinstance_fn=lambda device: isinstance(device, GenericStatus), - value_fn=lambda device: device.status, update_key="status", + value_fn=lambda device: device.status, + instance_check=GenericStatus, ), DeconzSensorDescription[Humidity]( key="humidity", - isinstance_fn=lambda device: isinstance(device, Humidity), - value_fn=lambda device: device.scaled_humidity, update_key="humidity", + value_fn=lambda device: device.scaled_humidity, + instance_check=Humidity, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, ), DeconzSensorDescription[LightLevel]( key="light_level", - isinstance_fn=lambda device: isinstance(device, LightLevel), - value_fn=lambda device: device.scaled_light_level, update_key="lightlevel", + value_fn=lambda device: device.scaled_light_level, + instance_check=LightLevel, device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=LIGHT_LUX, ), DeconzSensorDescription[Power]( key="power", - isinstance_fn=lambda device: isinstance(device, Power), - value_fn=lambda device: device.power, update_key="power", + value_fn=lambda device: device.power, + instance_check=Power, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), DeconzSensorDescription[Pressure]( key="pressure", - isinstance_fn=lambda device: isinstance(device, Pressure), - value_fn=lambda device: device.pressure, update_key="pressure", + value_fn=lambda device: device.pressure, + instance_check=Pressure, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PRESSURE_HPA, ), DeconzSensorDescription[Temperature]( key="temperature", - isinstance_fn=lambda device: isinstance(device, Temperature), - value_fn=lambda device: device.scaled_temperature, update_key="temperature", + value_fn=lambda device: device.scaled_temperature, + instance_check=Temperature, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), DeconzSensorDescription[Time]( key="last_set", - isinstance_fn=lambda device: isinstance(device, Time), - value_fn=lambda device: dt_util.parse_datetime(device.last_set), update_key="lastset", + value_fn=lambda device: dt_util.parse_datetime(device.last_set), + instance_check=Time, device_class=SensorDeviceClass.TIMESTAMP, state_class=SensorStateClass.TOTAL_INCREASING, ), DeconzSensorDescription[SensorResources]( key="battery", - isinstance_fn=lambda device: isinstance(device, PydeconzSensorBase), - value_fn=lambda device: device.battery, update_key="battery", - common=True, + value_fn=lambda device: device.battery, name_suffix="Battery", old_unique_id_suffix="battery", device_class=SensorDeviceClass.BATTERY, @@ -216,10 +211,8 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[SensorResources]( key="internal_temperature", - isinstance_fn=lambda device: isinstance(device, PydeconzSensorBase), - value_fn=lambda device: device.internal_temperature, update_key="temperature", - common=True, + value_fn=lambda device: device.internal_temperature, name_suffix="Temperature", old_unique_id_suffix="temperature", device_class=SensorDeviceClass.TEMPERATURE, @@ -262,7 +255,7 @@ async def async_setup_entry( known_device_entities: dict[str, set[str]] = { description.key: set() for description in ENTITY_DESCRIPTIONS - if description.common + if description.instance_check is None } @callback @@ -272,14 +265,16 @@ async def async_setup_entry( entities: list[DeconzSensor] = [] for description in ENTITY_DESCRIPTIONS: - if not description.isinstance_fn(sensor): + if description.instance_check and not isinstance( + sensor, description.instance_check + ): continue no_sensor_data = False if description.value_fn(sensor) is None: no_sensor_data = True - if description.common: + if description.instance_check is None: if ( sensor.type.startswith("CLIP") or (no_sensor_data and description.key != "battery") From 0e764b57c2dd50b7e0d994fcaaf4e79481add09f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 29 Sep 2022 09:37:21 +0200 Subject: [PATCH 021/985] Use SensorDeviceClass.SPEED in components (#79262) --- homeassistant/components/ambient_station/sensor.py | 5 +++++ homeassistant/components/buienradar/sensor.py | 7 +++++++ homeassistant/components/environment_canada/sensor.py | 2 ++ homeassistant/components/homematic/sensor.py | 1 + homeassistant/components/homematicip_cloud/sensor.py | 2 ++ homeassistant/components/lacrosse_view/sensor.py | 1 + homeassistant/components/netatmo/sensor.py | 2 ++ homeassistant/components/tellduslive/sensor.py | 2 ++ .../components/trafikverket_weatherstation/sensor.py | 2 ++ 9 files changed, 24 insertions(+) diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index a04c279915f..65c726bfff3 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -308,6 +308,7 @@ SENSOR_DESCRIPTIONS = ( name="Max gust", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( @@ -631,6 +632,7 @@ SENSOR_DESCRIPTIONS = ( name="Wind gust", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( @@ -638,18 +640,21 @@ SENSOR_DESCRIPTIONS = ( name="Wind avg 10m", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, ), SensorEntityDescription( key=TYPE_WINDSPDMPH_AVG2M, name="Wind avg 2m", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, ), SensorEntityDescription( key=TYPE_WINDSPEEDMPH, name="Wind speed", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 2f3e60b0646..08303120a92 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -138,6 +138,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="windspeed", name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", state_class=SensorStateClass.MEASUREMENT, ), @@ -175,6 +176,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="windgust", name="Wind gust", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), SensorEntityDescription( @@ -463,30 +465,35 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="windspeed_1d", name="Wind speed 1d", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), SensorEntityDescription( key="windspeed_2d", name="Wind speed 2d", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), SensorEntityDescription( key="windspeed_3d", name="Wind speed 3d", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), SensorEntityDescription( key="windspeed_4d", name="Wind speed 4d", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), SensorEntityDescription( key="windspeed_5d", name="Wind speed 5d", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), SensorEntityDescription( diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 08da60fe01f..5f22b251493 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -198,6 +198,7 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( key="wind_gust", name="Wind gust", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("wind_gust", {}).get("value"), ), @@ -205,6 +206,7 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( key="wind_speed", name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("wind_speed", {}).get("value"), ), diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 456a10b7630..c7a78c7bbcf 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -170,6 +170,7 @@ SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = { "WIND_SPEED": SensorEntityDescription( key="WIND_SPEED", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), "WIND_DIRECTION": SensorEntityDescription( diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 57a8b7bd714..bb9dd8021ed 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -344,6 +344,8 @@ class HomematicipEnergySensor(HomematicipGenericEntity, SensorEntity): class HomematicipWindspeedSensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP wind speed sensor.""" + _attr_device_class = SensorDeviceClass.SPEED + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the windspeed sensor.""" super().__init__(hap, device, post="Windspeed") diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py index 0f60ef4ca10..1ff3e78812f 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -81,6 +81,7 @@ SENSOR_DESCRIPTIONS = { state_class=SensorStateClass.MEASUREMENT, value_fn=get_value, native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, ), "Rain": LaCrosseSensorEntityDescription( key="Rain", diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index ff555ecd472..65ac610ef5d 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -200,6 +200,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="wind_strength", entity_registry_enabled_default=True, native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", state_class=SensorStateClass.MEASUREMENT, ), @@ -225,6 +226,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="gust_strength", entity_registry_enabled_default=False, native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", state_class=SensorStateClass.MEASUREMENT, ), diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py index 8d02763d428..e2995620fb1 100644 --- a/homeassistant/components/tellduslive/sensor.py +++ b/homeassistant/components/tellduslive/sensor.py @@ -76,12 +76,14 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { key=SENSOR_TYPE_WINDAVERAGE, name="Wind average", native_unit_of_measurement=SPEED_METERS_PER_SECOND, + device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_WINDGUST: SensorEntityDescription( key=SENSOR_TYPE_WINDGUST, name="Wind gust", native_unit_of_measurement=SPEED_METERS_PER_SECOND, + device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_UV: SensorEntityDescription( diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 68c47e9320c..4053ce7cbc4 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -89,6 +89,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( api_key="windforce", name="Wind speed", native_unit_of_measurement=SPEED_METERS_PER_SECOND, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", state_class=SensorStateClass.MEASUREMENT, ), @@ -97,6 +98,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( api_key="windforcemax", name="Wind speed max", native_unit_of_measurement=SPEED_METERS_PER_SECOND, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy-variant", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, From fab3ee90b2dbcc553297716515c95ab7331fcee7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 29 Sep 2022 09:38:06 +0200 Subject: [PATCH 022/985] Use SensorDeviceClass.VOLUME in components (#79253) --- homeassistant/components/flo/sensor.py | 1 + homeassistant/components/justnimbus/sensor.py | 5 +++++ homeassistant/components/kegtron/sensor.py | 3 +++ homeassistant/components/overkiz/sensor.py | 3 +++ homeassistant/components/p1_monitor/sensor.py | 1 + homeassistant/components/streamlabswater/sensor.py | 4 +++- homeassistant/components/suez_water/sensor.py | 7 ++++++- homeassistant/components/surepetcare/sensor.py | 1 + 8 files changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/flo/sensor.py b/homeassistant/components/flo/sensor.py index e7fbd293bd1..9f793b749e4 100644 --- a/homeassistant/components/flo/sensor.py +++ b/homeassistant/components/flo/sensor.py @@ -67,6 +67,7 @@ async def async_setup_entry( class FloDailyUsageSensor(FloEntity, SensorEntity): """Monitors the daily water usage.""" + _attr_device_class = SensorDeviceClass.VOLUME _attr_icon = WATER_ICON _attr_native_unit_of_measurement = VOLUME_GALLONS _attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING diff --git a/homeassistant/components/justnimbus/sensor.py b/homeassistant/components/justnimbus/sensor.py index 73a68ac9139..41f1e81d5b3 100644 --- a/homeassistant/components/justnimbus/sensor.py +++ b/homeassistant/components/justnimbus/sensor.py @@ -103,6 +103,7 @@ SENSOR_TYPES = ( name="Reservoir content", icon="mdi:car-coolant-level", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.reservoir_content, @@ -112,6 +113,7 @@ SENSOR_TYPES = ( name="Total saved", icon="mdi:water-opacity", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.total_saved, @@ -121,6 +123,7 @@ SENSOR_TYPES = ( name="Total replenished", icon="mdi:water", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.total_replenished, @@ -138,6 +141,7 @@ SENSOR_TYPES = ( name="Total use", icon="mdi:chart-donut", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.totver, @@ -147,6 +151,7 @@ SENSOR_TYPES = ( name="Max reservoir content", icon="mdi:waves", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.reservoir_content_max, diff --git a/homeassistant/components/kegtron/sensor.py b/homeassistant/components/kegtron/sensor.py index f52a7c79634..892d8651185 100644 --- a/homeassistant/components/kegtron/sensor.py +++ b/homeassistant/components/kegtron/sensor.py @@ -39,6 +39,7 @@ SENSOR_DESCRIPTIONS = { key=KegtronSensorDeviceClass.KEG_SIZE, icon="mdi:keg", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.MEASUREMENT, ), KegtronSensorDeviceClass.KEG_TYPE: SensorEntityDescription( @@ -49,12 +50,14 @@ SENSOR_DESCRIPTIONS = { key=KegtronSensorDeviceClass.VOLUME_START, icon="mdi:keg", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.MEASUREMENT, ), KegtronSensorDeviceClass.VOLUME_DISPENSED: SensorEntityDescription( key=KegtronSensorDeviceClass.VOLUME_DISPENSED, icon="mdi:keg", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL, ), KegtronSensorDeviceClass.PORT_STATE: SensorEntityDescription( diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index 83f123eaad7..e80e08e263a 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -90,6 +90,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ name="Water volume estimation at 40 °C", icon="mdi:water", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), @@ -98,6 +99,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ name="Water consumption", icon="mdi:water", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.MEASUREMENT, ), OverkizSensorDescription( @@ -105,6 +107,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ name="Outlet engine", icon="mdi:fan-chevron-down", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.MEASUREMENT, ), OverkizSensorDescription( diff --git a/homeassistant/components/p1_monitor/sensor.py b/homeassistant/components/p1_monitor/sensor.py index 757bf249ca1..e55c8dacea5 100644 --- a/homeassistant/components/p1_monitor/sensor.py +++ b/homeassistant/components/p1_monitor/sensor.py @@ -222,6 +222,7 @@ SENSORS_WATERMETER: tuple[SensorEntityDescription, ...] = ( name="Consumption Day", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, ), SensorEntityDescription( key="consumption_total", diff --git a/homeassistant/components/streamlabswater/sensor.py b/homeassistant/components/streamlabswater/sensor.py index afef8070fcb..1a1070d1ea8 100644 --- a/homeassistant/components/streamlabswater/sensor.py +++ b/homeassistant/components/streamlabswater/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import VOLUME_GALLONS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -80,6 +80,8 @@ class StreamlabsUsageData: class StreamLabsDailyUsage(SensorEntity): """Monitors the daily water usage.""" + _attr_device_class = SensorDeviceClass.VOLUME + def __init__(self, location_name, streamlabs_usage_data): """Initialize the daily water usage device.""" self._location_name = location_name diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index ac691829236..77b1de6555e 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -8,7 +8,11 @@ from pysuez import SuezClient from pysuez.client import PySuezError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, +) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, VOLUME_LITERS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -63,6 +67,7 @@ class SuezSensor(SensorEntity): _attr_name = NAME _attr_icon = ICON _attr_native_unit_of_measurement = VOLUME_LITERS + _attr_device_class = SensorDeviceClass.VOLUME def __init__(self, client): """Initialize the data object.""" diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index e9967054900..1ae22710060 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -87,6 +87,7 @@ class SureBattery(SurePetcareEntity, SensorEntity): class Felaqua(SurePetcareEntity, SensorEntity): """Sure Petcare Felaqua.""" + _attr_device_class = SensorDeviceClass.VOLUME _attr_native_unit_of_measurement = VOLUME_MILLILITERS def __init__( From c527defe3184b164acd014145566103463e00e95 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 29 Sep 2022 11:28:59 +0200 Subject: [PATCH 023/985] Use SensorDeviceClass.WEIGHT in components (#79277) --- homeassistant/components/bthome/sensor.py | 4 ++-- homeassistant/components/litterrobot/sensor.py | 1 + homeassistant/components/mysensors/sensor.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index d80763d4600..9d68ce2d3b4 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -145,14 +145,14 @@ SENSOR_DESCRIPTIONS = { # Used for mass sensor with kg unit (BTHomeSensorDeviceClass.MASS, Units.MASS_KILOGRAMS): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.MASS}_{Units.MASS_KILOGRAMS}", - device_class=None, + device_class=SensorDeviceClass.WEIGHT, native_unit_of_measurement=MASS_KILOGRAMS, state_class=SensorStateClass.MEASUREMENT, ), # Used for mass sensor with lb unit (BTHomeSensorDeviceClass.MASS, Units.MASS_POUNDS): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.MASS}_{Units.MASS_POUNDS}", - device_class=None, + device_class=SensorDeviceClass.WEIGHT, native_unit_of_measurement=MASS_POUNDS, state_class=SensorStateClass.MEASUREMENT, ), diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 1a8f066f54b..b9d70528cab 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -111,6 +111,7 @@ ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = { name="Pet weight", icon="mdi:scale", native_unit_of_measurement=MASS_POUNDS, + device_class=SensorDeviceClass.WEIGHT, ), ], FeederRobot: [ diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 6f940c5d625..dd10c203228 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -94,6 +94,7 @@ SENSORS: dict[str, SensorEntityDescription] = { "V_WEIGHT": SensorEntityDescription( key="V_WEIGHT", native_unit_of_measurement=MASS_KILOGRAMS, + device_class=SensorDeviceClass.WEIGHT, icon="mdi:weight-kilogram", ), "V_DISTANCE": SensorEntityDescription( From 4bd686bdb19471f09d662d917c56c6b79662fc54 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 29 Sep 2022 12:19:34 +0200 Subject: [PATCH 024/985] Use SensorDeviceClass.DISTANCE in components (#79285) * Use SensorDeviceClass.DISTANCE in components * Adjust mysensors --- homeassistant/components/buienradar/sensor.py | 1 + homeassistant/components/environment_canada/sensor.py | 1 + homeassistant/components/metoffice/sensor.py | 2 +- homeassistant/components/mysensors/sensor.py | 1 + homeassistant/components/opengarage/sensor.py | 1 + homeassistant/components/starline/sensor.py | 1 + homeassistant/components/wallbox/sensor.py | 1 + 7 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 08303120a92..279fdc145d5 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -170,6 +170,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="visibility", name="Visibility", native_unit_of_measurement=LENGTH_KILOMETERS, + device_class=SensorDeviceClass.DISTANCE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 5f22b251493..88ec055ad03 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -172,6 +172,7 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( key="visibility", name="Visibility", native_unit_of_measurement=LENGTH_KILOMETERS, + device_class=SensorDeviceClass.DISTANCE, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("visibility", {}).get("value"), ), diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index c553904a895..77532b379b6 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -114,8 +114,8 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="visibility_distance", name="Visibility distance", - device_class=None, native_unit_of_measurement=LENGTH_KILOMETERS, + device_class=SensorDeviceClass.DISTANCE, icon="mdi:eye", entity_registry_enabled_default=False, ), diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index dd10c203228..59c33a48884 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -100,6 +100,7 @@ SENSORS: dict[str, SensorEntityDescription] = { "V_DISTANCE": SensorEntityDescription( key="V_DISTANCE", native_unit_of_measurement=LENGTH_METERS, + device_class=SensorDeviceClass.DISTANCE, icon="mdi:ruler", ), "V_IMPEDANCE": SensorEntityDescription( diff --git a/homeassistant/components/opengarage/sensor.py b/homeassistant/components/opengarage/sensor.py index d09ed130152..bf75cd34998 100644 --- a/homeassistant/components/opengarage/sensor.py +++ b/homeassistant/components/opengarage/sensor.py @@ -29,6 +29,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="dist", native_unit_of_measurement=LENGTH_CENTIMETERS, + device_class=SensorDeviceClass.DISTANCE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index d4ea2d02555..588b9f93e08 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -81,6 +81,7 @@ SENSOR_TYPES: tuple[StarlineSensorEntityDescription, ...] = ( key="mileage", name_="Mileage", native_unit_of_measurement=LENGTH_KILOMETERS, + device_class=SensorDeviceClass.DISTANCE, icon="mdi:counter", ), ) diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index 2c4a8c67bed..1fae68da2e4 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -85,6 +85,7 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { name="Added Range", precision=0, native_unit_of_measurement=LENGTH_KILOMETERS, + device_class=SensorDeviceClass.DISTANCE, state_class=SensorStateClass.TOTAL_INCREASING, ), CHARGER_ADDED_ENERGY_KEY: WallboxSensorEntityDescription( From a1c26cd4cd65ab834820c4b786a7e27275779609 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Sep 2022 06:28:51 -0400 Subject: [PATCH 025/985] Add Leviton brand (#79244) --- .prettierignore | 1 + homeassistant/brands/leviton.json | 5 +++++ homeassistant/components/zwave_js/manifest.json | 5 +---- homeassistant/generated/integrations.json | 6 ++++++ homeassistant/generated/supported_brands.py | 1 - 5 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 homeassistant/brands/leviton.json diff --git a/.prettierignore b/.prettierignore index 950741ec8b2..a4d1d99079d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,3 +3,4 @@ azure-*.yml docs/source/_templates/* homeassistant/components/*/translations/*.json +homeassistant/generated/* diff --git a/homeassistant/brands/leviton.json b/homeassistant/brands/leviton.json new file mode 100644 index 00000000000..b6d78586c1b --- /dev/null +++ b/homeassistant/brands/leviton.json @@ -0,0 +1,5 @@ +{ + "domain": "leviton", + "name": "Leviton", + "iot_standards": ["zwave"] +} diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 9880d5bb5d1..8f0c93f6c3e 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -21,8 +21,5 @@ } ], "zeroconf": ["_zwave-js-server._tcp.local."], - "loggers": ["zwave_js_server"], - "supported_brands": { - "leviton_z_wave": "Leviton Z-Wave" - } + "loggers": ["zwave_js_server"] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 8943f00be34..cf7ed646daa 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2267,6 +2267,12 @@ "iot_class": "local_polling", "name": "LED BLE" }, + "leviton": { + "name": "Leviton", + "iot_standards": [ + "zwave" + ] + }, "lg_netcast": { "config_flow": false, "iot_class": "local_polling", diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py index 39f7c6bea05..50490d2c847 100644 --- a/homeassistant/generated/supported_brands.py +++ b/homeassistant/generated/supported_brands.py @@ -13,5 +13,4 @@ HAS_SUPPORTED_BRANDS = [ "thermobeacon", "wemo", "yalexs_ble", - "zwave_js", ] From d0ac1073a0f48f4d1c0dac62a8a8c1ddf1078a38 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 29 Sep 2022 13:40:53 +0300 Subject: [PATCH 026/985] Allow entries with same user_key for Pushover (#77904) * Allow entries with same user_key for Pushover * remove unique_id completely * Abort reauth if entered api_key already exists Update tests --- homeassistant/components/pushover/__init__.py | 4 ++ .../components/pushover/config_flow.py | 14 ++++- tests/components/pushover/test_config_flow.py | 51 ++++++++++++++++--- tests/components/pushover/test_init.py | 10 ++++ 4 files changed, 71 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/pushover/__init__.py b/homeassistant/components/pushover/__init__.py index fa9a9c5ebd9..3c0c92db044 100644 --- a/homeassistant/components/pushover/__init__.py +++ b/homeassistant/components/pushover/__init__.py @@ -25,6 +25,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up pushover from a config entry.""" + # remove unique_id for beta users + if entry.unique_id is not None: + hass.config_entries.async_update_entry(entry, unique_id=None) + pushover_api = PushoverAPI(entry.data[CONF_API_KEY]) try: await hass.async_add_executor_job( diff --git a/homeassistant/components/pushover/config_flow.py b/homeassistant/components/pushover/config_flow.py index 3f12446733e..ddb61d4bbc3 100644 --- a/homeassistant/components/pushover/config_flow.py +++ b/homeassistant/components/pushover/config_flow.py @@ -62,6 +62,12 @@ class PushBulletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None and self._reauth_entry: user_input = {**self._reauth_entry.data, **user_input} + self._async_abort_entries_match( + { + CONF_USER_KEY: user_input[CONF_USER_KEY], + CONF_API_KEY: user_input[CONF_API_KEY], + } + ) errors = await validate_input(self.hass, user_input) if not errors: self.hass.config_entries.async_update_entry( @@ -87,9 +93,13 @@ class PushBulletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - await self.async_set_unique_id(user_input[CONF_USER_KEY]) - self._abort_if_unique_id_configured() + self._async_abort_entries_match( + { + CONF_USER_KEY: user_input[CONF_USER_KEY], + CONF_API_KEY: user_input[CONF_API_KEY], + } + ) self._async_abort_entries_match({CONF_NAME: user_input[CONF_NAME]}) errors = await validate_input(self.hass, user_input) diff --git a/tests/components/pushover/test_config_flow.py b/tests/components/pushover/test_config_flow.py index 21c0bc3ab1e..1e919167c6a 100644 --- a/tests/components/pushover/test_config_flow.py +++ b/tests/components/pushover/test_config_flow.py @@ -48,12 +48,11 @@ async def test_flow_user(hass: HomeAssistant) -> None: assert result["data"] == MOCK_CONFIG -async def test_flow_user_key_already_configured(hass: HomeAssistant) -> None: - """Test user initialized flow with duplicate user key.""" +async def test_flow_user_key_api_key_exists(hass: HomeAssistant) -> None: + """Test user initialized flow with duplicate user key / api key pair.""" entry = MockConfigEntry( domain=DOMAIN, data=MOCK_CONFIG, - unique_id="MYUSERKEY", ) entry.add_to_hass(hass) @@ -171,7 +170,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: data=MOCK_CONFIG, ) - assert result["type"] == "form" + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result2 = await hass.config_entries.flow.async_configure( @@ -181,7 +180,7 @@ async def test_reauth_success(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "abort" + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -213,7 +212,47 @@ async def test_reauth_failed(hass: HomeAssistant, mock_pushover: MagicMock) -> N }, ) - assert result2["type"] == "form" + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == { CONF_API_KEY: "invalid_api_key", } + + +async def test_reauth_with_existing_config(hass: HomeAssistant) -> None: + """Test reauth fails if the api key entered exists in another entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + + second_entry = MOCK_CONFIG.copy() + second_entry[CONF_API_KEY] = "MYAPIKEY2" + + entry2 = MockConfigEntry( + domain=DOMAIN, + data=second_entry, + ) + entry2.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_CONFIG, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "MYAPIKEY2", + }, + ) + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" diff --git a/tests/components/pushover/test_init.py b/tests/components/pushover/test_init.py index 4d1ee3cae19..7a8b02c93a0 100644 --- a/tests/components/pushover/test_init.py +++ b/tests/components/pushover/test_init.py @@ -68,6 +68,16 @@ async def test_async_setup_entry_success(hass: HomeAssistant) -> None: assert entry.state == ConfigEntryState.LOADED +async def test_unique_id_updated(hass: HomeAssistant) -> None: + """Test updating unique_id to new format.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, unique_id="MYUSERKEY") + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + assert entry.unique_id is None + + async def test_async_setup_entry_failed_invalid_api_key( hass: HomeAssistant, mock_pushover: MagicMock ) -> None: From e7764b8bf19027cbebf0e0e38178fa20037cdac1 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 29 Sep 2022 13:41:59 +0300 Subject: [PATCH 027/985] Add ConfigEntry template function (#78030) * Add ConfigEntry template function * Remove looking up entry_id by entry title --- homeassistant/helpers/template.py | 11 +++++++++++ tests/helpers/test_template.py | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 5a4d631a8c8..083d0e530aa 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1063,6 +1063,14 @@ def integration_entities(hass: HomeAssistant, entry_name: str) -> Iterable[str]: ] +def entry_id(hass: HomeAssistant, entity_id: str) -> str | None: + """Get an entry ID from an entity ID.""" + entity_reg = entity_registry.async_get(hass) + if entity := entity_reg.async_get(entity_id): + return entity.config_entry_id + return None + + def device_id(hass: HomeAssistant, entity_id_or_device_name: str) -> str | None: """Get a device ID from an entity ID or device name.""" entity_reg = entity_registry.async_get(hass) @@ -2072,6 +2080,9 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["device_attr"] = hassfunction(device_attr) self.globals["is_device_attr"] = hassfunction(is_device_attr) + self.globals["entry_id"] = hassfunction(entry_id) + self.filters["entry_id"] = pass_context(self.globals["entry_id"]) + self.globals["device_id"] = hassfunction(device_id) self.filters["device_id"] = pass_context(self.globals["device_id"]) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index fa9ec4e76d6..9c9a1e42a98 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2458,6 +2458,30 @@ async def test_integration_entities(hass): assert info.rate_limit is None +async def test_entry_id(hass): + """Test entry_id function.""" + config_entry = MockConfigEntry(domain="light", title="Some integration") + config_entry.add_to_hass(hass) + entity_registry = mock_registry(hass) + entity_entry = entity_registry.async_get_or_create( + "sensor", "test", "test", suggested_object_id="test", config_entry=config_entry + ) + + info = render_to_info(hass, "{{ 'sensor.fail' | entry_id }}") + assert_result_info(info, None) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 56 | entry_id }}") + assert_result_info(info, None) + + info = render_to_info(hass, "{{ 'not_a_real_entity_id' | entry_id }}") + assert_result_info(info, None) + + info = render_to_info(hass, f"{{{{ entry_id('{entity_entry.entity_id}') }}}}") + assert_result_info(info, config_entry.entry_id) + assert info.rate_limit is None + + async def test_device_id(hass): """Test device_id function.""" config_entry = MockConfigEntry(domain="light") From 616b85df31b205dcd606ab6ad4c4e2ded5ff6dae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Sep 2022 08:34:51 -0400 Subject: [PATCH 028/985] Add DialogFlow to Google brand (#79245) --- homeassistant/brands/google.json | 3 ++- homeassistant/generated/integrations.json | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/brands/google.json b/homeassistant/brands/google.json index 8f3340cef29..5f37de46180 100644 --- a/homeassistant/brands/google.json +++ b/homeassistant/brands/google.json @@ -14,6 +14,7 @@ "google", "nest", "cast", - "hangouts" + "hangouts", + "dialogflow" ] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index cf7ed646daa..75e0296f3c8 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -867,11 +867,6 @@ "config_flow": false, "iot_class": null }, - "dialogflow": { - "config_flow": true, - "iot_class": "cloud_push", - "name": "Dialogflow" - }, "digital_ocean": { "config_flow": false, "iot_class": "local_polling", @@ -1678,6 +1673,11 @@ "config_flow": true, "iot_class": "cloud_push", "name": "Google Chat" + }, + "dialogflow": { + "config_flow": true, + "iot_class": "cloud_push", + "name": "Dialogflow" } } }, From 0b5289f7483dde5911f4a268233fea2ce3b417ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Sep 2022 02:42:55 -1000 Subject: [PATCH 029/985] Wait for disconnect when we are out of connection ble slots in esphome (#79246) --- .../components/esphome/bluetooth/client.py | 42 ++++++++++++++++++- .../components/esphome/entry_data.py | 15 +++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 2eb722bdddf..8e8d7cf6427 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -8,6 +8,7 @@ from typing import Any, TypeVar, cast import uuid from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError +import async_timeout from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.client import BaseBleakClient, NotifyCallback from bleak.backends.device import BLEDevice @@ -24,6 +25,10 @@ from .service import BleakGATTServiceESPHome DEFAULT_MTU = 23 GATT_HEADER_SIZE = 3 +DISCONNECT_TIMEOUT = 5.0 +CONNECT_FREE_SLOT_TIMEOUT = 2.0 +GATT_READ_TIMEOUT = 30.0 + DEFAULT_MAX_WRITE_WITHOUT_RESPONSE = DEFAULT_MTU - GATT_HEADER_SIZE _LOGGER = logging.getLogger(__name__) @@ -37,6 +42,19 @@ def mac_to_int(address: str) -> int: return int(address.replace(":", ""), 16) +def verify_connected(func: _WrapFuncType) -> _WrapFuncType: + """Define a wrapper throw BleakError if not connected.""" + + async def _async_wrap_bluetooth_connected_operation( + self: "ESPHomeClient", *args: Any, **kwargs: Any + ) -> Any: + if not self._is_connected: # pylint: disable=protected-access + raise BleakError("Not connected") + return await func(self, *args, **kwargs) + + return cast(_WrapFuncType, _async_wrap_bluetooth_connected_operation) + + def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType: """Define a wrapper throw esphome api errors as BleakErrors.""" @@ -128,6 +146,7 @@ class ESPHomeClient(BaseBleakClient): Returns: Boolean representing connection status. """ + await self._wait_for_free_connection_slot(CONNECT_FREE_SLOT_TIMEOUT) connected_future: asyncio.Future[bool] = asyncio.Future() @@ -179,8 +198,20 @@ class ESPHomeClient(BaseBleakClient): """Disconnect from the peripheral device.""" self._unsubscribe_connection_state() await self._client.bluetooth_device_disconnect(self._address_as_int) + await self._wait_for_free_connection_slot(DISCONNECT_TIMEOUT) return True + async def _wait_for_free_connection_slot(self, timeout: float) -> None: + """Wait for a free connection slot.""" + entry_data = self._async_get_entry_data() + if entry_data.ble_connections_free: + return + _LOGGER.debug( + "%s: Out of connection slots, waiting for a free one", self._source + ) + async with async_timeout.timeout(timeout): + await entry_data.wait_for_ble_connections_free() + @property def is_connected(self) -> bool: """Is Connected.""" @@ -191,11 +222,13 @@ class ESPHomeClient(BaseBleakClient): """Get ATT MTU size for active connection.""" return self._mtu or DEFAULT_MTU + @verify_connected @api_error_as_bleak_error async def pair(self, *args: Any, **kwargs: Any) -> bool: """Attempt to pair.""" raise NotImplementedError("Pairing is not available in ESPHome.") + @verify_connected @api_error_as_bleak_error async def unpair(self) -> bool: """Attempt to unpair.""" @@ -272,6 +305,7 @@ class ESPHomeClient(BaseBleakClient): raise BleakError(f"Characteristic {char_specifier} was not found!") return characteristic + @verify_connected @api_error_as_bleak_error async def read_gatt_char( self, @@ -289,9 +323,10 @@ class ESPHomeClient(BaseBleakClient): """ characteristic = self._resolve_characteristic(char_specifier) return await self._client.bluetooth_gatt_read( - self._address_as_int, characteristic.handle + self._address_as_int, characteristic.handle, GATT_READ_TIMEOUT ) + @verify_connected @api_error_as_bleak_error async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray: """Perform read operation on the specified GATT descriptor. @@ -302,9 +337,10 @@ class ESPHomeClient(BaseBleakClient): (bytearray) The read data. """ return await self._client.bluetooth_gatt_read_descriptor( - self._address_as_int, handle + self._address_as_int, handle, GATT_READ_TIMEOUT ) + @verify_connected @api_error_as_bleak_error async def write_gatt_char( self, @@ -326,6 +362,7 @@ class ESPHomeClient(BaseBleakClient): self._address_as_int, characteristic.handle, bytes(data), response ) + @verify_connected @api_error_as_bleak_error async def write_gatt_descriptor( self, handle: int, data: bytes | bytearray | memoryview @@ -340,6 +377,7 @@ class ESPHomeClient(BaseBleakClient): self._address_as_int, handle, bytes(data) ) + @verify_connected @api_error_as_bleak_error async def start_notify( self, diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index d85e12845da..ac2a148d899 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -89,6 +89,9 @@ class RuntimeEntryData: _storage_contents: dict[str, Any] | None = None ble_connections_free: int = 0 ble_connections_limit: int = 0 + _ble_connection_free_futures: list[asyncio.Future[int]] = field( + default_factory=list + ) @callback def async_update_ble_connection_limits(self, free: int, limit: int) -> None: @@ -97,6 +100,18 @@ class RuntimeEntryData: _LOGGER.debug("%s: BLE connection limits: %s/%s", name, free, limit) self.ble_connections_free = free self.ble_connections_limit = limit + if free: + for fut in self._ble_connection_free_futures: + fut.set_result(free) + self._ble_connection_free_futures.clear() + + async def wait_for_ble_connections_free(self) -> int: + """Wait until there are free BLE connections.""" + if self.ble_connections_free > 0: + return self.ble_connections_free + fut: asyncio.Future[int] = asyncio.Future() + self._ble_connection_free_futures.append(fut) + return await fut @callback def async_remove_entity( From 75510b8e90162a5b7a530d36d141cbada3df644c Mon Sep 17 00:00:00 2001 From: Jafar Atili Date: Thu, 29 Sep 2022 16:03:39 +0300 Subject: [PATCH 030/985] Add cover platform for switchbee integration (#78383) * Added Platform cover for switchbee integration * added cover to .coveragerc * Applied code review feedback from other PR * Addressed comments from other PRs * rebased * Re-add carriage return * Update homeassistant/components/switchbee/cover.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/switchbee/cover.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/switchbee/cover.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/switchbee/cover.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * addressed CR comments * fixes * fixes * more fixes * more fixes * separate entities for cover and somfy cover * fixed isort * more fixes * more fixes * Update homeassistant/components/switchbee/cover.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/switchbee/cover.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * more fixes * more fixes * more Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .coveragerc | 1 + .../components/switchbee/__init__.py | 7 +- .../components/switchbee/coordinator.py | 2 + homeassistant/components/switchbee/cover.py | 152 ++++++++++++++++++ homeassistant/components/switchbee/entity.py | 5 +- homeassistant/components/switchbee/light.py | 10 +- homeassistant/components/switchbee/switch.py | 4 +- 7 files changed, 171 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/switchbee/cover.py diff --git a/.coveragerc b/.coveragerc index 6c31546e718..ba07953cca3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1231,6 +1231,7 @@ omit = homeassistant/components/switchbee/__init__.py homeassistant/components/switchbee/button.py homeassistant/components/switchbee/coordinator.py + homeassistant/components/switchbee/cover.py homeassistant/components/switchbee/entity.py homeassistant/components/switchbee/light.py homeassistant/components/switchbee/switch.py diff --git a/homeassistant/components/switchbee/__init__.py b/homeassistant/components/switchbee/__init__.py index d841121889b..7a843697e8d 100644 --- a/homeassistant/components/switchbee/__init__.py +++ b/homeassistant/components/switchbee/__init__.py @@ -13,7 +13,12 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN from .coordinator import SwitchBeeCoordinator -PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.LIGHT, Platform.SWITCH] +PLATFORMS: list[Platform] = [ + Platform.BUTTON, + Platform.COVER, + Platform.LIGHT, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/switchbee/coordinator.py b/homeassistant/components/switchbee/coordinator.py index 1eddba27fd3..f7101cd5990 100644 --- a/homeassistant/components/switchbee/coordinator.py +++ b/homeassistant/components/switchbee/coordinator.py @@ -62,6 +62,8 @@ class SwitchBeeCoordinator(DataUpdateCoordinator[Mapping[int, SwitchBeeBaseDevic DeviceType.TimedPowerSwitch, DeviceType.Scenario, DeviceType.Dimmer, + DeviceType.Shutter, + DeviceType.Somfy, ] ) except SwitchBeeError as exp: diff --git a/homeassistant/components/switchbee/cover.py b/homeassistant/components/switchbee/cover.py new file mode 100644 index 00000000000..ea5494f7f5b --- /dev/null +++ b/homeassistant/components/switchbee/cover.py @@ -0,0 +1,152 @@ +"""Support for SwitchBee cover.""" + +from __future__ import annotations + +from typing import Any + +from switchbee.api import SwitchBeeError, SwitchBeeTokenError +from switchbee.const import SomfyCommand +from switchbee.device import SwitchBeeShutter, SwitchBeeSomfy + +from homeassistant.components.cover import ( + ATTR_POSITION, + CoverDeviceClass, + CoverEntity, + CoverEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import SwitchBeeCoordinator +from .entity import SwitchBeeDeviceEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up SwitchBee switch.""" + coordinator: SwitchBeeCoordinator = hass.data[DOMAIN][entry.entry_id] + entities: list[CoverEntity] = [] + + for device in coordinator.data.values(): + if isinstance(device, SwitchBeeShutter): + entities.append(SwitchBeeCoverEntity(device, coordinator)) + elif isinstance(device, SwitchBeeSomfy): + entities.append(SwitchBeeSomfyEntity(device, coordinator)) + + async_add_entities(entities) + + +class SwitchBeeSomfyEntity(SwitchBeeDeviceEntity[SwitchBeeSomfy], CoverEntity): + """Representation of a SwitchBee Somfy cover.""" + + _attr_device_class = CoverDeviceClass.SHUTTER + _attr_supported_features = ( + CoverEntityFeature.CLOSE | CoverEntityFeature.OPEN | CoverEntityFeature.STOP + ) + _attr_is_closed = None + + async def _fire_somfy_command(self, command: str) -> None: + """Async function to fire Somfy device command.""" + try: + await self.coordinator.api.set_state(self._device.id, command) + except (SwitchBeeError, SwitchBeeTokenError) as exp: + raise HomeAssistantError( + f"Failed to fire {command} for {self.name}, {str(exp)}" + ) from exp + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + return await self._fire_somfy_command(SomfyCommand.UP) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover.""" + return await self._fire_somfy_command(SomfyCommand.DOWN) + + async def async_stop_cover(self, **kwargs: Any) -> None: + """Stop a moving cover.""" + return await self._fire_somfy_command(SomfyCommand.MY) + + +class SwitchBeeCoverEntity(SwitchBeeDeviceEntity[SwitchBeeShutter], CoverEntity): + """Representation of a SwitchBee cover.""" + + _attr_device_class = CoverDeviceClass.SHUTTER + _attr_supported_features = ( + CoverEntityFeature.CLOSE + | CoverEntityFeature.OPEN + | CoverEntityFeature.SET_POSITION + | CoverEntityFeature.STOP + ) + _attr_is_closed = None + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_from_coordinator() + super()._handle_coordinator_update() + + def _update_from_coordinator(self) -> None: + """Update the entity attributes from the coordinator data.""" + + coordinator_device = self._get_coordinator_device() + + if coordinator_device.position == -1: + self._check_if_became_offline() + return + + # check if the device was offline (now online) and bring it back + self._check_if_became_online() + + self._attr_current_cover_position = coordinator_device.position + + if self.current_cover_position == 0: + self._attr_is_closed = True + else: + self._attr_is_closed = False + super()._handle_coordinator_update() + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + if self.current_cover_position == 100: + return + + await self.async_set_cover_position(position=100) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover.""" + if self.current_cover_position == 0: + return + + await self.async_set_cover_position(position=0) + + async def async_stop_cover(self, **kwargs: Any) -> None: + """Stop a moving cover.""" + # to stop the shutter, we just interrupt it with any state during operation + await self.async_set_cover_position( + position=self.current_cover_position, force=True + ) + + # fetch data from the Central Unit to get the new position + await self.coordinator.async_request_refresh() + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Async function to set position to cover.""" + if ( + self.current_cover_position == kwargs[ATTR_POSITION] + and "force" not in kwargs + ): + return + try: + await self.coordinator.api.set_state(self._device.id, kwargs[ATTR_POSITION]) + except (SwitchBeeError, SwitchBeeTokenError) as exp: + raise HomeAssistantError( + f"Failed to set {self.name} position to {kwargs[ATTR_POSITION]}, error: {str(exp)}" + ) from exp + + self._get_coordinator_device().position = kwargs[ATTR_POSITION] + self.coordinator.async_set_updated_data(self.coordinator.data) + self.async_write_ha_state() diff --git a/homeassistant/components/switchbee/entity.py b/homeassistant/components/switchbee/entity.py index 516932d6f4e..4fed0c61393 100644 --- a/homeassistant/components/switchbee/entity.py +++ b/homeassistant/components/switchbee/entity.py @@ -1,6 +1,6 @@ """Support for SwitchBee entity.""" import logging -from typing import Generic, TypeVar +from typing import Generic, TypeVar, cast from switchbee import SWITCHBEE_BRAND from switchbee.api import SwitchBeeDeviceOfflineError, SwitchBeeError @@ -108,3 +108,6 @@ class SwitchBeeDeviceEntity(SwitchBeeEntity[_DeviceTypeT]): self.name, ) self._is_online = True + + def _get_coordinator_device(self) -> _DeviceTypeT: + return cast(_DeviceTypeT, self.coordinator.data[self._device.id]) diff --git a/homeassistant/components/switchbee/light.py b/homeassistant/components/switchbee/light.py index 4740da4cbbe..7bcf64598c1 100644 --- a/homeassistant/components/switchbee/light.py +++ b/homeassistant/components/switchbee/light.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, cast +from typing import Any from switchbee.api import SwitchBeeDeviceOfflineError, SwitchBeeError from switchbee.device import ApiStateCommand, DeviceType, SwitchBeeDimmer @@ -72,9 +72,7 @@ class SwitchBeeLightEntity(SwitchBeeDeviceEntity[SwitchBeeDimmer], LightEntity): def _update_attrs_from_coordinator(self) -> None: - coordinator_device = cast( - SwitchBeeDimmer, self.coordinator.data[self._device.id] - ) + coordinator_device = self._get_coordinator_device() brightness = coordinator_device.brightness # module is offline @@ -112,7 +110,7 @@ class SwitchBeeLightEntity(SwitchBeeDeviceEntity[SwitchBeeDimmer], LightEntity): return # update the coordinator data manually we already know the Central Unit brightness data for this light - cast(SwitchBeeDimmer, self.coordinator.data[self._device.id]).brightness = state + self._get_coordinator_device().brightness = state self.coordinator.async_set_updated_data(self.coordinator.data) async def async_turn_off(self, **kwargs: Any) -> None: @@ -125,5 +123,5 @@ class SwitchBeeLightEntity(SwitchBeeDeviceEntity[SwitchBeeDimmer], LightEntity): ) from exp # update the coordinator manually - cast(SwitchBeeDimmer, self.coordinator.data[self._device.id]).brightness = 0 + self._get_coordinator_device().brightness = 0 self.coordinator.async_set_updated_data(self.coordinator.data) diff --git a/homeassistant/components/switchbee/switch.py b/homeassistant/components/switchbee/switch.py index bb0a6123de2..48fee37449c 100644 --- a/homeassistant/components/switchbee/switch.py +++ b/homeassistant/components/switchbee/switch.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, TypeVar, Union, cast +from typing import Any, TypeVar, Union from switchbee.api import SwitchBeeDeviceOfflineError, SwitchBeeError from switchbee.device import ( @@ -76,7 +76,7 @@ class SwitchBeeSwitchEntity(SwitchBeeDeviceEntity[_DeviceTypeT], SwitchEntity): def _update_from_coordinator(self) -> None: """Update the entity attributes from the coordinator data.""" - coordinator_device = cast(_DeviceTypeT, self.coordinator.data[self._device.id]) + coordinator_device = self._get_coordinator_device() if coordinator_device.state == -1: From da445e515b77500ca80630540c0c0668f8af4a37 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 29 Sep 2022 16:19:28 +0200 Subject: [PATCH 031/985] Rename options key in rainmachine (#79249) --- homeassistant/components/rainmachine/select.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/select.py b/homeassistant/components/rainmachine/select.py index 82dddfb8f3a..41383bffc4e 100644 --- a/homeassistant/components/rainmachine/select.py +++ b/homeassistant/components/rainmachine/select.py @@ -43,7 +43,7 @@ class FreezeProtectionSelectOption: class FreezeProtectionTemperatureMixin: """Define an entity description mixin to include an options list.""" - options: list[FreezeProtectionSelectOption] + extended_options: list[FreezeProtectionSelectOption] @dataclass @@ -63,7 +63,7 @@ SELECT_DESCRIPTIONS = ( entity_category=EntityCategory.CONFIG, api_category=DATA_RESTRICTIONS_UNIVERSAL, data_key="freezeProtectTemp", - options=[ + extended_options=[ FreezeProtectionSelectOption( api_value=0.0, imperial_label="32°F", @@ -128,7 +128,7 @@ class FreezeProtectionTemperatureSelect(RainMachineEntity, SelectEntity): self._api_value_to_label_map = {} self._label_to_api_value_map = {} - for option in description.options: + for option in description.extended_options: if unit_system == CONF_UNIT_SYSTEM_IMPERIAL: label = option.imperial_label else: From db1797beb4abee166a689115813f3404d4cb3bf0 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 29 Sep 2022 08:58:16 -0600 Subject: [PATCH 032/985] Use correct exception type for RainMachine select API error (#79309) --- homeassistant/components/rainmachine/select.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rainmachine/select.py b/homeassistant/components/rainmachine/select.py index 41383bffc4e..33a0a38ed15 100644 --- a/homeassistant/components/rainmachine/select.py +++ b/homeassistant/components/rainmachine/select.py @@ -9,6 +9,7 @@ from homeassistant.components.select import SelectEntity, SelectEntityDescriptio from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -145,7 +146,7 @@ class FreezeProtectionTemperatureSelect(RainMachineEntity, SelectEntity): {self.entity_description.data_key: self._label_to_api_value_map[option]} ) except RainMachineError as err: - raise ValueError(f"Error while setting {self.name}: {err}") from err + raise HomeAssistantError(f"Error while setting {self.name}: {err}") from err @callback def update_from_latest_data(self) -> None: From d742e65ef5ff8bfb76b7696a68230fdf77811f92 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 29 Sep 2022 11:22:28 -0500 Subject: [PATCH 033/985] Don't create Repairs issue on RainMachine entity replacement (#79310) * Don't create Repairs issue on RainMachine entity replacement * Strings --- .../components/rainmachine/strings.json | 13 ------------ .../rainmachine/translations/en.json | 13 ------------ homeassistant/components/rainmachine/util.py | 20 +------------------ 3 files changed, 1 insertion(+), 45 deletions(-) diff --git a/homeassistant/components/rainmachine/strings.json b/homeassistant/components/rainmachine/strings.json index 95b92e99294..7634c0a69c5 100644 --- a/homeassistant/components/rainmachine/strings.json +++ b/homeassistant/components/rainmachine/strings.json @@ -27,18 +27,5 @@ } } } - }, - "issues": { - "replaced_old_entity": { - "title": "The {old_entity_id} entity will be removed", - "fix_flow": { - "step": { - "confirm": { - "title": "The {old_entity_id} entity will be removed", - "description": "Update any automations or scripts that use this entity to instead use `{replacement_entity_id}`." - } - } - } - } } } diff --git a/homeassistant/components/rainmachine/translations/en.json b/homeassistant/components/rainmachine/translations/en.json index 3e5d824ee08..9369eeae4c8 100644 --- a/homeassistant/components/rainmachine/translations/en.json +++ b/homeassistant/components/rainmachine/translations/en.json @@ -18,19 +18,6 @@ } } }, - "issues": { - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Update any automations or scripts that use this entity to instead use `{replacement_entity_id}`.", - "title": "The {old_entity_id} entity will be removed" - } - } - }, - "title": "The {old_entity_id} entity will be removed" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/util.py b/homeassistant/components/rainmachine/util.py index 3c66d530cf4..67ffc83d5bd 100644 --- a/homeassistant/components/rainmachine/util.py +++ b/homeassistant/components/rainmachine/util.py @@ -14,10 +14,9 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, LOGGER +from .const import LOGGER SIGNAL_REBOOT_COMPLETED = "rainmachine_reboot_completed_{0}" SIGNAL_REBOOT_REQUESTED = "rainmachine_reboot_requested_{0}" @@ -70,23 +69,6 @@ def async_finish_entity_domain_replacements( continue old_entity_id = registry_entry.entity_id - translation_key = "replaced_old_entity" - - async_create_issue( - hass, - DOMAIN, - f"{translation_key}_{old_entity_id}", - breaks_in_ha_version=strategy.breaks_in_ha_version, - is_fixable=True, - is_persistent=True, - severity=IssueSeverity.WARNING, - translation_key=translation_key, - translation_placeholders={ - "old_entity_id": old_entity_id, - "replacement_entity_id": strategy.replacement_entity_id, - }, - ) - if strategy.remove_old_entity: LOGGER.info('Removing old entity: "%s"', old_entity_id) ent_reg.async_remove(old_entity_id) From 11c09f4fd81ba7cf79333d16600c0d4d992a46a2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 29 Sep 2022 17:22:30 +0000 Subject: [PATCH 034/985] Check if `new_version` is not empty string in Shelly update platform (#79300) --- homeassistant/components/shelly/update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index 63972e9456d..fa37b394b6c 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -163,7 +163,7 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity): new_version = self.entity_description.latest_version( self.wrapper.device.status, ) - if new_version is not None: + if new_version not in (None, ""): return cast(str, new_version) return self.installed_version From 114db26fcf76d4f5294c5eb8dd8ef7a7191e36a4 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 29 Sep 2022 19:22:41 +0200 Subject: [PATCH 035/985] Update frontend to 20220929.0 (#79317) --- 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 f45372ac302..bcc574a4cad 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==20220928.0"], + "requirements": ["home-assistant-frontend==20220929.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8b1ced2c220..38b6ee1e52b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.17.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220928.0 +home-assistant-frontend==20220929.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 498c02ba14b..93daecfa29c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -862,7 +862,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20220928.0 +home-assistant-frontend==20220929.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44c99d4bb3d..ffa67eb0f02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -642,7 +642,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20220928.0 +home-assistant-frontend==20220929.0 # homeassistant.components.home_connect homeconnect==0.7.2 From f64596517248c451a6462f780a4de0073418e287 Mon Sep 17 00:00:00 2001 From: HarvsG <11440490+HarvsG@users.noreply.github.com> Date: Thu, 29 Sep 2022 17:24:06 +0000 Subject: [PATCH 036/985] Add repair for missing Bayesian `prob_given_false` (#79303) --- .../components/bayesian/binary_sensor.py | 19 ++++++-- homeassistant/components/bayesian/repairs.py | 17 ++++++- .../components/bayesian/strings.json | 12 +++++ .../components/bayesian/translations/en.json | 4 ++ .../components/bayesian/test_binary_sensor.py | 44 +++++++++++++++++++ 5 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/bayesian/strings.json diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 0e943b2d0ad..706c7ecdfd7 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import OrderedDict import logging +from typing import Any import voluptuous as vol @@ -34,7 +35,7 @@ from homeassistant.helpers.template import result_as_boolean from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DOMAIN, PLATFORMS -from .repairs import raise_mirrored_entries +from .repairs import raise_mirrored_entries, raise_no_prob_given_false ATTR_OBSERVATIONS = "observations" ATTR_OCCURRED_OBSERVATION_ENTITIES = "occurred_observation_entities" @@ -62,7 +63,7 @@ NUMERIC_STATE_SCHEMA = vol.Schema( vol.Optional(CONF_ABOVE): vol.Coerce(float), vol.Optional(CONF_BELOW): vol.Coerce(float), vol.Required(CONF_P_GIVEN_T): vol.Coerce(float), - vol.Required(CONF_P_GIVEN_F): vol.Coerce(float), + vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float), }, required=True, ) @@ -73,7 +74,7 @@ STATE_SCHEMA = vol.Schema( vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TO_STATE): cv.string, vol.Required(CONF_P_GIVEN_T): vol.Coerce(float), - vol.Required(CONF_P_GIVEN_F): vol.Coerce(float), + vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float), }, required=True, ) @@ -83,7 +84,7 @@ TEMPLATE_SCHEMA = vol.Schema( CONF_PLATFORM: CONF_TEMPLATE, vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Required(CONF_P_GIVEN_T): vol.Coerce(float), - vol.Required(CONF_P_GIVEN_F): vol.Coerce(float), + vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float), }, required=True, ) @@ -128,6 +129,16 @@ async def async_setup_platform( probability_threshold = config[CONF_PROBABILITY_THRESHOLD] device_class = config.get(CONF_DEVICE_CLASS) + # Should deprecate in some future version (2022.10 at time of writing) & make prob_given_false required in schemas. + broken_observations: list[dict[str, Any]] = [] + for observation in observations: + if CONF_P_GIVEN_F not in observation: + text: str = f"{name}/{observation.get(CONF_ENTITY_ID,'')}{observation.get(CONF_VALUE_TEMPLATE,'')}" + raise_no_prob_given_false(hass, observation, text) + _LOGGER.error("Missing prob_given_false YAML entry for %s", text) + broken_observations.append(observation) + observations = [x for x in observations if x not in broken_observations] + async_add_entities( [ BayesianBinarySensor( diff --git a/homeassistant/components/bayesian/repairs.py b/homeassistant/components/bayesian/repairs.py index a1391f8c550..a1d4f142527 100644 --- a/homeassistant/components/bayesian/repairs.py +++ b/homeassistant/components/bayesian/repairs.py @@ -31,9 +31,24 @@ def raise_mirrored_entries(hass: HomeAssistant, observations, text: str = "") -> "mirrored_entry/" + text, breaks_in_ha_version="2022.10.0", is_fixable=False, - is_persistent=False, severity=issue_registry.IssueSeverity.WARNING, translation_key="manual_migration", translation_placeholders={"entity": text}, learn_more_url="https://github.com/home-assistant/core/pull/67631", ) + + +# Should deprecate in some future version (2022.10 at time of writing) & make prob_given_false required in schemas. +def raise_no_prob_given_false(hass: HomeAssistant, observation, text: str) -> None: + """In previous 2022.9 and earlier, prob_given_false was optional and had a default version.""" + issue_registry.async_create_issue( + hass, + DOMAIN, + f"no_prob_given_false/{text}", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=issue_registry.IssueSeverity.ERROR, + translation_key="no_prob_given_false", + translation_placeholders={"entity": text}, + learn_more_url="https://github.com/home-assistant/core/pull/67631", + ) diff --git a/homeassistant/components/bayesian/strings.json b/homeassistant/components/bayesian/strings.json new file mode 100644 index 00000000000..338795624cd --- /dev/null +++ b/homeassistant/components/bayesian/strings.json @@ -0,0 +1,12 @@ +{ + "issues": { + "manual_migration": { + "description": "The Bayesian integration now also updates the probability if the observed `to_state`, `above`, `below`, or `value_template` evaluates to `False` rather than only `True`. So it is no longer required to have duplicate, complementary entries for each binary state. Please remove the mirrored entry for `{entity}`.", + "title": "Manual YAML fix required for Bayesian" + }, + "no_prob_given_false": { + "description": "In the Bayesian integration `prob_given_false` is now a required configuration variable as there was no mathematical rationale for the previous default value. Please add this to your `configuration.yml` for `bayesian/{entity}`. These observations will be ignored until you do.", + "title": "Manual YAML addition required for Bayesian" + } + } +} diff --git a/homeassistant/components/bayesian/translations/en.json b/homeassistant/components/bayesian/translations/en.json index ae9e5645f73..f95e153d986 100644 --- a/homeassistant/components/bayesian/translations/en.json +++ b/homeassistant/components/bayesian/translations/en.json @@ -3,6 +3,10 @@ "manual_migration": { "description": "The Bayesian integration now also updates the probability if the observed `to_state`, `above`, `below`, or `value_template` evaluates to `False` rather than only `True`. So it is no longer required to have duplicate, complementary entries for each binary state. Please remove the mirrored entry for `{entity}`.", "title": "Manual YAML fix required for Bayesian" + }, + "no_prob_given_false": { + "description": "In the Bayesian integration `prob_given_false` is now a required configuration variable as there was no mathematical rationale for the previous default value. Please add this to your `configuration.yml` for `bayesian/{entity}`. These observations will be ignored until you do.", + "title": "Manual YAML addition required for Bayesian" } } } diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 0344e2b9445..e16033c66a2 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -587,6 +587,50 @@ async def test_mirrored_observations(hass): ) +async def test_missing_prob_given_false(hass): + """Test whether missing prob_given_false are detected and appropriate issues are created.""" + + config = { + "binary_sensor": { + "platform": "bayesian", + "name": "missingpgf", + "observations": [ + { + "platform": "state", + "entity_id": "binary_sensor.test_monitored", + "to_state": "on", + "prob_given_true": 0.8, + }, + { + "platform": "template", + "value_template": "{{states('sensor.test_monitored2') == 'off'}}", + "prob_given_true": 0.79, + }, + { + "platform": "numeric_state", + "entity_id": "sensor.test_monitored1", + "above": 5, + "prob_given_true": 0.7, + }, + ], + "prior": 0.1, + } + } + assert len(async_get(hass).issues) == 0 + assert await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + hass.states.async_set("sensor.test_monitored2", "on") + await hass.async_block_till_done() + + assert len(async_get(hass).issues) == 3 + assert ( + async_get(hass).issues[ + ("bayesian", "no_prob_given_false/missingpgf/sensor.test_monitored1") + ] + is not None + ) + + async def test_probability_updates(hass): """Test probability update function.""" prob_given_true = [0.3, 0.6, 0.8] From b659a19f4aaa6107d242a42aa80c74eb2bc16b8f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 29 Sep 2022 12:24:52 -0500 Subject: [PATCH 037/985] Don't create Repairs issue on Guardian entity replacement (#79311) --- .../components/guardian/strings.json | 11 ---------- .../components/guardian/translations/en.json | 11 ---------- homeassistant/components/guardian/util.py | 20 +------------------ 3 files changed, 1 insertion(+), 41 deletions(-) diff --git a/homeassistant/components/guardian/strings.json b/homeassistant/components/guardian/strings.json index 33ddcf637a4..683f13c8d36 100644 --- a/homeassistant/components/guardian/strings.json +++ b/homeassistant/components/guardian/strings.json @@ -29,17 +29,6 @@ } } } - }, - "replaced_old_entity": { - "title": "The {old_entity_id} entity will be removed", - "fix_flow": { - "step": { - "confirm": { - "title": "The {old_entity_id} entity will be removed", - "description": "Update any automations or scripts that use this entity to instead use `{replacement_entity_id}`." - } - } - } } } } diff --git a/homeassistant/components/guardian/translations/en.json b/homeassistant/components/guardian/translations/en.json index ac87ae36506..1aaf8b888c8 100644 --- a/homeassistant/components/guardian/translations/en.json +++ b/homeassistant/components/guardian/translations/en.json @@ -29,17 +29,6 @@ } }, "title": "The {deprecated_service} service will be removed" - }, - "replaced_old_entity": { - "fix_flow": { - "step": { - "confirm": { - "description": "Update any automations or scripts that use this entity to instead use `{replacement_entity_id}`.", - "title": "The {old_entity_id} entity will be removed" - } - } - }, - "title": "The {old_entity_id} entity will be removed" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py index 9966435e7b0..250fee58db5 100644 --- a/homeassistant/components/guardian/util.py +++ b/homeassistant/components/guardian/util.py @@ -14,10 +14,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN, LOGGER +from .const import LOGGER DEFAULT_UPDATE_INTERVAL = timedelta(seconds=30) @@ -56,23 +55,6 @@ def async_finish_entity_domain_replacements( continue old_entity_id = registry_entry.entity_id - translation_key = "replaced_old_entity" - - async_create_issue( - hass, - DOMAIN, - f"{translation_key}_{old_entity_id}", - breaks_in_ha_version=strategy.breaks_in_ha_version, - is_fixable=True, - is_persistent=True, - severity=IssueSeverity.WARNING, - translation_key=translation_key, - translation_placeholders={ - "old_entity_id": old_entity_id, - "replacement_entity_id": strategy.replacement_entity_id, - }, - ) - if strategy.remove_old_entity: LOGGER.info('Removing old entity: "%s"', old_entity_id) ent_reg.async_remove(old_entity_id) From ee32e0eb3f8017bc1fc5578d73ee753e059ed686 Mon Sep 17 00:00:00 2001 From: Dennis Schroer Date: Thu, 29 Sep 2022 19:25:23 +0200 Subject: [PATCH 038/985] Update huisbaasje-client 0.1.0 to energyflip-client 0.2.0 (#79233) --- .../components/huisbaasje/__init__.py | 20 ++-- .../components/huisbaasje/config_flow.py | 17 +-- homeassistant/components/huisbaasje/const.py | 2 +- .../components/huisbaasje/manifest.json | 2 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- .../components/huisbaasje/test_config_flow.py | 108 +++++++++++++++--- tests/components/huisbaasje/test_init.py | 16 +-- tests/components/huisbaasje/test_sensor.py | 12 +- 9 files changed, 131 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index fa810d823ca..a3d8863f566 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging import async_timeout -from huisbaasje import Huisbaasje, HuisbaasjeException +from energyflip import EnergyFlip, EnergyFlipException from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform @@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Huisbaasje from a config entry.""" # Create the Huisbaasje client - huisbaasje = Huisbaasje( + energyflip = EnergyFlip( username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], source_types=SOURCE_TYPES, @@ -40,13 +40,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Attempt authentication. If this fails, an exception is thrown try: - await huisbaasje.authenticate() - except HuisbaasjeException as exception: + await energyflip.authenticate() + except EnergyFlipException as exception: _LOGGER.error("Authentication failed: %s", str(exception)) return False async def async_update_data(): - return await async_update_huisbaasje(huisbaasje) + return await async_update_huisbaasje(energyflip) # Create a coordinator for polling updates coordinator = DataUpdateCoordinator( @@ -80,17 +80,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def async_update_huisbaasje(huisbaasje): +async def async_update_huisbaasje(energyflip): """Update the data by performing a request to Huisbaasje.""" try: # Note: asyncio.TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. async with async_timeout.timeout(FETCH_TIMEOUT): - if not huisbaasje.is_authenticated(): + if not energyflip.is_authenticated(): _LOGGER.warning("Huisbaasje is unauthenticated. Reauthenticating") - await huisbaasje.authenticate() + await energyflip.authenticate() - current_measurements = await huisbaasje.current_measurements() + current_measurements = await energyflip.current_measurements() return { source_type: { @@ -112,7 +112,7 @@ async def async_update_huisbaasje(huisbaasje): } for source_type in SOURCE_TYPES } - except HuisbaasjeException as exception: + except EnergyFlipException as exception: raise UpdateFailed(f"Error communicating with API: {exception}") from exception diff --git a/homeassistant/components/huisbaasje/config_flow.py b/homeassistant/components/huisbaasje/config_flow.py index 4139b0d75c5..fc3a1c06a15 100644 --- a/homeassistant/components/huisbaasje/config_flow.py +++ b/homeassistant/components/huisbaasje/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Huisbaasje integration.""" import logging -from huisbaasje import Huisbaasje, HuisbaasjeConnectionException, HuisbaasjeException +from energyflip import EnergyFlip, EnergyFlipConnectionException, EnergyFlipException import voluptuous as vol from homeassistant import config_entries @@ -31,10 +31,10 @@ class HuisbaasjeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: user_id = await self._validate_input(user_input) - except HuisbaasjeConnectionException as exception: + except EnergyFlipConnectionException as exception: _LOGGER.warning(exception) errors["base"] = "cannot_connect" - except HuisbaasjeException as exception: + except EnergyFlipException as exception: _LOGGER.warning(exception) errors["base"] = "invalid_auth" except AbortFlow: @@ -72,9 +72,12 @@ class HuisbaasjeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): username = user_input[CONF_USERNAME] password = user_input[CONF_PASSWORD] - huisbaasje = Huisbaasje(username, password) + energyflip = EnergyFlip(username, password) - # Attempt authentication. If this fails, an HuisbaasjeException will be thrown - await huisbaasje.authenticate() + # Attempt authentication. If this fails, an EnergyFlipException will be thrown + await energyflip.authenticate() - return huisbaasje.get_user_id() + # Request customer overview. This also sets the user id on the client + await energyflip.customer_overview() + + return energyflip.get_user_id() diff --git a/homeassistant/components/huisbaasje/const.py b/homeassistant/components/huisbaasje/const.py index 637ebd03a17..481f11b2a36 100644 --- a/homeassistant/components/huisbaasje/const.py +++ b/homeassistant/components/huisbaasje/const.py @@ -1,5 +1,5 @@ """Constants for the Huisbaasje integration.""" -from huisbaasje.const import ( +from energyflip.const import ( SOURCE_TYPE_ELECTRICITY, SOURCE_TYPE_ELECTRICITY_IN, SOURCE_TYPE_ELECTRICITY_IN_LOW, diff --git a/homeassistant/components/huisbaasje/manifest.json b/homeassistant/components/huisbaasje/manifest.json index bf3155ed9b8..2963a82512b 100644 --- a/homeassistant/components/huisbaasje/manifest.json +++ b/homeassistant/components/huisbaasje/manifest.json @@ -3,7 +3,7 @@ "name": "Huisbaasje", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huisbaasje", - "requirements": ["huisbaasje-client==0.1.0"], + "requirements": ["energyflip-client==0.2.1"], "codeowners": ["@dennisschroer"], "iot_class": "cloud_polling", "loggers": ["huisbaasje"] diff --git a/requirements_all.txt b/requirements_all.txt index 93daecfa29c..e0e7f4261ea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -622,6 +622,9 @@ elmax_api==0.0.2 # homeassistant.components.emulated_roku emulated_roku==0.2.1 +# homeassistant.components.huisbaasje +energyflip-client==0.2.1 + # homeassistant.components.enocean enocean==0.50 @@ -882,9 +885,6 @@ httplib2==0.20.4 # homeassistant.components.huawei_lte huawei-lte-api==1.6.1 -# homeassistant.components.huisbaasje -huisbaasje-client==0.1.0 - # homeassistant.components.hydrawise hydrawiser==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ffa67eb0f02..cf7b95efd29 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -475,6 +475,9 @@ elmax_api==0.0.2 # homeassistant.components.emulated_roku emulated_roku==0.2.1 +# homeassistant.components.huisbaasje +energyflip-client==0.2.1 + # homeassistant.components.enocean enocean==0.50 @@ -659,9 +662,6 @@ httplib2==0.20.4 # homeassistant.components.huawei_lte huawei-lte-api==1.6.1 -# homeassistant.components.huisbaasje -huisbaasje-client==0.1.0 - # homeassistant.components.hyperion hyperion-py==0.7.5 diff --git a/tests/components/huisbaasje/test_config_flow.py b/tests/components/huisbaasje/test_config_flow.py index 8aac11baf6d..e270079de8c 100644 --- a/tests/components/huisbaasje/test_config_flow.py +++ b/tests/components/huisbaasje/test_config_flow.py @@ -1,11 +1,13 @@ """Test the Huisbaasje config flow.""" from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow -from homeassistant.components.huisbaasje.config_flow import ( - HuisbaasjeConnectionException, - HuisbaasjeException, +from energyflip import ( + EnergyFlipConnectionException, + EnergyFlipException, + EnergyFlipUnauthenticatedException, ) + +from homeassistant import config_entries, data_entry_flow from homeassistant.components.huisbaasje.const import DOMAIN from tests.common import MockConfigEntry @@ -21,9 +23,11 @@ async def test_form(hass): assert result["errors"] == {} with patch( - "huisbaasje.Huisbaasje.authenticate", return_value=None + "energyflip.EnergyFlip.authenticate", return_value=None ) as mock_authenticate, patch( - "huisbaasje.Huisbaasje.get_user_id", + "energyflip.EnergyFlip.customer_overview", return_value=None + ) as mock_customer_overview, patch( + "energyflip.EnergyFlip.get_user_id", return_value="test-id", ) as mock_get_user_id, patch( "homeassistant.components.huisbaasje.async_setup_entry", @@ -46,6 +50,7 @@ async def test_form(hass): "password": "test-password", } assert len(mock_authenticate.mock_calls) == 1 + assert len(mock_customer_overview.mock_calls) == 1 assert len(mock_get_user_id.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -57,8 +62,8 @@ async def test_form_invalid_auth(hass): ) with patch( - "huisbaasje.Huisbaasje.authenticate", - side_effect=HuisbaasjeException, + "energyflip.EnergyFlip.authenticate", + side_effect=EnergyFlipException, ): form_result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -72,15 +77,15 @@ async def test_form_invalid_auth(hass): assert form_result["errors"] == {"base": "invalid_auth"} -async def test_form_cannot_connect(hass): - """Test we handle cannot connect error.""" +async def test_form_authenticate_cannot_connect(hass): + """Test we handle cannot connect error in authenticate.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( - "huisbaasje.Huisbaasje.authenticate", - side_effect=HuisbaasjeConnectionException, + "energyflip.EnergyFlip.authenticate", + side_effect=EnergyFlipConnectionException, ): form_result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -94,14 +99,80 @@ async def test_form_cannot_connect(hass): assert form_result["errors"] == {"base": "cannot_connect"} -async def test_form_unknown_error(hass): - """Test we handle an unknown error.""" +async def test_form_authenticate_unknown_error(hass): + """Test we handle an unknown error in authenticate.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( - "huisbaasje.Huisbaasje.authenticate", + "energyflip.EnergyFlip.authenticate", + side_effect=Exception, + ): + form_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert form_result["type"] == data_entry_flow.FlowResultType.FORM + assert form_result["errors"] == {"base": "unknown"} + + +async def test_form_customer_overview_cannot_connect(hass): + """Test we handle cannot connect error in customer_overview.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("energyflip.EnergyFlip.authenticate", return_value=None), patch( + "energyflip.EnergyFlip.customer_overview", + side_effect=EnergyFlipConnectionException, + ): + form_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert form_result["type"] == data_entry_flow.FlowResultType.FORM + assert form_result["errors"] == {"base": "cannot_connect"} + + +async def test_form_customer_overview_authentication_error(hass): + """Test we handle an unknown error in customer_overview.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("energyflip.EnergyFlip.authenticate", return_value=None), patch( + "energyflip.EnergyFlip.customer_overview", + side_effect=EnergyFlipUnauthenticatedException, + ): + form_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert form_result["type"] == data_entry_flow.FlowResultType.FORM + assert form_result["errors"] == {"base": "invalid_auth"} + + +async def test_form_customer_overview_unknown_error(hass): + """Test we handle an unknown error in customer_overview.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("energyflip.EnergyFlip.authenticate", return_value=None), patch( + "energyflip.EnergyFlip.customer_overview", side_effect=Exception, ): form_result = await hass.config_entries.flow.async_configure( @@ -133,10 +204,9 @@ async def test_form_entry_exists(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("huisbaasje.Huisbaasje.authenticate", return_value=None), patch( - "huisbaasje.Huisbaasje.get_user_id", - return_value="test-id", - ), patch( + with patch("energyflip.EnergyFlip.authenticate", return_value=None), patch( + "energyflip.EnergyFlip.customer_overview", return_value=None + ), patch("energyflip.EnergyFlip.get_user_id", return_value="test-id",), patch( "homeassistant.components.huisbaasje.async_setup_entry", return_value=True, ): diff --git a/tests/components/huisbaasje/test_init.py b/tests/components/huisbaasje/test_init.py index 859cfc4df83..30de00fd64f 100644 --- a/tests/components/huisbaasje/test_init.py +++ b/tests/components/huisbaasje/test_init.py @@ -1,7 +1,7 @@ """Test cases for the initialisation of the Huisbaasje integration.""" from unittest.mock import patch -from huisbaasje import HuisbaasjeException +from energyflip import EnergyFlipException from homeassistant.components import huisbaasje from homeassistant.config_entries import ConfigEntryState @@ -24,11 +24,11 @@ async def test_setup(hass: HomeAssistant): async def test_setup_entry(hass: HomeAssistant): """Test for successfully setting a config entry.""" with patch( - "huisbaasje.Huisbaasje.authenticate", return_value=None + "energyflip.EnergyFlip.authenticate", return_value=None ) as mock_authenticate, patch( - "huisbaasje.Huisbaasje.is_authenticated", return_value=True + "energyflip.EnergyFlip.is_authenticated", return_value=True ) as mock_is_authenticated, patch( - "huisbaasje.Huisbaasje.current_measurements", + "energyflip.EnergyFlip.current_measurements", return_value=MOCK_CURRENT_MEASUREMENTS, ) as mock_current_measurements: hass.config.components.add(huisbaasje.DOMAIN) @@ -68,7 +68,7 @@ async def test_setup_entry(hass: HomeAssistant): async def test_setup_entry_error(hass: HomeAssistant): """Test for successfully setting a config entry.""" with patch( - "huisbaasje.Huisbaasje.authenticate", side_effect=HuisbaasjeException + "energyflip.EnergyFlip.authenticate", side_effect=EnergyFlipException ) as mock_authenticate: hass.config.components.add(huisbaasje.DOMAIN) config_entry = MockConfigEntry( @@ -103,11 +103,11 @@ async def test_setup_entry_error(hass: HomeAssistant): async def test_unload_entry(hass: HomeAssistant): """Test for successfully unloading the config entry.""" with patch( - "huisbaasje.Huisbaasje.authenticate", return_value=None + "energyflip.EnergyFlip.authenticate", return_value=None ) as mock_authenticate, patch( - "huisbaasje.Huisbaasje.is_authenticated", return_value=True + "energyflip.EnergyFlip.is_authenticated", return_value=True ) as mock_is_authenticated, patch( - "huisbaasje.Huisbaasje.current_measurements", + "energyflip.EnergyFlip.current_measurements", return_value=MOCK_CURRENT_MEASUREMENTS, ) as mock_current_measurements: hass.config.components.add(huisbaasje.DOMAIN) diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py index 84e1f71071c..43789988003 100644 --- a/tests/components/huisbaasje/test_sensor.py +++ b/tests/components/huisbaasje/test_sensor.py @@ -29,11 +29,11 @@ from tests.common import MockConfigEntry async def test_setup_entry(hass: HomeAssistant): """Test for successfully loading sensor states.""" with patch( - "huisbaasje.Huisbaasje.authenticate", return_value=None + "energyflip.EnergyFlip.authenticate", return_value=None ) as mock_authenticate, patch( - "huisbaasje.Huisbaasje.is_authenticated", return_value=True + "energyflip.EnergyFlip.is_authenticated", return_value=True ) as mock_is_authenticated, patch( - "huisbaasje.Huisbaasje.current_measurements", + "energyflip.EnergyFlip.current_measurements", return_value=MOCK_CURRENT_MEASUREMENTS, ) as mock_current_measurements: @@ -344,11 +344,11 @@ async def test_setup_entry(hass: HomeAssistant): async def test_setup_entry_absent_measurement(hass: HomeAssistant): """Test for successfully loading sensor states when response does not contain all measurements.""" with patch( - "huisbaasje.Huisbaasje.authenticate", return_value=None + "energyflip.EnergyFlip.authenticate", return_value=None ) as mock_authenticate, patch( - "huisbaasje.Huisbaasje.is_authenticated", return_value=True + "energyflip.EnergyFlip.is_authenticated", return_value=True ) as mock_is_authenticated, patch( - "huisbaasje.Huisbaasje.current_measurements", + "energyflip.EnergyFlip.current_measurements", return_value=MOCK_LIMITED_CURRENT_MEASUREMENTS, ) as mock_current_measurements: From ba6a81c565f162c50166bf776b21b6a2271d7eef Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Thu, 29 Sep 2022 11:26:28 -0600 Subject: [PATCH 039/985] Resolve traceback error when using variables in template triggers (#77287) Co-authored-by: Erik --- homeassistant/helpers/trigger.py | 63 ++++++++++++++++++++++++------- tests/helpers/test_trigger.py | 64 +++++++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 15 deletions(-) diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index 9fde56ec7aa..4cb724a6435 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -2,10 +2,10 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Coroutine import functools import logging -from typing import TYPE_CHECKING, Any, Protocol, TypedDict +from typing import TYPE_CHECKING, Any, Protocol, TypedDict, cast import voluptuous as vol @@ -16,7 +16,13 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_VARIABLES, ) -from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Context, + HomeAssistant, + callback, + is_callback, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import IntegrationNotFound, async_get_integration @@ -101,20 +107,51 @@ async def async_validate_trigger_config( def _trigger_action_wrapper( hass: HomeAssistant, action: Callable, conf: ConfigType ) -> Callable: - """Wrap trigger action with extra vars if configured.""" + """Wrap trigger action with extra vars if configured. + + If action is a coroutine function, a coroutine function will be returned. + If action is a callback, a callback will be returned. + """ if CONF_VARIABLES not in conf: return action - @functools.wraps(action) - async def with_vars( - run_variables: dict[str, Any], context: Context | None = None - ) -> None: - """Wrap action with extra vars.""" - trigger_variables = conf[CONF_VARIABLES] - run_variables.update(trigger_variables.async_render(hass, run_variables)) - await action(run_variables, context) + # Check for partials to properly determine if coroutine function + check_func = action + while isinstance(check_func, functools.partial): + check_func = check_func.func - return with_vars + wrapper_func: Callable[..., None] | Callable[..., Coroutine[Any, Any, None]] + if asyncio.iscoroutinefunction(check_func): + async_action = cast(Callable[..., Coroutine[Any, Any, None]], action) + + @functools.wraps(async_action) + async def async_with_vars( + run_variables: dict[str, Any], context: Context | None = None + ) -> None: + """Wrap action with extra vars.""" + trigger_variables = conf[CONF_VARIABLES] + run_variables.update(trigger_variables.async_render(hass, run_variables)) + await action(run_variables, context) + + wrapper_func = async_with_vars + + else: + + @functools.wraps(action) + async def with_vars( + run_variables: dict[str, Any], context: Context | None = None + ) -> None: + """Wrap action with extra vars.""" + trigger_variables = conf[CONF_VARIABLES] + run_variables.update(trigger_variables.async_render(hass, run_variables)) + action(run_variables, context) + + if is_callback(check_func): + with_vars = callback(with_vars) + + wrapper_func = with_vars + + return wrapper_func async def async_initialize_triggers( diff --git a/tests/helpers/test_trigger.py b/tests/helpers/test_trigger.py index 7cee307f3ec..9cd3b0956ce 100644 --- a/tests/helpers/test_trigger.py +++ b/tests/helpers/test_trigger.py @@ -1,12 +1,13 @@ """The tests for the trigger helper.""" -from unittest.mock import MagicMock, call, patch +from unittest.mock import ANY, MagicMock, call, patch import pytest import voluptuous as vol -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers.trigger import ( _async_get_trigger_platform, + async_initialize_triggers, async_validate_trigger_config, ) from homeassistant.setup import async_setup_component @@ -137,3 +138,62 @@ async def test_trigger_alias( "Automation trigger 'My event' triggered by event 'trigger_event'" in caplog.text ) + + +async def test_async_initialize_triggers( + hass: HomeAssistant, calls: list[ServiceCall], caplog: pytest.LogCaptureFixture +) -> None: + """Test async_initialize_triggers with different action types.""" + + log_cb = MagicMock() + + action_calls = [] + + trigger_config = await async_validate_trigger_config( + hass, + [ + { + "platform": "event", + "event_type": ["trigger_event"], + "variables": { + "name": "Paulus", + "via_event": "{{ trigger.event.event_type }}", + }, + } + ], + ) + + async def async_action(*args): + action_calls.append([*args]) + + @callback + def cb_action(*args): + action_calls.append([*args]) + + def non_cb_action(*args): + action_calls.append([*args]) + + for action in (async_action, cb_action, non_cb_action): + action_calls = [] + + unsub = await async_initialize_triggers( + hass, + trigger_config, + action, + "test", + "", + log_cb, + ) + await hass.async_block_till_done() + + hass.bus.async_fire("trigger_event") + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert len(action_calls) == 1 + assert action_calls[0][0]["name"] == "Paulus" + assert action_calls[0][0]["via_event"] == "trigger_event" + log_cb.assert_called_once_with(ANY, "Initialized trigger") + + log_cb.reset_mock() + unsub() From d7d6637d7960ed8e4992af27ec12f217256a01e9 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 29 Sep 2022 14:29:41 -0400 Subject: [PATCH 040/985] Unregister Google sheets services during unload (#79314) * Unregister services during unload - Google Sheets * uno mas --- homeassistant/components/google_sheets/__init__.py | 11 ++++++++++- tests/components/google_sheets/test_init.py | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_sheets/__init__.py b/homeassistant/components/google_sheets/__init__.py index a4c10da7f23..ea96288371c 100644 --- a/homeassistant/components/google_sheets/__init__.py +++ b/homeassistant/components/google_sheets/__init__.py @@ -10,7 +10,7 @@ from google.oauth2.credentials import Credentials from gspread import Client import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -69,6 +69,15 @@ def async_entry_has_scopes(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" hass.data[DOMAIN].pop(entry.entry_id) + loaded_entries = [ + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + if len(loaded_entries) == 1: + for service_name in hass.services.async_services()[DOMAIN]: + hass.services.async_remove(DOMAIN, service_name) + return True diff --git a/tests/components/google_sheets/test_init.py b/tests/components/google_sheets/test_init.py index d060e01bac2..c32eb345534 100644 --- a/tests/components/google_sheets/test_init.py +++ b/tests/components/google_sheets/test_init.py @@ -80,6 +80,7 @@ async def mock_setup_integration( assert len(entries) == 1 await hass.config_entries.async_unload(entries[0].entry_id) await hass.async_block_till_done() + assert not len(hass.services.async_services().get(DOMAIN, {})) assert not hass.data.get(DOMAIN) assert entries[0].state is ConfigEntryState.NOT_LOADED From a01f18a3ac38933e3ffca9cb2f22a8bcfc97e74b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Sep 2022 08:37:31 -1000 Subject: [PATCH 041/985] Handle short local names from esphome proxies (#79321) --- .../components/esphome/bluetooth/scanner.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/scanner.py b/homeassistant/components/esphome/bluetooth/scanner.py index fbd5f185907..36138192f8f 100644 --- a/homeassistant/components/esphome/bluetooth/scanner.py +++ b/homeassistant/components/esphome/bluetooth/scanner.py @@ -83,15 +83,23 @@ class ESPHomeScanner(BaseHaScanner): """Call the registered callback.""" now = time.monotonic() address = ":".join(TWO_CHAR.findall("%012X" % adv.address)) # must be upper + name = adv.name + if prev_discovery := self._discovered_devices.get(address): + # If the last discovery had the full local name + # and this one doesn't, keep the old one as we + # always want the full local name over the short one + if len(prev_discovery.name) > len(adv.name): + name = prev_discovery.name + advertisement_data = AdvertisementData( # type: ignore[no-untyped-call] - local_name=None if adv.name == "" else adv.name, + local_name=None if name == "" else name, manufacturer_data=adv.manufacturer_data, service_data=adv.service_data, service_uuids=adv.service_uuids, ) device = BLEDevice( # type: ignore[no-untyped-call] address=address, - name=adv.name, + name=name, details=self._details, rssi=adv.rssi, ) From d5b966d942a07036451505a4236d61d2884c7b68 Mon Sep 17 00:00:00 2001 From: Vincent Giorgi Date: Thu, 29 Sep 2022 21:55:45 +0200 Subject: [PATCH 042/985] Add Airthings BLE component (#77284) Co-authored-by: Paulus Schoutsen Co-authored-by: J. Nick Koston --- .coveragerc | 2 + CODEOWNERS | 2 + homeassistant/brands/airthings.json | 5 + .../components/airthings_ble/__init__.py | 73 +++++++ .../components/airthings_ble/config_flow.py | 169 +++++++++++++++ .../components/airthings_ble/const.py | 9 + .../components/airthings_ble/manifest.json | 15 ++ .../components/airthings_ble/sensor.py | 185 +++++++++++++++++ .../components/airthings_ble/strings.json | 23 +++ .../airthings_ble/translations/en.json | 21 ++ homeassistant/generated/bluetooth.py | 4 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 16 +- requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/airthings_ble/__init__.py | 99 +++++++++ tests/components/airthings_ble/conftest.py | 8 + .../airthings_ble/test_config_flow.py | 194 ++++++++++++++++++ 18 files changed, 829 insertions(+), 3 deletions(-) create mode 100644 homeassistant/brands/airthings.json create mode 100644 homeassistant/components/airthings_ble/__init__.py create mode 100644 homeassistant/components/airthings_ble/config_flow.py create mode 100644 homeassistant/components/airthings_ble/const.py create mode 100644 homeassistant/components/airthings_ble/manifest.json create mode 100644 homeassistant/components/airthings_ble/sensor.py create mode 100644 homeassistant/components/airthings_ble/strings.json create mode 100644 homeassistant/components/airthings_ble/translations/en.json create mode 100644 tests/components/airthings_ble/__init__.py create mode 100644 tests/components/airthings_ble/conftest.py create mode 100644 tests/components/airthings_ble/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index ba07953cca3..45b9c8d1086 100644 --- a/.coveragerc +++ b/.coveragerc @@ -37,6 +37,8 @@ omit = homeassistant/components/airnow/sensor.py homeassistant/components/airthings/__init__.py homeassistant/components/airthings/sensor.py + homeassistant/components/airthings_ble/__init__.py + homeassistant/components/airthings_ble/sensor.py homeassistant/components/airtouch4/__init__.py homeassistant/components/airtouch4/climate.py homeassistant/components/airtouch4/const.py diff --git a/CODEOWNERS b/CODEOWNERS index 5c39337af74..d6d4ca61613 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -45,6 +45,8 @@ build.json @home-assistant/supervisor /tests/components/airnow/ @asymworks /homeassistant/components/airthings/ @danielhiversen /tests/components/airthings/ @danielhiversen +/homeassistant/components/airthings_ble/ @vincegio +/tests/components/airthings_ble/ @vincegio /homeassistant/components/airtouch4/ @LonePurpleWolf /tests/components/airtouch4/ @LonePurpleWolf /homeassistant/components/airvisual/ @bachya diff --git a/homeassistant/brands/airthings.json b/homeassistant/brands/airthings.json new file mode 100644 index 00000000000..e83546f9d61 --- /dev/null +++ b/homeassistant/brands/airthings.json @@ -0,0 +1,5 @@ +{ + "domain": "airthings", + "name": "Airthings", + "integrations": ["airthings", "airthings_ble"] +} diff --git a/homeassistant/components/airthings_ble/__init__.py b/homeassistant/components/airthings_ble/__init__.py new file mode 100644 index 00000000000..4e066ea8447 --- /dev/null +++ b/homeassistant/components/airthings_ble/__init__.py @@ -0,0 +1,73 @@ +"""The Airthings BLE integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from airthings_ble import AirthingsBluetoothDeviceData + +from homeassistant.components import bluetooth +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Airthings BLE device from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + address = entry.unique_id + + elevation = hass.config.elevation + is_metric = hass.config.units.is_metric + assert address is not None + + ble_device = bluetooth.async_ble_device_from_address(hass, address) + + if not ble_device: + raise ConfigEntryNotReady( + f"Could not find Airthings device with address {address}" + ) + + async def _async_update_method(): + """Get data from Airthings BLE.""" + ble_device = bluetooth.async_ble_device_from_address(hass, address) + airthings = AirthingsBluetoothDeviceData(_LOGGER, elevation, is_metric) + + try: + data = await airthings.update_device(ble_device) + except Exception as err: + raise UpdateFailed(f"Unable to fetch data: {err}") from err + + return data + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN, + update_method=_async_update_method, + update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/airthings_ble/config_flow.py b/homeassistant/components/airthings_ble/config_flow.py new file mode 100644 index 00000000000..6d5df7ddd56 --- /dev/null +++ b/homeassistant/components/airthings_ble/config_flow.py @@ -0,0 +1,169 @@ +"""Config flow for Airthings BlE integration.""" + +from __future__ import annotations + +import dataclasses +import logging +from typing import Any + +from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice +from bleak import BleakError +import voluptuous as vol + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth import ( + BluetoothServiceInfo, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN, MFCT_ID + +_LOGGER = logging.getLogger(__name__) + + +@dataclasses.dataclass +class Discovery: + """A discovered bluetooth device.""" + + name: str + discovery_info: BluetoothServiceInfo + device: AirthingsDevice + + +def get_name(device: AirthingsDevice) -> str: + """Generate name with identifier for device.""" + return f"{device.name} ({device.identifier})" + + +class AirthingsDeviceUpdateError(Exception): + """Custom error class for device updates.""" + + +class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Airthings BLE.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovered_device: Discovery | None = None + self._discovered_devices: dict[str, Discovery] = {} + + async def _get_device_data( + self, discovery_info: BluetoothServiceInfo + ) -> AirthingsDevice: + ble_device = bluetooth.async_ble_device_from_address( + self.hass, discovery_info.address + ) + if ble_device is None: + _LOGGER.debug("no ble_device in _get_device_data") + raise AirthingsDeviceUpdateError("No ble_device") + + airthings = AirthingsBluetoothDeviceData(_LOGGER) + + try: + data = await airthings.update_device(ble_device) + except BleakError as err: + _LOGGER.error( + "Error connecting to and getting data from %s: %s", + discovery_info.address, + err, + ) + raise AirthingsDeviceUpdateError("Failed getting device data") from err + except Exception as err: + _LOGGER.error( + "Unknown error occurred from %s: %s", discovery_info.address, err + ) + raise err + return data + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + _LOGGER.debug("Discovered BT device: %s", discovery_info) + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + + try: + device = await self._get_device_data(discovery_info) + except AirthingsDeviceUpdateError: + return self.async_abort(reason="cannot_connect") + except Exception: # pylint: disable=broad-except + return self.async_abort(reason="unknown") + + name = get_name(device) + self.context["title_placeholders"] = {"name": name} + self._discovered_device = Discovery(name, discovery_info, device) + + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + if user_input is not None: + return self.async_create_entry( + title=self.context["title_placeholders"]["name"], data={} + ) + + self._set_confirm_only() + return self.async_show_form( + step_id="bluetooth_confirm", + description_placeholders=self.context["title_placeholders"], + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + discovery = self._discovered_devices[address] + + self.context["title_placeholders"] = { + "name": discovery.name, + } + + self._discovered_device = discovery + + return self.async_create_entry(title=discovery.name, data={}) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + + if MFCT_ID not in discovery_info.manufacturer_data: + continue + + try: + device = await self._get_device_data(discovery_info) + except AirthingsDeviceUpdateError: + return self.async_abort(reason="cannot_connect") + except Exception: # pylint: disable=broad-except + return self.async_abort(reason="unknown") + name = get_name(device) + self._discovered_devices[address] = Discovery(name, discovery_info, device) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + titles = { + address: get_name(discovery.device) + for (address, discovery) in self._discovered_devices.items() + } + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_ADDRESS): vol.In(titles), + }, + ), + ) diff --git a/homeassistant/components/airthings_ble/const.py b/homeassistant/components/airthings_ble/const.py new file mode 100644 index 00000000000..96372919e70 --- /dev/null +++ b/homeassistant/components/airthings_ble/const.py @@ -0,0 +1,9 @@ +"""Constants for Airthings BLE.""" + +DOMAIN = "airthings_ble" +MFCT_ID = 820 + +VOLUME_BECQUEREL = "Bq/m³" +VOLUME_PICOCURIE = "pCi/L" + +DEFAULT_SCAN_INTERVAL = 300 diff --git a/homeassistant/components/airthings_ble/manifest.json b/homeassistant/components/airthings_ble/manifest.json new file mode 100644 index 00000000000..dca2dbbb562 --- /dev/null +++ b/homeassistant/components/airthings_ble/manifest.json @@ -0,0 +1,15 @@ +{ + "domain": "airthings_ble", + "name": "Airthings BLE", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/airthings_ble", + "requirements": ["airthings-ble==0.5.2"], + "dependencies": ["bluetooth"], + "codeowners": ["@vincegio"], + "iot_class": "local_polling", + "bluetooth": [ + { + "manufacturer_id": 820 + } + ] +} diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py new file mode 100644 index 00000000000..0f0ca2e4af5 --- /dev/null +++ b/homeassistant/components/airthings_ble/sensor.py @@ -0,0 +1,185 @@ +"""Support for airthings ble sensors.""" +from __future__ import annotations + +import logging + +from airthings_ble import AirthingsDevice + +from homeassistant import config_entries +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_BILLION, + CONCENTRATION_PARTS_PER_MILLION, + LIGHT_LUX, + PERCENTAGE, + PRESSURE_MBAR, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN, VOLUME_BECQUEREL, VOLUME_PICOCURIE + +_LOGGER = logging.getLogger(__name__) + +SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = { + "radon_1day_avg": SensorEntityDescription( + key="radon_1day_avg", + native_unit_of_measurement=VOLUME_BECQUEREL, + name="Radon 1-day average", + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:radioactive", + ), + "radon_longterm_avg": SensorEntityDescription( + key="radon_longterm_avg", + native_unit_of_measurement=VOLUME_BECQUEREL, + name="Radon longterm average", + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:radioactive", + ), + "radon_1day_level": SensorEntityDescription( + key="radon_1day_level", + name="Radon 1-day level", + icon="mdi:radioactive", + ), + "radon_longterm_level": SensorEntityDescription( + key="radon_longterm_level", + name="Radon longterm level", + icon="mdi:radioactive", + ), + "temperature": SensorEntityDescription( + key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + name="Temperature", + ), + "humidity": SensorEntityDescription( + key="humidity", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + name="Humidity", + ), + "pressure": SensorEntityDescription( + key="pressure", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=PRESSURE_MBAR, + name="Pressure", + ), + "battery": SensorEntityDescription( + key="battery", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + name="Battery", + ), + "co2": SensorEntityDescription( + key="co2", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + name="co2", + ), + "voc": SensorEntityDescription( + key="voc", + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + name="VOC", + icon="mdi:cloud", + ), + "illuminance": SensorEntityDescription( + key="illuminance", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=LIGHT_LUX, + name="Illuminance", + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Airthings BLE sensors.""" + is_metric = hass.config.units.is_metric + + coordinator: DataUpdateCoordinator[AirthingsDevice] = hass.data[DOMAIN][ + entry.entry_id + ] + + # we need to change some units + sensors_mapping = SENSORS_MAPPING_TEMPLATE.copy() + if not is_metric: + for val in sensors_mapping.values(): + if val.native_unit_of_measurement is not VOLUME_BECQUEREL: + continue + val.native_unit_of_measurement = VOLUME_PICOCURIE + + entities = [] + _LOGGER.debug("got sensors: %s", coordinator.data.sensors) + for sensor_type, sensor_value in coordinator.data.sensors.items(): + if sensor_type not in sensors_mapping: + _LOGGER.debug( + "Unknown sensor type detected: %s, %s", + sensor_type, + sensor_value, + ) + continue + entities.append( + AirthingsSensor(coordinator, coordinator.data, sensors_mapping[sensor_type]) + ) + + async_add_entities(entities) + + +class AirthingsSensor( + CoordinatorEntity[DataUpdateCoordinator[AirthingsDevice]], SensorEntity +): + """Airthings BLE sensors for the device.""" + + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_has_entity_name = True + + def __init__( + self, + coordinator: DataUpdateCoordinator, + airthings_device: AirthingsDevice, + entity_description: SensorEntityDescription, + ) -> None: + """Populate the airthings entity with relevant data.""" + super().__init__(coordinator) + self.entity_description = entity_description + + name = f"{airthings_device.name} {airthings_device.identifier}" + + self._attr_unique_id = f"{name}_{entity_description.key}" + + self._id = airthings_device.address + self._attr_device_info = DeviceInfo( + connections={ + ( + CONNECTION_BLUETOOTH, + airthings_device.address, + ) + }, + name=name, + manufacturer="Airthings", + hw_version=airthings_device.hw_version, + sw_version=airthings_device.sw_version, + ) + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + return self.coordinator.data.sensors[self.entity_description.key] diff --git a/homeassistant/components/airthings_ble/strings.json b/homeassistant/components/airthings_ble/strings.json new file mode 100644 index 00000000000..1cfc4ccd592 --- /dev/null +++ b/homeassistant/components/airthings_ble/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/components/airthings_ble/translations/en.json b/homeassistant/components/airthings_ble/translations/en.json new file mode 100644 index 00000000000..d24df64f135 --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 3036f691f00..cffcac7558c 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -5,6 +5,10 @@ To update, run python3 -m script.hassfest from __future__ import annotations BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ + { + "domain": "airthings_ble", + "manufacturer_id": 820, + }, { "domain": "bluemaestro", "manufacturer_id": 307, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ba6c76d329a..98f1d3ab7f8 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -16,6 +16,7 @@ FLOWS = { "airly", "airnow", "airthings", + "airthings_ble", "airtouch4", "airvisual", "airzone", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 75e0296f3c8..5a97ffa2dd0 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -71,9 +71,19 @@ "name": "AirNow" }, "airthings": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Airthings" + "name": "Airthings", + "integrations": { + "airthings": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Airthings" + }, + "airthings_ble": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Airthings BLE" + } + } }, "airtouch4": { "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index e0e7f4261ea..db76ee15df4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -293,6 +293,9 @@ aioymaps==1.2.2 # homeassistant.components.airly airly==1.1.0 +# homeassistant.components.airthings_ble +airthings-ble==0.5.2 + # homeassistant.components.airthings airthings_cloud==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf7b95efd29..d2c3c22a590 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -268,6 +268,9 @@ aioymaps==1.2.2 # homeassistant.components.airly airly==1.1.0 +# homeassistant.components.airthings_ble +airthings-ble==0.5.2 + # homeassistant.components.airthings airthings_cloud==0.1.0 diff --git a/tests/components/airthings_ble/__init__.py b/tests/components/airthings_ble/__init__.py new file mode 100644 index 00000000000..c6b59e02c15 --- /dev/null +++ b/tests/components/airthings_ble/__init__.py @@ -0,0 +1,99 @@ +"""Tests for the Airthings BLE integration.""" +from typing import Union +from unittest.mock import patch + +from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak + + +def patch_async_setup_entry(return_value=True): + """Patch async setup entry to return True.""" + return patch( + "homeassistant.components.airthings_ble.async_setup_entry", + return_value=return_value, + ) + + +def patch_async_ble_device_from_address( + return_value: Union[BluetoothServiceInfoBleak, None] +): + """Patch async ble device from address to return a given value.""" + return patch( + "homeassistant.components.bluetooth.async_ble_device_from_address", + return_value=return_value, + ) + + +def patch_airthings_ble(return_value=AirthingsDevice, side_effect=None): + """Patch airthings-ble device fetcher with given values and effects.""" + return patch.object( + AirthingsBluetoothDeviceData, + "update_device", + return_value=return_value, + side_effect=side_effect, + ) + + +WAVE_SERVICE_INFO = BluetoothServiceInfoBleak( + name="cc-cc-cc-cc-cc-cc", + address="cc:cc:cc:cc:cc:cc", + rssi=-61, + manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"}, + service_data={}, + service_uuids=["b42e1c08-ade7-11e4-89d3-123b93f75cba"], + source="local", + device=BLEDevice( + "cc:cc:cc:cc:cc:cc", + "cc-cc-cc-cc-cc-cc", + ), + advertisement=AdvertisementData( + manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"}, + service_uuids=["b42e1c08-ade7-11e4-89d3-123b93f75cba"], + ), + connectable=True, + time=0, +) + +UNKNOWN_SERVICE_INFO = BluetoothServiceInfoBleak( + name="unknown", + address="00:cc:cc:cc:cc:cc", + rssi=-61, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + device=BLEDevice( + "cc:cc:cc:cc:cc:cc", + "unknown", + ), + advertisement=AdvertisementData( + manufacturer_data={}, + service_uuids=[], + ), + connectable=True, + time=0, +) + +WAVE_DEVICE_INFO = AirthingsDevice( + hw_version="REV A", + sw_version="G-BLE-1.5.3-master+0", + name="Airthings Wave+", + identifier="123456", + sensors={ + "illuminance": 25, + "battery": 85, + "humidity": 60.0, + "radon_1day_avg": 30, + "radon_longterm_avg": 30, + "temperature": 21.0, + "co2": 500.0, + "voc": 155.0, + "radon_1day_level": "very low", + "radon_longterm_level": "very low", + "pressure": 1020, + }, + address="cc:cc:cc:cc:cc:cc", +) diff --git a/tests/components/airthings_ble/conftest.py b/tests/components/airthings_ble/conftest.py new file mode 100644 index 00000000000..3df082c4361 --- /dev/null +++ b/tests/components/airthings_ble/conftest.py @@ -0,0 +1,8 @@ +"""Define fixtures available for all tests.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/airthings_ble/test_config_flow.py b/tests/components/airthings_ble/test_config_flow.py new file mode 100644 index 00000000000..ddddcdbc94a --- /dev/null +++ b/tests/components/airthings_ble/test_config_flow.py @@ -0,0 +1,194 @@ +"""Test the Airthings BLE config flow.""" +from unittest.mock import patch + +from airthings_ble import AirthingsDevice +from bleak import BleakError + +from homeassistant.components.airthings_ble.const import DOMAIN +from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER +from homeassistant.const import CONF_ADDRESS +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import ( + UNKNOWN_SERVICE_INFO, + WAVE_DEVICE_INFO, + WAVE_SERVICE_INFO, + patch_airthings_ble, + patch_async_ble_device_from_address, + patch_async_setup_entry, +) + +from tests.common import MockConfigEntry + + +async def test_bluetooth_discovery(hass: HomeAssistant): + """Test discovery via bluetooth with a valid device.""" + with patch_async_ble_device_from_address(WAVE_SERVICE_INFO): + with patch_airthings_ble( + AirthingsDevice(name="Airthings Wave+", identifier="123456") + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WAVE_SERVICE_INFO, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + assert result["description_placeholders"] == {"name": "Airthings Wave+ (123456)"} + + with patch_async_setup_entry(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"not": "empty"} + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Airthings Wave+ (123456)" + assert result["result"].unique_id == "cc:cc:cc:cc:cc:cc" + + +async def test_bluetooth_discovery_no_BLEDevice(hass: HomeAssistant): + """Test discovery via bluetooth but there's no BLEDevice.""" + with patch_async_ble_device_from_address(None): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WAVE_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + +async def test_bluetooth_discovery_airthings_ble_update_failed( + hass: HomeAssistant, +): + """Test discovery via bluetooth but there's an exception from airthings-ble.""" + for loop in [(Exception(), "unknown"), (BleakError(), "cannot_connect")]: + exc, reason = loop + with patch_async_ble_device_from_address(WAVE_SERVICE_INFO): + with patch_airthings_ble(side_effect=exc): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WAVE_SERVICE_INFO, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == reason + + +async def test_bluetooth_discovery_already_setup(hass: HomeAssistant): + """Test discovery via bluetooth with a valid device when already setup.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="cc:cc:cc:cc:cc:cc", + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WAVE_DEVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_user_setup(hass: HomeAssistant): + """Test the user initiated form.""" + with patch( + "homeassistant.components.airthings_ble.config_flow.async_discovered_service_info", + return_value=[WAVE_SERVICE_INFO], + ): + with patch_async_ble_device_from_address(WAVE_SERVICE_INFO): + with patch_airthings_ble( + AirthingsDevice(name="Airthings Wave+", identifier="123456") + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] is None + assert result["data_schema"] is not None + schema = result["data_schema"].schema + + assert schema.get(CONF_ADDRESS).container == { + "cc:cc:cc:cc:cc:cc": "Airthings Wave+ (123456)" + } + + with patch( + "homeassistant.components.airthings_ble.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_ADDRESS: "cc:cc:cc:cc:cc:cc"} + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Airthings Wave+ (123456)" + assert result["result"].unique_id == "cc:cc:cc:cc:cc:cc" + + +async def test_user_setup_no_device(hass: HomeAssistant): + """Test the user initiated form without any device detected.""" + with patch( + "homeassistant.components.airthings_ble.config_flow.async_discovered_service_info", + return_value=[], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_user_setup_existing_and_unknown_device(hass: HomeAssistant): + """Test the user initiated form with existing devices and unknown ones.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="cc:cc:cc:cc:cc:cc", + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.airthings_ble.config_flow.async_discovered_service_info", + return_value=[UNKNOWN_SERVICE_INFO, WAVE_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_user_setup_unknown_error(hass: HomeAssistant): + """Test the user initiated form with an unknown error.""" + with patch( + "homeassistant.components.airthings_ble.config_flow.async_discovered_service_info", + return_value=[WAVE_SERVICE_INFO], + ): + with patch_async_ble_device_from_address(WAVE_SERVICE_INFO): + with patch_airthings_ble(None, Exception()): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "unknown" + + +async def test_user_setup_unable_to_connect(hass: HomeAssistant): + """Test the user initiated form with a device that's failing connection.""" + with patch( + "homeassistant.components.airthings_ble.config_flow.async_discovered_service_info", + return_value=[WAVE_SERVICE_INFO], + ): + with patch_async_ble_device_from_address(WAVE_SERVICE_INFO): + with patch_airthings_ble(side_effect=BleakError("An error")): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "cannot_connect" From 21b078eeb7db17b16517775fcdaa7df8f2372565 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 30 Sep 2022 00:42:29 +0000 Subject: [PATCH 043/985] [ci skip] Translation update --- .../airthings_ble/translations/de.json | 23 ++++++ .../airthings_ble/translations/en.json | 4 +- .../airthings_ble/translations/fr.json | 23 ++++++ .../airthings_ble/translations/pt-BR.json | 23 ++++++ .../components/apcupsd/translations/ca.json | 3 +- .../components/apcupsd/translations/fr.json | 19 +++++ .../components/apcupsd/translations/hu.json | 26 +++++++ .../components/apcupsd/translations/tr.json | 26 +++++++ .../components/bayesian/translations/ca.json | 10 +++ .../components/bayesian/translations/de.json | 12 +++ .../components/bayesian/translations/en.json | 20 ++--- .../components/bayesian/translations/es.json | 12 +++ .../components/bayesian/translations/id.json | 12 +++ .../components/bayesian/translations/pl.json | 12 +++ .../bayesian/translations/pt-BR.json | 12 +++ .../components/bayesian/translations/tr.json | 12 +++ .../components/bluetooth/translations/et.json | 6 ++ .../components/bluetooth/translations/tr.json | 6 ++ .../components/braviatv/translations/et.json | 2 +- .../components/braviatv/translations/tr.json | 12 ++- .../dsmr_reader/translations/ca.json | 5 ++ .../dsmr_reader/translations/tr.json | 18 +++++ .../components/ezviz/translations/et.json | 2 +- .../components/ezviz/translations/fr.json | 10 +-- .../components/ezviz/translations/hu.json | 4 +- .../components/ezviz/translations/tr.json | 4 +- .../forked_daapd/translations/de.json | 14 ++-- .../forked_daapd/translations/en.json | 14 ++-- .../forked_daapd/translations/es.json | 14 ++-- .../forked_daapd/translations/et.json | 14 ++-- .../forked_daapd/translations/hu.json | 14 ++-- .../forked_daapd/translations/id.json | 14 ++-- .../forked_daapd/translations/pl.json | 14 ++-- .../forked_daapd/translations/pt-BR.json | 14 ++-- .../forked_daapd/translations/tr.json | 14 ++-- .../forked_daapd/translations/zh-Hant.json | 14 ++-- .../garages_amsterdam/translations/pl.json | 2 +- .../google_sheets/translations/tr.json | 35 +++++++++ .../components/guardian/translations/en.json | 11 +++ .../components/guardian/translations/et.json | 4 +- .../components/guardian/translations/tr.json | 17 ++++- .../components/ibeacon/translations/et.json | 23 ++++++ .../components/ibeacon/translations/tr.json | 23 ++++++ .../components/kegtron/translations/et.json | 3 + .../components/kegtron/translations/tr.json | 22 ++++++ .../keymitt_ble/translations/tr.json | 27 +++++++ .../components/lametric/translations/tr.json | 3 +- .../components/lidarr/translations/bg.json | 3 +- .../components/lidarr/translations/et.json | 42 +++++++++++ .../components/lidarr/translations/tr.json | 42 +++++++++++ .../litterrobot/translations/sensor.tr.json | 3 + .../litterrobot/translations/tr.json | 6 ++ .../components/moon/translations/ca.json | 6 ++ .../components/moon/translations/tr.json | 6 ++ .../nibe_heatpump/translations/tr.json | 25 +++++++ .../components/openuv/translations/tr.json | 10 +++ .../components/radarr/translations/bg.json | 3 +- .../components/radarr/translations/tr.json | 48 ++++++++++++ .../rainmachine/translations/en.json | 13 ++++ .../rainmachine/translations/tr.json | 13 ++++ .../components/roomba/translations/hu.json | 4 +- .../components/roomba/translations/tr.json | 4 +- .../components/season/translations/tr.json | 6 ++ .../components/sensor/translations/et.json | 6 +- .../components/sensor/translations/hu.json | 12 ++- .../components/sensor/translations/id.json | 6 +- .../components/sensor/translations/tr.json | 12 ++- .../components/shelly/translations/tr.json | 8 ++ .../simplisafe/translations/tr.json | 6 ++ .../components/switchbee/translations/tr.json | 32 ++++++++ .../components/tasmota/translations/et.json | 10 +++ .../components/tasmota/translations/tr.json | 10 +++ .../components/tautulli/translations/tr.json | 1 + .../components/uptime/translations/tr.json | 6 ++ .../volvooncall/translations/et.json | 1 + .../volvooncall/translations/tr.json | 1 + .../components/zha/translations/et.json | 75 ++++++++++++++++++- .../components/zha/translations/hu.json | 2 + .../components/zha/translations/pl.json | 2 + .../components/zha/translations/pt-BR.json | 2 + .../components/zha/translations/tr.json | 2 + .../components/zha/translations/zh-Hant.json | 2 + .../components/zwave_js/translations/et.json | 6 ++ .../components/zwave_js/translations/tr.json | 6 ++ 84 files changed, 952 insertions(+), 118 deletions(-) create mode 100644 homeassistant/components/airthings_ble/translations/de.json create mode 100644 homeassistant/components/airthings_ble/translations/fr.json create mode 100644 homeassistant/components/airthings_ble/translations/pt-BR.json create mode 100644 homeassistant/components/apcupsd/translations/fr.json create mode 100644 homeassistant/components/apcupsd/translations/hu.json create mode 100644 homeassistant/components/apcupsd/translations/tr.json create mode 100644 homeassistant/components/bayesian/translations/ca.json create mode 100644 homeassistant/components/bayesian/translations/de.json create mode 100644 homeassistant/components/bayesian/translations/es.json create mode 100644 homeassistant/components/bayesian/translations/id.json create mode 100644 homeassistant/components/bayesian/translations/pl.json create mode 100644 homeassistant/components/bayesian/translations/pt-BR.json create mode 100644 homeassistant/components/bayesian/translations/tr.json create mode 100644 homeassistant/components/dsmr_reader/translations/tr.json create mode 100644 homeassistant/components/google_sheets/translations/tr.json create mode 100644 homeassistant/components/ibeacon/translations/et.json create mode 100644 homeassistant/components/ibeacon/translations/tr.json create mode 100644 homeassistant/components/kegtron/translations/tr.json create mode 100644 homeassistant/components/keymitt_ble/translations/tr.json create mode 100644 homeassistant/components/lidarr/translations/et.json create mode 100644 homeassistant/components/lidarr/translations/tr.json create mode 100644 homeassistant/components/nibe_heatpump/translations/tr.json create mode 100644 homeassistant/components/radarr/translations/tr.json create mode 100644 homeassistant/components/switchbee/translations/tr.json diff --git a/homeassistant/components/airthings_ble/translations/de.json b/homeassistant/components/airthings_ble/translations/de.json new file mode 100644 index 00000000000..0368cb1dd4e --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/en.json b/homeassistant/components/airthings_ble/translations/en.json index d24df64f135..245f0fecd2c 100644 --- a/homeassistant/components/airthings_ble/translations/en.json +++ b/homeassistant/components/airthings_ble/translations/en.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", - "no_devices_found": "No devices found on the network" + "cannot_connect": "Failed to connect", + "no_devices_found": "No devices found on the network", + "unknown": "Unexpected error" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/airthings_ble/translations/fr.json b/homeassistant/components/airthings_ble/translations/fr.json new file mode 100644 index 00000000000..89920f2b345 --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "cannot_connect": "\u00c9chec de connexion", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "unknown": "Erreur inattendue" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/pt-BR.json b/homeassistant/components/airthings_ble/translations/pt-BR.json new file mode 100644 index 00000000000..a5093f346c1 --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "cannot_connect": "Falhou ao conectar", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "unknown": "Erro inesperado" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Saiba como funciona" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/ca.json b/homeassistant/components/apcupsd/translations/ca.json index 414cfb55ce6..bd4f7ee1826 100644 --- a/homeassistant/components/apcupsd/translations/ca.json +++ b/homeassistant/components/apcupsd/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", + "no_status": "Amfitri\u00f3 no ha informat d'estat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3" diff --git a/homeassistant/components/apcupsd/translations/fr.json b/homeassistant/components/apcupsd/translations/fr.json new file mode 100644 index 00000000000..a60eb14fafd --- /dev/null +++ b/homeassistant/components/apcupsd/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "no_status": "Aucun \u00e9tat n'est signal\u00e9 par H\u00f4te" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/hu.json b/homeassistant/components/apcupsd/translations/hu.json new file mode 100644 index 00000000000..365050da78f --- /dev/null +++ b/homeassistant/components/apcupsd/translations/hu.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "no_status": "Nincs \u00e1llapotjelent\u00e9s" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "port": "Port" + }, + "description": "Adja meg azt az g\u00e9p c\u00edm\u00e9t \u00e9s a portot, amelyen az apcupsd fut." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Az APC UPS d\u00e9mon konfigur\u00e1l\u00e1sa YAML haszn\u00e1lat\u00e1val elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el az APC UPS Daemon YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "Az APC UPS Daemon YAML konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/tr.json b/homeassistant/components/apcupsd/translations/tr.json new file mode 100644 index 00000000000..cae36e5752f --- /dev/null +++ b/homeassistant/components/apcupsd/translations/tr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "no_status": "Sunucu herhangi bir durum bildirilmedi" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Sunucu", + "port": "Port" + }, + "description": "apcupsd NIS'nin sunuldu\u011fu ana bilgisayar\u0131 ve ba\u011flant\u0131 noktas\u0131n\u0131 girin." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "APC UPS Daemon'un YAML kullan\u0131larak yap\u0131land\u0131r\u0131lmas\u0131 kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. \n\n APC UPS Daemon YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "APC UPS Daemon YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bayesian/translations/ca.json b/homeassistant/components/bayesian/translations/ca.json new file mode 100644 index 00000000000..45c96135eb7 --- /dev/null +++ b/homeassistant/components/bayesian/translations/ca.json @@ -0,0 +1,10 @@ +{ + "issues": { + "manual_migration": { + "title": "Es necessita una correcci\u00f3 manual YAML per a Bayesian" + }, + "no_prob_given_false": { + "title": "Es necessita afegir configuraci\u00f3 manual YAML per a Bayesian" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bayesian/translations/de.json b/homeassistant/components/bayesian/translations/de.json new file mode 100644 index 00000000000..2c3cfa28f5a --- /dev/null +++ b/homeassistant/components/bayesian/translations/de.json @@ -0,0 +1,12 @@ +{ + "issues": { + "manual_migration": { + "description": "Die Bayes'sche Integration aktualisiert nun auch die Wahrscheinlichkeit, wenn das beobachtete \u201eto_state\u201c, \u201eabove\u201c, \u201ebelow\u201c oder \u201evalue_template\u201c zu \u201eFalse\u201c und nicht nur zu \u201eTrue\u201c ausgewertet wird. Es ist also nicht l\u00e4nger erforderlich, doppelte, komplement\u00e4re Eintr\u00e4ge f\u00fcr jeden bin\u00e4ren Zustand zu haben. Bitte entferne den gespiegelten Eintrag f\u00fcr ` {entity} `.", + "title": "Manuelle YAML-Korrektur f\u00fcr Bayes erforderlich" + }, + "no_prob_given_false": { + "description": "In der Bayes'schen Integration ist `prob_given_false` jetzt eine erforderliche Konfigurationsvariable, da es keine mathematische Begr\u00fcndung f\u00fcr den vorherigen Standardwert gab. Bitte f\u00fcge dies deiner `configuration.yml` f\u00fcr `bayesian/ {entity} ` hinzu. Diese Beobachtungen werden ignoriert, bis du dies tust.", + "title": "Manuelle YAML-Erg\u00e4nzung f\u00fcr Bayes erforderlich" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bayesian/translations/en.json b/homeassistant/components/bayesian/translations/en.json index f95e153d986..e408467205c 100644 --- a/homeassistant/components/bayesian/translations/en.json +++ b/homeassistant/components/bayesian/translations/en.json @@ -1,12 +1,12 @@ { - "issues": { - "manual_migration": { - "description": "The Bayesian integration now also updates the probability if the observed `to_state`, `above`, `below`, or `value_template` evaluates to `False` rather than only `True`. So it is no longer required to have duplicate, complementary entries for each binary state. Please remove the mirrored entry for `{entity}`.", - "title": "Manual YAML fix required for Bayesian" - }, - "no_prob_given_false": { - "description": "In the Bayesian integration `prob_given_false` is now a required configuration variable as there was no mathematical rationale for the previous default value. Please add this to your `configuration.yml` for `bayesian/{entity}`. These observations will be ignored until you do.", - "title": "Manual YAML addition required for Bayesian" + "issues": { + "manual_migration": { + "description": "The Bayesian integration now also updates the probability if the observed `to_state`, `above`, `below`, or `value_template` evaluates to `False` rather than only `True`. So it is no longer required to have duplicate, complementary entries for each binary state. Please remove the mirrored entry for `{entity}`.", + "title": "Manual YAML fix required for Bayesian" + }, + "no_prob_given_false": { + "description": "In the Bayesian integration `prob_given_false` is now a required configuration variable as there was no mathematical rationale for the previous default value. Please add this to your `configuration.yml` for `bayesian/{entity}`. These observations will be ignored until you do.", + "title": "Manual YAML addition required for Bayesian" + } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/bayesian/translations/es.json b/homeassistant/components/bayesian/translations/es.json new file mode 100644 index 00000000000..dd38daee74b --- /dev/null +++ b/homeassistant/components/bayesian/translations/es.json @@ -0,0 +1,12 @@ +{ + "issues": { + "manual_migration": { + "description": "La integraci\u00f3n Bayesian ahora tambi\u00e9n actualiza la probabilidad si el 'to_state', 'above', 'below' o 'value_template' observado se eval\u00faa como 'False' en lugar de solo 'True'. Por lo tanto ya no es necesario tener entradas complementarias duplicadas para cada estado binario. Por favor, elimina la entrada duplicada para `{entity}`.", + "title": "Es necesario corregir manualmente el YAML para Bayesian" + }, + "no_prob_given_false": { + "description": "En la integraci\u00f3n Bayesian `prob_given_false` ahora es una variable de configuraci\u00f3n requerida ya que no hab\u00eda una justificaci\u00f3n matem\u00e1tica para el valor predeterminado anterior. Por favor, a\u00f1ade esto a tu `configuration.yml` para `bayesian/{entity}`. Estas observaciones se ignorar\u00e1n hasta que lo hagas.", + "title": "Es necesario realizar una adici\u00f3n manual al YAML de Bayesian" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bayesian/translations/id.json b/homeassistant/components/bayesian/translations/id.json new file mode 100644 index 00000000000..6c9673d4a34 --- /dev/null +++ b/homeassistant/components/bayesian/translations/id.json @@ -0,0 +1,12 @@ +{ + "issues": { + "manual_migration": { + "description": "Integrasi Bayesian sekarang juga memperbarui probabilitas jika nilai `to_state`, `above`, `below`, atau `value_template` yang diamati dievaluasi menjadi `False` dan bukan hanya `True`. Jadi, tidak lagi diperlukan duplikat entri pelengkap untuk setiap status biner. Hapus entri cerminan untuk `{entity}`.", + "title": "Perbaikan YAML manual diperlukan untuk integrasi Bayesian" + }, + "no_prob_given_false": { + "description": "Pada integrasi Bayesian nilai `prob_given_false` sekarang merupakan variabel konfigurasi yang diperlukan karena tidak ada alasan matematis untuk nilai default sebelumnya. Tambahkan ini ke `configuration.yml` untuk `bayesian/{entity}`. Pengamatan ini akan diabaikan hingga Anda melakukannya.", + "title": "Penambahan YAML manual diperlukan untuk integrasi Bayesian" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bayesian/translations/pl.json b/homeassistant/components/bayesian/translations/pl.json new file mode 100644 index 00000000000..f9dfa69a457 --- /dev/null +++ b/homeassistant/components/bayesian/translations/pl.json @@ -0,0 +1,12 @@ +{ + "issues": { + "manual_migration": { + "description": "Integracja bayesowska aktualizuje teraz r\u00f3wnie\u017c prawdopodobie\u0144stwo, je\u015bli obserwowane warto\u015bci \u201eto_state\u201d, \u201eabove\u201d, \u201ebelow\u201d lub \u201evalue_template\u201d maj\u0105 warto\u015b\u0107 \u201eFalse\u201d, a nie tylko \u201eTrue\u201d. Dzi\u0119ki temu nie jest ju\u017c wymagane posiadanie zduplikowanych, uzupe\u0142niaj\u0105cych si\u0119 wpis\u00f3w dla ka\u017cdego stanu binarnego. Usu\u0144 lustrzany wpis dla `{entity}`.", + "title": "Wymagana r\u0119czna poprawa wpisu YAML dla integracji bayesowskiej" + }, + "no_prob_given_false": { + "description": "W integracji bayesowskiej `prob_given_false` jest teraz wymagan\u0105 zmienn\u0105 konfiguracyjn\u0105, poniewa\u017c nie by\u0142o matematycznego uzasadnienia dla poprzedniej warto\u015bci domy\u015blnej. Prosz\u0119 doda\u0107 to do pliku `configuration.yml` dla `bayesian/{entity}`. Te obserwacje b\u0119d\u0105 ignorowane, dop\u00f3ki tego nie zrobisz.", + "title": "Wymagane r\u0119czne dodanie wpisu YAML dla integracji bayesowskiej" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bayesian/translations/pt-BR.json b/homeassistant/components/bayesian/translations/pt-BR.json new file mode 100644 index 00000000000..f0e758e40a9 --- /dev/null +++ b/homeassistant/components/bayesian/translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "issues": { + "manual_migration": { + "description": "A integra\u00e7\u00e3o Bayesiana agora tamb\u00e9m atualiza a probabilidade se o observado `to_state`, `above`, `below` ou `value_template` for avaliado como `False` em vez de apenas `True`. Portanto, n\u00e3o \u00e9 mais necess\u00e1rio ter entradas duplicadas e complementares para cada estado bin\u00e1rio. Por favor, remova a entrada espelhada para `{entity}`.", + "title": "Corre\u00e7\u00e3o manual de YAML \u00e9 necess\u00e1ria para Bayesian" + }, + "no_prob_given_false": { + "description": "Na integra\u00e7\u00e3o Bayesiana, `prob_given_false` agora \u00e9 uma vari\u00e1vel de configura\u00e7\u00e3o necess\u00e1ria, pois n\u00e3o havia l\u00f3gica matem\u00e1tica para o valor padr\u00e3o anterior. Por favor, adicione isso ao seu `configuration.yml` para `bayesian/ {entity} `. Essas observa\u00e7\u00f5es ser\u00e3o ignoradas at\u00e9 que voc\u00ea o fa\u00e7a.", + "title": "Adi\u00e7\u00e3o YAML manual necess\u00e1ria para Bayesian" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bayesian/translations/tr.json b/homeassistant/components/bayesian/translations/tr.json new file mode 100644 index 00000000000..976e1d15b94 --- /dev/null +++ b/homeassistant/components/bayesian/translations/tr.json @@ -0,0 +1,12 @@ +{ + "issues": { + "manual_migration": { + "description": "Bayesian entegrasyonu art\u0131k, g\u00f6zlemlenen \"durum\", \"yukar\u0131da\", \"a\u015fa\u011f\u0131da\" veya \"de\u011fer_\u015fablonu\" yaln\u0131zca \"Do\u011fru\" yerine \"Yanl\u0131\u015f\" olarak de\u011ferlendirilirse olas\u0131l\u0131\u011f\u0131 da g\u00fcnceller. Bu nedenle, art\u0131k her ikili durum i\u00e7in yinelenen, tamamlay\u0131c\u0131 giri\u015flere sahip olmak gerekmez. L\u00fctfen ` {entity} ` i\u00e7in yans\u0131t\u0131lm\u0131\u015f giri\u015fi kald\u0131r\u0131n.", + "title": "Bayesian i\u00e7in manuel YAML d\u00fczeltmesi gerekli" + }, + "no_prob_given_false": { + "description": "Bayesian entegrasyonunda 'prob_given_false', \u00f6nceki varsay\u0131lan de\u011fer i\u00e7in matematiksel bir gerek\u00e7e olmad\u0131\u011f\u0131 i\u00e7in art\u0131k gerekli bir yap\u0131land\u0131rma de\u011fi\u015fkenidir. L\u00fctfen bunu \"bayesian/ {entity} \" i\u00e7in \"configuration.yml\" dosyan\u0131za ekleyin. Bu g\u00f6zlemler siz yapana kadar yok say\u0131lacakt\u0131r.", + "title": "Bayesian i\u00e7in manuel YAML eklemesi gerekiyor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/et.json b/homeassistant/components/bluetooth/translations/et.json index 5ada590decf..5579ab5b62d 100644 --- a/homeassistant/components/bluetooth/translations/et.json +++ b/homeassistant/components/bluetooth/translations/et.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "Bluetoothi t\u00f6\u00f6kindluse ja j\u00f5udluse parandamiseks soovitame tungivalt v\u00e4rskendada Home Assistanti operatsioonis\u00fcsteemi versioonile 9.0 v\u00f5i uuemale.", + "title": "Uuenda operatsioonis\u00fcsteemile Home Assistant 9.0 v\u00f5i uuem" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/tr.json b/homeassistant/components/bluetooth/translations/tr.json index 2ffd8c80814..787bc1fed94 100644 --- a/homeassistant/components/bluetooth/translations/tr.json +++ b/homeassistant/components/bluetooth/translations/tr.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "Bluetooth g\u00fcvenilirli\u011fini ve performans\u0131n\u0131 art\u0131rmak i\u00e7in Home Assistant \u0130\u015fletim Sisteminin 9.0 veya sonraki bir s\u00fcr\u00fcm\u00fcne g\u00fcncellemenizi \u00f6nemle tavsiye ederiz.", + "title": "Home Assistant \u0130\u015fletim Sistemi 9.0 veya \u00fczeri g\u00fcncelleme" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/braviatv/translations/et.json b/homeassistant/components/braviatv/translations/et.json index 83ee91c3206..b2863001eba 100644 --- a/homeassistant/components/braviatv/translations/et.json +++ b/homeassistant/components/braviatv/translations/et.json @@ -17,7 +17,7 @@ "pin": "PIN kood", "use_psk": "PSK autentimise kasutamine" }, - "description": "Sisesta Sony Bravia teleris kuvatud PIN-kood.\n\n Kui PIN-koodi ei kuvata pead teleri Home Assistan'i sidumise t\u00fchistama. Mine: Seaded - > V\u00f5rk - > Kaugseadme seaded - > Kaugseadme registreerimise t\u00fchistamine.", + "description": "Sisestage Sony Bravia teleril n\u00e4idatud PIN-kood. \n\nKui PIN-koodi ei kuvata, peate teleril Home Assistant'i registreerimise t\u00fchistama, minge aadressile: Seaded -> Network -> Remote device settings -> Deregister remote device. \n\nPIN-koodi asemel v\u00f5ite kasutada PSK (Pre-Shared-Key). PSK on kasutaja m\u00e4\u00e4ratud salajane v\u00f5ti, mida kasutatakse juurdep\u00e4\u00e4su kontrollimiseks. See autentimismeetod on soovitatav kui stabiilsem. PSK lubamiseks teleril minge aadressil: Settings -> Network -> Home Network Setup -> IP Control. Seej\u00e4rel m\u00e4rgistage ruut \"Kasutage PSK autentimist\" ja sisestage PIN-koodi asemel PSK.", "title": "Sony Bravia TV autoriseerimine" }, "confirm": { diff --git a/homeassistant/components/braviatv/translations/tr.json b/homeassistant/components/braviatv/translations/tr.json index cf5cc45640e..edbbaa7ef31 100644 --- a/homeassistant/components/braviatv/translations/tr.json +++ b/homeassistant/components/braviatv/translations/tr.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "no_ip_control": "TV'nizde IP Kontrol\u00fc devre d\u0131\u015f\u0131 veya TV desteklenmiyor." + "no_ip_control": "TV'nizde IP Kontrol\u00fc devre d\u0131\u015f\u0131 veya TV desteklenmiyor.", + "not_bravia_device": "Cihaz bir Bravia TV de\u011fildir." }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "invalid_host": "Ge\u00e7ersiz ana bilgisayar ad\u0131 veya IP adresi", "unsupported_model": "TV modeliniz desteklenmiyor." }, "step": { "authorize": { "data": { - "pin": "PIN Kodu" + "pin": "PIN Kodu", + "use_psk": "PSK kimlik do\u011frulamas\u0131n\u0131 kullan\u0131n" }, - "description": "Sony Bravia TV'de g\u00f6sterilen PIN kodunu girin. \n\n PIN kodu g\u00f6r\u00fcnt\u00fclenmiyorsa, TV'nizde Home Assistant kayd\u0131n\u0131 iptal etmeniz gerekir, \u015furaya gidin: Ayarlar - > A\u011f - > Uzak cihaz ayarlar\u0131 - > Uzak cihaz\u0131n kayd\u0131n\u0131 iptal et.Home Assistant", + "description": "Sony Bravia TV'de g\u00f6sterilen PIN kodunu girin. \n\n PIN kodu g\u00f6r\u00fcnt\u00fclenmezse, TV'nizde Home Assistant kayd\u0131n\u0131 iptal etmeniz gerekir, \u015furaya gidin: Ayarlar - > A\u011f - > Uzak cihaz ayarlar\u0131 - > Uzak cihaz\u0131n kayd\u0131n\u0131 sil. \n\n PIN yerine PSK (\u00d6n Payla\u015f\u0131ml\u0131 Anahtar) kullanabilirsiniz. PSK, eri\u015fim kontrol\u00fc i\u00e7in kullan\u0131lan kullan\u0131c\u0131 tan\u0131ml\u0131 bir gizli anahtard\u0131r. Bu kimlik do\u011frulama y\u00f6nteminin daha kararl\u0131 olmas\u0131 \u00f6nerilir. TV'nizde PSK'y\u0131 etkinle\u015ftirmek i\u00e7in \u015furaya gidin: Ayarlar - > A\u011f - > Ev A\u011f\u0131 Kurulumu - > IP Kontrol\u00fc. Ard\u0131ndan \u00abPSK kimlik do\u011frulamas\u0131n\u0131 kullan\u00bb kutusunu i\u015faretleyin ve PIN yerine PSK'n\u0131z\u0131 girin.", "title": "Sony Bravia TV'yi yetkilendirin" }, + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + }, "user": { "data": { "host": "Ana Bilgisayar" diff --git a/homeassistant/components/dsmr_reader/translations/ca.json b/homeassistant/components/dsmr_reader/translations/ca.json index cc92e3ec9f1..901034bca2a 100644 --- a/homeassistant/components/dsmr_reader/translations/ca.json +++ b/homeassistant/components/dsmr_reader/translations/ca.json @@ -3,5 +3,10 @@ "abort": { "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuraci\u00f3 YAML de DSMR Reader est\u00e0 sent eliminada" + } } } \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/tr.json b/homeassistant/components/dsmr_reader/translations/tr.json new file mode 100644 index 00000000000..aca2ecaf6fb --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "DSMR Reader'da 'b\u00f6l\u00fcnm\u00fc\u015f konu' veri kaynaklar\u0131n\u0131 yap\u0131land\u0131rd\u0131\u011f\u0131n\u0131zdan emin olun." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "DSMR Reader'\u0131n YAML kullan\u0131larak yap\u0131land\u0131r\u0131lmas\u0131 kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. \n\n DSMR Reader YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "DSMR Okuyucu yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/et.json b/homeassistant/components/ezviz/translations/et.json index 55a6e6784c1..1897d57aa91 100644 --- a/homeassistant/components/ezviz/translations/et.json +++ b/homeassistant/components/ezviz/translations/et.json @@ -35,7 +35,7 @@ "username": "Kasutajanimi" }, "description": "M\u00e4\u00e4ra oma piirkonna URL k\u00e4sitsi", - "title": "\u00dchenduse loomine kohandatud Ezvizi URL-iga" + "title": "Loo \u00fchendus kohandatud EZVIZi URL-iga" } } }, diff --git a/homeassistant/components/ezviz/translations/fr.json b/homeassistant/components/ezviz/translations/fr.json index 475eb0bcfc7..33ff73f59d4 100644 --- a/homeassistant/components/ezviz/translations/fr.json +++ b/homeassistant/components/ezviz/translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "Le compte est d\u00e9j\u00e0 configur\u00e9", - "ezviz_cloud_account_missing": "Compte cloud Ezviz manquant. Veuillez reconfigurer le compte cloud Ezviz", + "ezviz_cloud_account_missing": "Compte cloud EZVIZ manquant. Veuillez reconfigurer le compte cloud EZVIZ", "unknown": "Erreur inattendue" }, "error": { @@ -17,8 +17,8 @@ "password": "Mot de passe", "username": "Nom d'utilisateur" }, - "description": "Entrez les informations d'identification RTSP pour la cam\u00e9ra Ezviz {serial} avec IP {ip_address}", - "title": "Cam\u00e9ra Ezviz d\u00e9couverte" + "description": "Saisissez les informations d'identification RTSP pour la cam\u00e9ra EZVIZ {serial} \u00e0 l'adresse\u00a0IP {ip_address}", + "title": "Cam\u00e9ra EZVIZ d\u00e9couverte" }, "user": { "data": { @@ -26,7 +26,7 @@ "url": "URL", "username": "Nom d'utilisateur" }, - "title": "Connectez-vous \u00e0 Ezviz Cloud" + "title": "Connectez-vous \u00e0 EZVIZ Cloud" }, "user_custom_url": { "data": { @@ -35,7 +35,7 @@ "username": "Nom d'utilisateur" }, "description": "Sp\u00e9cifiez manuellement l'URL de votre r\u00e9gion", - "title": "Connectez-vous \u00e0 l'URL Ezviz personnalis\u00e9e" + "title": "Connectez-vous \u00e0 l'URL EZVIZ personnalis\u00e9e" } } }, diff --git a/homeassistant/components/ezviz/translations/hu.json b/homeassistant/components/ezviz/translations/hu.json index 49cd4b43d08..7c9b5218f91 100644 --- a/homeassistant/components/ezviz/translations/hu.json +++ b/homeassistant/components/ezviz/translations/hu.json @@ -17,7 +17,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "\u00cdrja be az RTSP-hiteles\u00edt\u0151 adatokat az Ezviz {serial} kamer\u00e1hoz IP- {ip_address}", + "description": "\u00cdrja be az RTSP-hiteles\u00edt\u0151 adatokat az Ezviz {serial}, {ip_address} IP-c\u00edm\u0171 kamer\u00e1hoz", "title": "Felfedezett Ezviz kamera" }, "user": { @@ -35,7 +35,7 @@ "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, "description": "Adja meg k\u00e9zzel a r\u00e9gi\u00f3 URL-j\u00e9t", - "title": "Csatlakozzon az Ezviz-hez egy\u00e9ni URL" + "title": "Csatlakozzon egyedi Ezviz URL-hez" } } }, diff --git a/homeassistant/components/ezviz/translations/tr.json b/homeassistant/components/ezviz/translations/tr.json index a1ba775da7f..ecd7ce78f42 100644 --- a/homeassistant/components/ezviz/translations/tr.json +++ b/homeassistant/components/ezviz/translations/tr.json @@ -26,7 +26,7 @@ "url": "URL", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "title": "Ezviz Cloud'a ba\u011flan\u0131n" + "title": "EZVIZ Cloud'a ba\u011flan\u0131n" }, "user_custom_url": { "data": { @@ -35,7 +35,7 @@ "username": "Kullan\u0131c\u0131 Ad\u0131" }, "description": "B\u00f6lge URL'nizi manuel olarak belirtin", - "title": "\u00d6zel Ezviz URL'sine ba\u011flan\u0131n" + "title": "\u00d6zel EZVIZ URL'sine ba\u011flan\u0131n" } } }, diff --git a/homeassistant/components/forked_daapd/translations/de.json b/homeassistant/components/forked_daapd/translations/de.json index 984358f02ba..3754d70f449 100644 --- a/homeassistant/components/forked_daapd/translations/de.json +++ b/homeassistant/components/forked_daapd/translations/de.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "not_forked_daapd": "Das Ger\u00e4t ist kein Forked-Daapd-Server." + "not_forked_daapd": "Das Ger\u00e4t ist kein Owntone-Server." }, "error": { - "forbidden": "Verbindung kann nicht hergestellt werden. Bitte \u00fcberpr\u00fcfe deine forked-daapd-Netzwerkberechtigungen.", + "forbidden": "Verbindung kann nicht hergestellt werden. Bitte \u00fcberpr\u00fcfe deine Owntone-Netzwerkberechtigungen.", "unknown_error": "Unerwarteter Fehler", - "websocket_not_enabled": "Forked-Daapd-Server-Websocket nicht aktiviert.", + "websocket_not_enabled": "Owntone Server Websocket nicht aktiviert.", "wrong_host_or_port": "Verbindung konnte nicht hergestellt werden. Bitte Host und Port pr\u00fcfen.", "wrong_password": "Ung\u00fcltiges Passwort", - "wrong_server_type": "F\u00fcr die forked-daapd Integration ist ein forked-daapd Server mit der Version >= 27.0 erforderlich." + "wrong_server_type": "Die Owntone-Integration erfordert einen Owntone-Server mit Version >= 27.0." }, "flow_title": "{name} ({host})", "step": { @@ -21,7 +21,7 @@ "password": "API-Passwort (leer lassen, wenn kein Passwort vorhanden ist)", "port": "API Port" }, - "title": "Forked-Daapd-Ger\u00e4t einrichten" + "title": "Owntone Ger\u00e4t einrichten" } } }, @@ -34,8 +34,8 @@ "tts_pause_time": "Sekunden bis zur Pause vor und nach der TTS", "tts_volume": "TTS-Lautst\u00e4rke (Float im Bereich [0,1])" }, - "description": "Lege verschiedene Optionen f\u00fcr die Forked-Daapd-Integration fest.", - "title": "Konfigurieren der Forked-Daapd-Optionen" + "description": "Lege verschiedene Optionen f\u00fcr die Owntone-Integration fest.", + "title": "Konfigurieren der Owntone Optionen" } } } diff --git a/homeassistant/components/forked_daapd/translations/en.json b/homeassistant/components/forked_daapd/translations/en.json index cf7a1e5281b..b043969a5ea 100644 --- a/homeassistant/components/forked_daapd/translations/en.json +++ b/homeassistant/components/forked_daapd/translations/en.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "Device is already configured", - "not_forked_daapd": "Device is not a forked-daapd server." + "not_forked_daapd": "Device is not an Owntone server." }, "error": { - "forbidden": "Unable to connect. Please check your forked-daapd network permissions.", + "forbidden": "Unable to connect. Please check your Owntone network permissions.", "unknown_error": "Unexpected error", - "websocket_not_enabled": "forked-daapd server websocket not enabled.", + "websocket_not_enabled": "Owntone server websocket not enabled.", "wrong_host_or_port": "Unable to connect. Please check host and port.", "wrong_password": "Incorrect password.", - "wrong_server_type": "The forked-daapd integration requires a forked-daapd server with version >= 27.0." + "wrong_server_type": "The Owntone integration requires an Owntone server with version >= 27.0." }, "flow_title": "{name} ({host})", "step": { @@ -21,7 +21,7 @@ "password": "API password (leave blank if no password)", "port": "API port" }, - "title": "Set up forked-daapd device" + "title": "Set up Owntone device" } } }, @@ -34,8 +34,8 @@ "tts_pause_time": "Seconds to pause before and after TTS", "tts_volume": "TTS volume (float in range [0,1])" }, - "description": "Set various options for the forked-daapd integration.", - "title": "Configure forked-daapd options" + "description": "Set various options for the Owntone integration.", + "title": "Configure Owntone options" } } } diff --git a/homeassistant/components/forked_daapd/translations/es.json b/homeassistant/components/forked_daapd/translations/es.json index 999ec0846d5..9b5720e9795 100644 --- a/homeassistant/components/forked_daapd/translations/es.json +++ b/homeassistant/components/forked_daapd/translations/es.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "not_forked_daapd": "El dispositivo no es un servidor forked-daapd." + "not_forked_daapd": "El dispositivo no es un servidor Owntone." }, "error": { - "forbidden": "No se puede conectar. Por favor, comprueba los permisos de red de tu forked-daapd.", + "forbidden": "No se puede conectar. Por favor, comprueba tus permisos de red de Owntone.", "unknown_error": "Error inesperado", - "websocket_not_enabled": "Websocket del servidor forked-daapd no habilitado.", + "websocket_not_enabled": "Websocket del servidor Owntone no habilitado.", "wrong_host_or_port": "No se ha podido conectar. Por favor, comprueba host y puerto.", "wrong_password": "Contrase\u00f1a incorrecta.", - "wrong_server_type": "La integraci\u00f3n forked-daapd requiere un servidor forked-daapd con versi\u00f3n >= 27.0." + "wrong_server_type": "La integraci\u00f3n Owntone requiere un servidor Owntone con versi\u00f3n >= 27.0." }, "flow_title": "{name} ({host})", "step": { @@ -21,7 +21,7 @@ "password": "Contrase\u00f1a API (d\u00e9jala en blanco si no hay contrase\u00f1a)", "port": "Puerto API" }, - "title": "Configurar dispositivo forked-daapd" + "title": "Configurar dispositivo Owntone" } } }, @@ -34,8 +34,8 @@ "tts_pause_time": "Segundos para pausar antes y despu\u00e9s del TTS", "tts_volume": "Volumen TTS (decimal en el rango [0,1])" }, - "description": "Establece varias opciones para la integraci\u00f3n de forked-daapd.", - "title": "Configurar opciones de forked-daapd" + "description": "Configura varias opciones para la integraci\u00f3n Owntone.", + "title": "Configurar opciones de Owntone" } } } diff --git a/homeassistant/components/forked_daapd/translations/et.json b/homeassistant/components/forked_daapd/translations/et.json index a9413cf0cea..72e0ee77293 100644 --- a/homeassistant/components/forked_daapd/translations/et.json +++ b/homeassistant/components/forked_daapd/translations/et.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", - "not_forked_daapd": "Seade ei ole forked-daapd server." + "not_forked_daapd": "Seade ei oleOwntone server." }, "error": { - "forbidden": "Ei saa \u00fchendust. Kontrolli oma forked-daapd sidumise v\u00f5rgu\u00f5igusi.", + "forbidden": "Ei saa \u00fchendust. Kontrolli oma Owntone sidumise v\u00f5rgu\u00f5igusi.", "unknown_error": "Tundmatu viga", - "websocket_not_enabled": "forked- daapd serveri veebisoklit pole lubatud.", + "websocket_not_enabled": "Owntone serveri veebisoklit pole lubatud.", "wrong_host_or_port": "\u00dchendust ei saa luua. Palun kontrolli hosti ja porti.", "wrong_password": "Vale salas\u00f5na.", - "wrong_server_type": "Forked-daapd sidumine n\u00f5uab forked-daapd serveri versioon >= 27.0." + "wrong_server_type": "Owntone sidumine n\u00f5uab Owntone serveri versioon >= 27.0." }, "flow_title": "{name} ({host})", "step": { @@ -21,7 +21,7 @@ "password": "API salas\u00f5na (j\u00e4ta t\u00fchjaks kui salas\u00f5na puudub)", "port": "" }, - "title": "Seadista forked-daapd seade" + "title": "Seadista Owntone seade" } } }, @@ -34,8 +34,8 @@ "tts_pause_time": "Paus sekundites enne ja p\u00e4rast TTS teavitust", "tts_volume": "TTS helitugevus (ujukoma vahemikus [0-1])" }, - "description": "M\u00e4\u00e4rake forked-daapd-i sidumise erinevad valikud.", - "title": "Forked- daapd valikute seadistamine" + "description": "M\u00e4\u00e4ra Owntone sidumise erinevad valikud.", + "title": "Owntone valikute seadistamine" } } } diff --git a/homeassistant/components/forked_daapd/translations/hu.json b/homeassistant/components/forked_daapd/translations/hu.json index 51285d2f7d4..e10e4f19bd2 100644 --- a/homeassistant/components/forked_daapd/translations/hu.json +++ b/homeassistant/components/forked_daapd/translations/hu.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "not_forked_daapd": "Az eszk\u00f6z nem forked-daapd kiszolg\u00e1l\u00f3." + "not_forked_daapd": "Az eszk\u00f6z nem Owntone-kiszolg\u00e1l\u00f3." }, "error": { - "forbidden": "A csatlakoz\u00e1s sikertelen. K\u00e9rem, ellen\u0151rizze a forked-daapd h\u00e1l\u00f3zati enged\u00e9lyeket.", + "forbidden": "Nem siker\u00fclt csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze az Owntone h\u00e1l\u00f3zati enged\u00e9lyeit.", "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", - "websocket_not_enabled": "forked-daapd szerver websocket nincs enged\u00e9lyezve.", + "websocket_not_enabled": "Az Owntone server websocket nincs enged\u00e9lyezve.", "wrong_host_or_port": "A csatlakoz\u00e1s sikertelen. K\u00e9rem, ellen\u0151rizze a c\u00edmet \u00e9s a portot.", "wrong_password": "Helytelen jelsz\u00f3.", - "wrong_server_type": "A forked-daapd integr\u00e1ci\u00f3hoz forked-daapd szerver sz\u00fcks\u00e9ges, amelynek verzi\u00f3ja legal\u00e1bb 27.0." + "wrong_server_type": "Az Owntone integr\u00e1ci\u00f3hoz egy Owntone szerverre van sz\u00fcks\u00e9g, amelynek verzi\u00f3ja legal\u00e1bb 27.0." }, "flow_title": "{name} ({host})", "step": { @@ -21,7 +21,7 @@ "password": "API jelsz\u00f3 (hagyja \u00fcresen, ha nincs jelsz\u00f3)", "port": "API port" }, - "title": "\u00c1ll\u00edtsa be a forked-daapd eszk\u00f6zt" + "title": "Owntone eszk\u00f6z be\u00e1ll\u00edt\u00e1sa" } } }, @@ -34,8 +34,8 @@ "tts_pause_time": "M\u00e1sodpercek a TTS el\u0151tti \u00e9s ut\u00e1ni sz\u00fcnethez", "tts_volume": "TTS hanger\u0151 (lebeg\u0151 a [0,1] tartom\u00e1nyban)" }, - "description": "A forked-daapd integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sai.", - "title": "A forked-daapd be\u00e1ll\u00edt\u00e1sainak konfigur\u00e1l\u00e1sa" + "description": "Az Owntone integr\u00e1ci\u00f3 k\u00fcl\u00f6nb\u00f6z\u0151 be\u00e1ll\u00edt\u00e1sai.", + "title": "Owntone be\u00e1ll\u00edt\u00e1sok konfigur\u00e1l\u00e1sa" } } } diff --git a/homeassistant/components/forked_daapd/translations/id.json b/homeassistant/components/forked_daapd/translations/id.json index f57a8fb8566..1563443699c 100644 --- a/homeassistant/components/forked_daapd/translations/id.json +++ b/homeassistant/components/forked_daapd/translations/id.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", - "not_forked_daapd": "Perangkat bukan server forked-daapd." + "not_forked_daapd": "Perangkat bukan server Owntone." }, "error": { - "forbidden": "Tidak dapat terhubung. Periksa izin jaringan forked-daapd Anda.", + "forbidden": "Tidak dapat terhubung. Periksa izin jaringan Owntone Anda.", "unknown_error": "Kesalahan yang tidak diharapkan", - "websocket_not_enabled": "Websocket server forked-daapd tidak diaktifkan.", + "websocket_not_enabled": "Websocket server Owntone tidak diaktifkan.", "wrong_host_or_port": "Tidak dapat terhubung. Periksa nilai host dan port.", "wrong_password": "Kata sandi salah.", - "wrong_server_type": "Integrasi forked-daapd membutuhkan server forked-daapd dengan versi >= 27.0." + "wrong_server_type": "Integrasi Owntone membutuhkan server forked-daapd dengan versi >= 27.0." }, "flow_title": "{name} ({host})", "step": { @@ -21,7 +21,7 @@ "password": "Kata sandi API (kosongkan jika tidak ada kata sandi)", "port": "Port API" }, - "title": "Siapkan perangkat forked-daapd" + "title": "Siapkan perangkat Owntone" } } }, @@ -34,8 +34,8 @@ "tts_pause_time": "Tenggang waktu dalam detik sebelum dan setelah TTS", "tts_volume": "Volume TTS (bilangan float dalam rentang [0,1])" }, - "description": "Tentukan berbagai opsi untuk integrasi forked-daapd.", - "title": "Konfigurasikan opsi forked-daapd" + "description": "Tentukan berbagai opsi untuk integrasi Owntone.", + "title": "Konfigurasikan opsi Owntone" } } } diff --git a/homeassistant/components/forked_daapd/translations/pl.json b/homeassistant/components/forked_daapd/translations/pl.json index bb20159bd1a..b560fb1ef90 100644 --- a/homeassistant/components/forked_daapd/translations/pl.json +++ b/homeassistant/components/forked_daapd/translations/pl.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "not_forked_daapd": "Urz\u0105dzenie nie jest serwerem forked-daapd" + "not_forked_daapd": "Urz\u0105dzenie nie jest serwerem Owntone" }, "error": { - "forbidden": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia. Sprawd\u017a uprawnienia sieciowe forked-daapd.", + "forbidden": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia. Sprawd\u017a uprawnienia sieciowe Owntone.", "unknown_error": "Nieoczekiwany b\u0142\u0105d", - "websocket_not_enabled": "Websocket serwera forked-daapd nie jest w\u0142\u0105czony", + "websocket_not_enabled": "Websocket serwera Owntone nie jest w\u0142\u0105czony", "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", - "wrong_server_type": "Integracja forked-daapd wymaga serwera forked-daapd w wersji >= 27.0" + "wrong_server_type": "Integracja Owntone wymaga serwera Owntone w wersji >= 27.0" }, "flow_title": "{name} ({host})", "step": { @@ -21,7 +21,7 @@ "password": "Has\u0142o API (pozostaw puste, je\u015bli nie ma has\u0142a)", "port": "Port API" }, - "title": "Konfiguracja urz\u0105dzenia forked-daapd" + "title": "Konfiguracja urz\u0105dzenia Owntone" } } }, @@ -34,8 +34,8 @@ "tts_pause_time": "Przerwa przed i po TTS (w sekundach)", "tts_volume": "G\u0142o\u015bno\u015b\u0107 TTS (w zakresie od 0 do 1, np. 0.5 = 50%)" }, - "description": "Ustawianie r\u00f3\u017cnych opcji dla integracji forked-daapd", - "title": "Konfiguracja opcji forked-daapd" + "description": "Ustawianie r\u00f3\u017cnych opcji dla integracji Owntone", + "title": "Konfiguracja opcji Owntone" } } } diff --git a/homeassistant/components/forked_daapd/translations/pt-BR.json b/homeassistant/components/forked_daapd/translations/pt-BR.json index 1b768604bef..adf57ed7ba1 100644 --- a/homeassistant/components/forked_daapd/translations/pt-BR.json +++ b/homeassistant/components/forked_daapd/translations/pt-BR.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", - "not_forked_daapd": "O dispositivo n\u00e3o \u00e9 um servidor forked-daapd." + "not_forked_daapd": "O dispositivo n\u00e3o \u00e9 um servidor Owntone." }, "error": { - "forbidden": "Incapaz de conectar. Verifique suas permiss\u00f5es de rede forked-daapd.", + "forbidden": "Incapaz de conectar. Verifique suas permiss\u00f5es de rede Owntone.", "unknown_error": "Erro inesperado", - "websocket_not_enabled": "websocket do servidor forked-daapd n\u00e3o ativado.", + "websocket_not_enabled": "Websocket do servidor Owntone n\u00e3o ativado.", "wrong_host_or_port": "N\u00e3o foi poss\u00edvel conectar. Por favor, verifique o endere\u00e7o e a porta.", "wrong_password": "Senha incorreta.", - "wrong_server_type": "A integra\u00e7\u00e3o forked-daapd requer um servidor forked-daapd com vers\u00e3o >= 27.0." + "wrong_server_type": "A integra\u00e7\u00e3o Owntone requer um servidor Owntone com vers\u00e3o > = 27.0." }, "flow_title": "{name} ({host})", "step": { @@ -21,7 +21,7 @@ "password": "Senha da API (deixe em branco se n\u00e3o houver senha)", "port": "Porta API" }, - "title": "Configurar dispositivo forked-daapd" + "title": "Configurar dispositivo Owntone" } } }, @@ -34,8 +34,8 @@ "tts_pause_time": "Segundos para pausar antes e depois do TTS", "tts_volume": "Volume TTS (flutua\u00e7\u00e3o na faixa [0,1])" }, - "description": "Defina v\u00e1rias op\u00e7\u00f5es para a integra\u00e7\u00e3o forked-daapd.", - "title": "Configurar op\u00e7\u00f5es forked-daapd" + "description": "Defina v\u00e1rias op\u00e7\u00f5es para a integra\u00e7\u00e3o do Owntone.", + "title": "Configurar op\u00e7\u00f5es do Owntone" } } } diff --git a/homeassistant/components/forked_daapd/translations/tr.json b/homeassistant/components/forked_daapd/translations/tr.json index 6c838e93055..c9345c00ec5 100644 --- a/homeassistant/components/forked_daapd/translations/tr.json +++ b/homeassistant/components/forked_daapd/translations/tr.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "not_forked_daapd": "Cihaz, forked-daapd sunucusu de\u011fil." + "not_forked_daapd": "Cihaz bir Owntone sunucusu de\u011fil." }, "error": { - "forbidden": "Ba\u011flan\u0131lam\u0131yor. L\u00fctfen forked-daapd a\u011f izinlerinizi kontrol edin.", + "forbidden": "Ba\u011flan\u0131lam\u0131yor. L\u00fctfen Owntone a\u011f izinlerinizi kontrol edin.", "unknown_error": "Beklenmeyen hata", - "websocket_not_enabled": "forked-daapd sunucu websocket etkin de\u011fil.", + "websocket_not_enabled": "Owntone sunucusu websocket etkinle\u015ftirilmedi.", "wrong_host_or_port": "Ba\u011flan\u0131lam\u0131yor. L\u00fctfen ana bilgisayar\u0131 ve ba\u011flant\u0131 noktas\u0131n\u0131 kontrol edin.", "wrong_password": "Yanl\u0131\u015f parola.", - "wrong_server_type": "> = 27.0 s\u00fcr\u00fcm\u00fcne sahip bir forked-daapd sunucusu gerektirir." + "wrong_server_type": "Owntone entegrasyonu, > = 27.0 s\u00fcr\u00fcm\u00fcne sahip bir Owntone sunucusu gerektirir." }, "flow_title": "{name} ({host})", "step": { @@ -21,7 +21,7 @@ "password": "API parolas\u0131 (parola yoksa bo\u015f b\u0131rak\u0131n)", "port": "API Port" }, - "title": "Forked-daapd cihaz\u0131n\u0131 kurun" + "title": "Owntone cihaz\u0131n\u0131 kurun" } } }, @@ -34,8 +34,8 @@ "tts_pause_time": "TTS'den \u00f6nce ve sonra duraklatmak i\u00e7in saniyeler", "tts_volume": "TTS ses seviyesi (aral\u0131k [0,1])" }, - "description": "Forked-daapd entegrasyonu i\u00e7in \u00e7e\u015fitli se\u00e7enekleri ayarlay\u0131n.", - "title": "Forked-daapd se\u00e7eneklerini yap\u0131land\u0131r\u0131n" + "description": "Owntone entegrasyonu i\u00e7in \u00e7e\u015fitli se\u00e7enekleri ayarlay\u0131n.", + "title": "Owntone se\u00e7eneklerini yap\u0131land\u0131rma" } } } diff --git a/homeassistant/components/forked_daapd/translations/zh-Hant.json b/homeassistant/components/forked_daapd/translations/zh-Hant.json index 9d91fb93033..51963bed10f 100644 --- a/homeassistant/components/forked_daapd/translations/zh-Hant.json +++ b/homeassistant/components/forked_daapd/translations/zh-Hant.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "not_forked_daapd": "\u88dd\u7f6e\u4e26\u975e forked-daapd \u4f3a\u670d\u5668\u3002" + "not_forked_daapd": "\u88dd\u7f6e\u4e26\u975e Owntone \u4f3a\u670d\u5668\u3002" }, "error": { - "forbidden": "\u7121\u6cd5\u9023\u7dda\uff0c\u8acb\u78ba\u8a8d forked-daapd \u7db2\u8def\u6b0a\u9650\u3002", + "forbidden": "\u7121\u6cd5\u9023\u7dda\uff0c\u8acb\u78ba\u8a8d Owntone \u7db2\u8def\u6b0a\u9650\u3002", "unknown_error": "\u672a\u9810\u671f\u932f\u8aa4", - "websocket_not_enabled": "forked-daapd \u4f3a\u670d\u5668 websocket \u672a\u958b\u555f\u3002", + "websocket_not_enabled": "Owntone \u4f3a\u670d\u5668 websocket \u672a\u958b\u555f\u3002", "wrong_host_or_port": "\u7121\u6cd5\u9023\u7dda\uff0c\u8acb\u78ba\u8a8d\u4e3b\u6a5f\u8207\u901a\u8a0a\u57e0\u3002", "wrong_password": "\u5bc6\u78bc\u932f\u8aa4\u3002", - "wrong_server_type": "forked-daapd \u6574\u5408\u9700\u8981\u7248\u6b21 >= 27.0 \u7248\u4e4b forked-daapd \u4f3a\u670d\u5668\u3002" + "wrong_server_type": "Owntone \u6574\u5408\u9700\u8981\u7248\u6b21 >= 27.0 \u7248\u4e4b Owntone \u4f3a\u670d\u5668\u3002" }, "flow_title": "{name} ({host})", "step": { @@ -21,7 +21,7 @@ "password": "API \u5bc6\u78bc\uff08\u5047\u5982\u7121\u5bc6\u78bc\uff0c\u8acb\u7559\u7a7a\uff09", "port": "API \u901a\u8a0a\u57e0" }, - "title": "\u8a2d\u5b9a forked-daapd \u88dd\u7f6e" + "title": "\u8a2d\u5b9a Owntone \u88dd\u7f6e" } } }, @@ -34,8 +34,8 @@ "tts_pause_time": "\u65bc TTS \u524d\u5f8c\u66ab\u505c\u79d2\u6578", "tts_volume": "TTS \u97f3\u91cf\uff08\u6d6e\u52d5\u7bc4\u570d [0,1]\uff09" }, - "description": "\u8a2d\u5b9a forked-daapd \u6574\u5408\u9078\u9805\u3002", - "title": "forked-daapd \u8a2d\u5b9a\u9078\u9805" + "description": "\u8a2d\u5b9a Owntone \u6574\u5408\u9078\u9805\u3002", + "title": "Owntone \u8a2d\u5b9a\u9078\u9805" } } } diff --git a/homeassistant/components/garages_amsterdam/translations/pl.json b/homeassistant/components/garages_amsterdam/translations/pl.json index a9f220d9bfc..d8207f45e83 100644 --- a/homeassistant/components/garages_amsterdam/translations/pl.json +++ b/homeassistant/components/garages_amsterdam/translations/pl.json @@ -14,5 +14,5 @@ } } }, - "title": "Parkingi Amsterdamie" + "title": "Parkingi w Amsterdamie" } \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/tr.json b/homeassistant/components/google_sheets/translations/tr.json new file mode 100644 index 00000000000..b92ca894b57 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/tr.json @@ -0,0 +1,35 @@ +{ + "application_credentials": { + "description": "Home Assistant'\u0131n Google E-Tablolar\u0131n\u0131za eri\u015fmesine izin vermek i\u00e7in [OAuth izin ekran\u0131]( {oauth_consent_url} ) i\u00e7in [talimatlar\u0131]( {more_info_url} ) uygulay\u0131n. Ayr\u0131ca, hesab\u0131n\u0131za ba\u011fl\u0131 Uygulama Kimlik Bilgileri olu\u015fturman\u0131z gerekir:\n 1. [Kimlik Bilgileri]( {oauth_creds_url} ) \u00f6\u011fesine gidin ve **Kimlik Bilgileri Olu\u015ftur**'u t\u0131klay\u0131n.\n 1. A\u00e7\u0131l\u0131r listeden **OAuth istemci kimli\u011fi**'ni se\u00e7in.\n 1. Uygulama T\u00fcr\u00fc i\u00e7in **Web uygulamas\u0131**'n\u0131 se\u00e7in. \n\n" + }, + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "create_spreadsheet_failure": "E-tablo olu\u015fturulurken hata olu\u015ftu, ayr\u0131nt\u0131lar i\u00e7in hata g\u00fcnl\u00fc\u011f\u00fcne bak\u0131n", + "invalid_access_token": "Ge\u00e7ersiz eri\u015fim anahtar\u0131", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "oauth_error": "Ge\u00e7ersiz anahtar verileri al\u0131nd\u0131.", + "open_spreadsheet_failure": "E-tablo a\u00e7\u0131l\u0131rken hata olu\u015ftu, ayr\u0131nt\u0131lar i\u00e7in hata g\u00fcnl\u00fc\u011f\u00fcne bak\u0131n", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "timeout_connect": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131", + "unknown": "Beklenmeyen hata" + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131 ve \u015fu adreste e-tablo olu\u015fturuldu: {url}" + }, + "step": { + "auth": { + "title": "Google Hesab\u0131n\u0131 Ba\u011fla" + }, + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + }, + "reauth_confirm": { + "description": "Google E-Tablolar entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulanmas\u0131 gerekiyor", + "title": "Entegrasyonu Yeniden Do\u011frula" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/en.json b/homeassistant/components/guardian/translations/en.json index 1aaf8b888c8..ac87ae36506 100644 --- a/homeassistant/components/guardian/translations/en.json +++ b/homeassistant/components/guardian/translations/en.json @@ -29,6 +29,17 @@ } }, "title": "The {deprecated_service} service will be removed" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Update any automations or scripts that use this entity to instead use `{replacement_entity_id}`.", + "title": "The {old_entity_id} entity will be removed" + } + } + }, + "title": "The {old_entity_id} entity will be removed" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/et.json b/homeassistant/components/guardian/translations/et.json index 37eee5acf33..95c98315435 100644 --- a/homeassistant/components/guardian/translations/et.json +++ b/homeassistant/components/guardian/translations/et.json @@ -23,7 +23,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Uuenda k\u00f5iki seda teenust kasutavaid automatiseerimisi v\u00f5i skripte, et need kasutaksid selle asemel teenust `{alternate_service}}, mille siht\u00fcksuse ID on `{alternate_target}}. Seej\u00e4rel kl\u00f5psa allpool nuppu ESITA, et m\u00e4rkida see probleem lahendatuks.", + "description": "V\u00e4rskenda k\u00f5iki automaatikaid v\u00f5i skripte, mis seda teenust kasutavad, et kasutada selle asemel teenust '{alternate_service}', mille sihtolemi ID on '{alternate_target}'.", "title": "Teenus {deprecated_service} eemaldatakse" } } @@ -34,7 +34,7 @@ "fix_flow": { "step": { "confirm": { - "description": "See olem on asendatud olemiga \"{replacement_entity_id}\".", + "description": "Uuenda k\u00f5iki automaatikaid v\u00f5i skripte, mis kasutavad seda olemit, et kasutada selle asemel `{replacement_entity_id}}.", "title": "Olem {old_entity_id} eemaldatakse" } } diff --git a/homeassistant/components/guardian/translations/tr.json b/homeassistant/components/guardian/translations/tr.json index e5de0cb73cd..c7e557a38f2 100644 --- a/homeassistant/components/guardian/translations/tr.json +++ b/homeassistant/components/guardian/translations/tr.json @@ -23,12 +23,23 @@ "fix_flow": { "step": { "confirm": { - "description": "Bu hizmeti kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131, bunun yerine \" {alternate_service} {alternate_target} hizmetini kullanacak \u015fekilde g\u00fcncelleyin. Ard\u0131ndan, bu sorunu \u00e7\u00f6z\u00fcld\u00fc olarak i\u015faretlemek i\u00e7in a\u015fa\u011f\u0131daki G\u00d6NDER'i t\u0131klay\u0131n.", - "title": "{deprecated_service} hizmeti kald\u0131r\u0131l\u0131yor" + "description": "Bu hizmeti kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131, bunun yerine \" {alternate_service} {alternate_target} hizmetini kullanacak \u015fekilde g\u00fcncelleyin.", + "title": "{deprecated_service} hizmeti kald\u0131r\u0131lacak" } } }, - "title": "{deprecated_service} hizmeti kald\u0131r\u0131l\u0131yor" + "title": "{deprecated_service} hizmeti kald\u0131r\u0131lacak" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Bunun yerine ` {replacement_entity_id} ` kullanmak i\u00e7in bu varl\u0131\u011f\u0131 kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131 g\u00fcncelleyin.", + "title": "{old_entity_id} varl\u0131\u011f\u0131 kald\u0131r\u0131lacak" + } + } + }, + "title": "{old_entity_id} varl\u0131\u011f\u0131 kald\u0131r\u0131lacak" } } } \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/et.json b/homeassistant/components/ibeacon/translations/et.json new file mode 100644 index 00000000000..34d319b2a08 --- /dev/null +++ b/homeassistant/components/ibeacon/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "iBeacon Tracker'i kasutamiseks peab olema konfigureeritud v\u00e4hemalt \u00fcks Bluetooth-adapter v\u00f5i kaugjuhtimispult.", + "single_instance_allowed": "Juba h\u00e4\u00e4lestatud. V\u00f5imalik on ainult \u00fcks sidumine." + }, + "step": { + "user": { + "description": "Kas soovite iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "Minimaalne RSSI" + }, + "description": "iBeacone, mille RSSI v\u00e4\u00e4rtus on madalam kui minimaalne RSSI, ignoreeritakse. Kui integratsioon n\u00e4eb naabruses asuvaid iBeacone, v\u00f5ib selle v\u00e4\u00e4rtuse suurendamine aidata." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/tr.json b/homeassistant/components/ibeacon/translations/tr.json new file mode 100644 index 00000000000..2bb5e530f6d --- /dev/null +++ b/homeassistant/components/ibeacon/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "iBeacon Tracker'\u0131 kullanmak i\u00e7in en az bir Bluetooth adapt\u00f6r\u00fc veya uzaktan kumanda yap\u0131land\u0131r\u0131lmal\u0131d\u0131r.", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "user": { + "description": "iBeacon Tracker'\u0131 kurmak istiyor musunuz?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "Minimum RSSI" + }, + "description": "Minimum RSSI de\u011ferinden daha d\u00fc\u015f\u00fck bir RSSI de\u011ferine sahip iBeacon'lar yok say\u0131l\u0131r. Entegrasyon kom\u015fu iBeacon'lar\u0131 g\u00f6r\u00fcyorsa, bu de\u011feri art\u0131rmak yard\u0131mc\u0131 olabilir." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/et.json b/homeassistant/components/kegtron/translations/et.json index a83aceef49d..170815ec87e 100644 --- a/homeassistant/components/kegtron/translations/et.json +++ b/homeassistant/components/kegtron/translations/et.json @@ -1,6 +1,9 @@ { "config": { "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud", "not_supported": "Seadet ei toetata" }, "flow_title": "{name}", diff --git a/homeassistant/components/kegtron/translations/tr.json b/homeassistant/components/kegtron/translations/tr.json new file mode 100644 index 00000000000..f0ddbc274c9 --- /dev/null +++ b/homeassistant/components/kegtron/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "not_supported": "Cihaz desteklenmiyor" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/tr.json b/homeassistant/components/keymitt_ble/translations/tr.json new file mode 100644 index 00000000000..49b3f7d917c --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/tr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "no_unconfigured_devices": "Yap\u0131land\u0131r\u0131lmam\u0131\u015f cihaz bulunamad\u0131.", + "unknown": "Beklenmeyen hata" + }, + "error": { + "linking": "E\u015fle\u015ftirilemedi, l\u00fctfen tekrar deneyin. MicroBot e\u015fle\u015ftirme modunda m\u0131?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Cihaz adresi", + "name": "Ad" + }, + "title": "MicroBot cihaz\u0131n\u0131 kurun" + }, + "link": { + "description": "Home Assistant'a kaydolmak i\u00e7in LED sabit pembe veya ye\u015fil oldu\u011funda MicroBot Push'taki d\u00fc\u011fmeye bas\u0131n.", + "title": "E\u015fle\u015ftirme" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/tr.json b/homeassistant/components/lametric/translations/tr.json index 1801d6aac08..83f1f0e2ca4 100644 --- a/homeassistant/components/lametric/translations/tr.json +++ b/homeassistant/components/lametric/translations/tr.json @@ -7,7 +7,8 @@ "link_local_address": "Ba\u011flant\u0131 yerel adresleri desteklenmiyor", "missing_configuration": "LaMetric entegrasyonu yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", "no_devices": "Yetkili kullan\u0131c\u0131n\u0131n LaMetric cihaz\u0131 yok", - "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})" + "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})", + "unknown": "Beklenmeyen hata" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", diff --git a/homeassistant/components/lidarr/translations/bg.json b/homeassistant/components/lidarr/translations/bg.json index 4e22178a11d..040b54c06e1 100644 --- a/homeassistant/components/lidarr/translations/bg.json +++ b/homeassistant/components/lidarr/translations/bg.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", - "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430", + "zeroconf_failed": "API \u043a\u043b\u044e\u0447\u044a\u0442 \u043d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d. \u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0433\u043e \u0440\u044a\u0447\u043d\u043e." }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/lidarr/translations/et.json b/homeassistant/components/lidarr/translations/et.json new file mode 100644 index 00000000000..0a88c87659f --- /dev/null +++ b/homeassistant/components/lidarr/translations/et.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge", + "wrong_app": "Vale rakendus. Palun proovi uuesti", + "zeroconf_failed": "API v\u00f5tit ei leitud. Sisesta see k\u00e4sitsi" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API v\u00f5ti" + }, + "description": "Lidarri sidumine tuleb Lidarr API-ga k\u00e4sitsi uuesti autentida", + "title": "Taastuvasta sidumine" + }, + "user": { + "data": { + "api_key": "API v\u00f5ti", + "url": "URL", + "verify_ssl": "Kontrolli SSL serte" + }, + "description": "API-v\u00f5tme saab automaatselt alla laadida, kui rakenduses pole sisselogimismandaate m\u00e4\u00e4ratud.\n API-v\u00f5tme leiate Lidarri veebikasutajaliidese jaotisest Seaded > \u00dcldine." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "Soovitud ja j\u00e4rjekorras kuvatavate kirjete maksimaalne arv", + "upcoming_days": "Kalendris kuvatavate eelseisvate p\u00e4evade arv" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/tr.json b/homeassistant/components/lidarr/translations/tr.json new file mode 100644 index 00000000000..39785efb9b0 --- /dev/null +++ b/homeassistant/components/lidarr/translations/tr.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata", + "wrong_app": "Yanl\u0131\u015f uygulamaya ula\u015f\u0131ld\u0131. L\u00fctfen tekrar deneyin", + "zeroconf_failed": "API anahtar\u0131 bulunamad\u0131. L\u00fctfen manuel olarak girin" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API Anahtar\u0131" + }, + "description": "Lidarr entegrasyonunun, Lidarr API ile manuel olarak yeniden do\u011frulanmas\u0131 gerekiyor", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "url": "URL", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" + }, + "description": "Giri\u015f kimlik bilgileri uygulamada ayarlanmad\u0131ysa API anahtar\u0131 otomatik olarak al\u0131nabilir.\n API anahtar\u0131n\u0131z, Lidarr Web Kullan\u0131c\u0131 Aray\u00fcz\u00fcndeki Ayarlar > Genel b\u00f6l\u00fcm\u00fcnde bulunabilir." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "Aranan ve kuyrukta g\u00f6r\u00fcnt\u00fclenecek maksimum kay\u0131t say\u0131s\u0131", + "upcoming_days": "Takvimde g\u00f6r\u00fcnt\u00fclenecek yakla\u015fan g\u00fcn say\u0131s\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.tr.json b/homeassistant/components/litterrobot/translations/sensor.tr.json index 2db5e574f7e..e9848e96501 100644 --- a/homeassistant/components/litterrobot/translations/sensor.tr.json +++ b/homeassistant/components/litterrobot/translations/sensor.tr.json @@ -4,6 +4,7 @@ "br": "Kapak \u00c7\u0131kar\u0131ld\u0131", "ccc": "Temizleme Tamamland\u0131", "ccp": "Temizleme Devam Ediyor", + "cd": "Kedi Tespit Edildi", "csf": "Kedi Sens\u00f6r\u00fc Hatas\u0131", "csi": "Kedi Sens\u00f6r\u00fc Kesildi", "cst": "Kedi Sens\u00f6r Zamanlamas\u0131", @@ -19,6 +20,8 @@ "otf": "A\u015f\u0131r\u0131 Tork Ar\u0131zas\u0131", "p": "Durduruldu", "pd": "S\u0131k\u0131\u015fma Alg\u0131lama", + "pwrd": "G\u00fcc\u00fc Kapatma", + "pwru": "G\u00fcc\u00fc A\u00e7ma", "rdy": "Haz\u0131r", "scf": "Ba\u015flang\u0131\u00e7ta Cat Sens\u00f6r\u00fc Hatas\u0131", "sdf": "Ba\u015flang\u0131\u00e7ta Hazne Dolu", diff --git a/homeassistant/components/litterrobot/translations/tr.json b/homeassistant/components/litterrobot/translations/tr.json index 193413280eb..70b19443055 100644 --- a/homeassistant/components/litterrobot/translations/tr.json +++ b/homeassistant/components/litterrobot/translations/tr.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "Vakum varl\u0131k \u00f6znitelikleri art\u0131k tan\u0131 sens\u00f6rleri olarak mevcuttur. \n\n L\u00fctfen bu \u00f6znitelikleri kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131 ayarlay\u0131n.", + "title": "Litter-Robot \u00f6znitelikleri art\u0131k kendi sens\u00f6rleridir" + } } } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/ca.json b/homeassistant/components/moon/translations/ca.json index 085de62df8c..6476e90a84b 100644 --- a/homeassistant/components/moon/translations/ca.json +++ b/homeassistant/components/moon/translations/ca.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "La configuraci\u00f3 de la Lluna mitjan\u00e7ant YAML s'ha eliminat de Home Assistant.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de la Lluna s'ha eliminat" + } + }, "title": "Lluna" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/tr.json b/homeassistant/components/moon/translations/tr.json index 0abcd94692c..d1c5810d269 100644 --- a/homeassistant/components/moon/translations/tr.json +++ b/homeassistant/components/moon/translations/tr.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Moon'un YAML kullan\u0131larak yap\u0131land\u0131r\u0131lmas\u0131 kald\u0131r\u0131ld\u0131.\n\nMevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lmaz.\n\nYAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Moon YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" + } + }, "title": "Ay" } \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/tr.json b/homeassistant/components/nibe_heatpump/translations/tr.json new file mode 100644 index 00000000000..6f044f9b9d4 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "address": "Ge\u00e7ersiz uzak IP adresi belirtildi. Adres bir IPV4 adresi olmal\u0131d\u0131r.", + "address_in_use": "Se\u00e7ilen dinleme ba\u011flant\u0131 noktas\u0131 bu sistemde zaten kullan\u0131l\u0131yor.", + "model": "Se\u00e7ilen model modbus40'\u0131 desteklemiyor gibi g\u00f6r\u00fcn\u00fcyor", + "read": "Pompadan okuma iste\u011finde hata. 'Uzaktan okuma ba\u011flant\u0131 noktas\u0131' veya 'Uzak IP adresinizi' do\u011frulay\u0131n.", + "unknown": "Beklenmeyen hata", + "write": "Pompaya yazma iste\u011finde hata. \"Uzak yazma ba\u011flant\u0131 noktas\u0131\" veya \"Uzak IP adresi\"nizi do\u011frulay\u0131n." + }, + "step": { + "user": { + "data": { + "ip_address": "Uzak IP adresi", + "listening_port": "Yerel dinleme ba\u011flant\u0131 noktas\u0131", + "remote_read_port": "Uzaktan okuma ba\u011flant\u0131 noktas\u0131", + "remote_write_port": "Uzaktan yazma ba\u011flant\u0131 noktas\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/tr.json b/homeassistant/components/openuv/translations/tr.json index d5caa40721a..cf70500f213 100644 --- a/homeassistant/components/openuv/translations/tr.json +++ b/homeassistant/components/openuv/translations/tr.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Bu hizmeti kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131, bunun yerine hedef olarak \u015fu varl\u0131k kimliklerinden biriyle ` {alternate_service} ` hizmetini kullanacak \u015fekilde g\u00fcncelleyin: ` {alternate_targets} `.", + "title": "{deprecated_service} hizmeti kald\u0131r\u0131l\u0131yor" + }, + "deprecated_service_single_alternate_target": { + "description": "Bu hizmeti kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131, hedef olarak `{alternate_targets}` ile `{alternate_service}` hizmetini kullanacak \u015fekilde g\u00fcncelleyin.", + "title": "{deprecated_service} hizmeti kald\u0131r\u0131l\u0131yor" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radarr/translations/bg.json b/homeassistant/components/radarr/translations/bg.json index 5ce0eec5412..e735a1995eb 100644 --- a/homeassistant/components/radarr/translations/bg.json +++ b/homeassistant/components/radarr/translations/bg.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", - "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430", + "zeroconf_failed": "API \u043a\u043b\u044e\u0447\u044a\u0442 \u043d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d. \u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0433\u043e \u0440\u044a\u0447\u043d\u043e." }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/radarr/translations/tr.json b/homeassistant/components/radarr/translations/tr.json new file mode 100644 index 00000000000..256cba85647 --- /dev/null +++ b/homeassistant/components/radarr/translations/tr.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata", + "wrong_app": "Yanl\u0131\u015f uygulamaya ula\u015f\u0131ld\u0131. L\u00fctfen tekrar deneyin", + "zeroconf_failed": "API anahtar\u0131 bulunamad\u0131. L\u00fctfen manuel olarak girin" + }, + "step": { + "reauth_confirm": { + "description": "Radarr entegrasyonunun Radarr API ile manuel olarak yeniden do\u011frulanmas\u0131 gerekiyor", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "url": "URL", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" + }, + "description": "Giri\u015f kimlik bilgileri uygulamada ayarlanmad\u0131ysa API anahtar\u0131 otomatik olarak al\u0131nabilir.\n API anahtar\u0131n\u0131z, Radarr Web Kullan\u0131c\u0131 Aray\u00fcz\u00fcndeki Ayarlar > Genel b\u00f6l\u00fcm\u00fcnde bulunabilir." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Radarr'\u0131n YAML kullan\u0131larak yap\u0131land\u0131r\u0131lmas\u0131 kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. \n\n Radarr YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Radarr YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + }, + "removed_attributes": { + "description": "Film sayma sens\u00f6r\u00fcn\u00fcn dikkatli bir \u015fekilde devre d\u0131\u015f\u0131 b\u0131rak\u0131lmas\u0131nda baz\u0131 \u00f6nemli de\u011fi\u015fiklikler yap\u0131ld\u0131. \n\n Bu sens\u00f6r, b\u00fcy\u00fck veritabanlar\u0131nda sorunlara neden olabilir. Hala kullanmak istiyorsan\u0131z, bunu yapabilirsiniz. \n\n Film adlar\u0131 art\u0131k film sens\u00f6r\u00fcne \u00f6znitelik olarak dahil edilmemektedir. \n\n Yakla\u015fan kald\u0131r\u0131ld\u0131. Takvim \u00f6\u011feleri olmas\u0131 gerekti\u011fi gibi modernize ediliyor. Disk alan\u0131 art\u0131k her klas\u00f6r i\u00e7in bir tane olmak \u00fczere farkl\u0131 sens\u00f6rlere b\u00f6l\u00fcnm\u00fc\u015ft\u00fcr. \n\n Otomasyonlar i\u00e7in ger\u00e7ek de\u011feri olmad\u0131\u011f\u0131 i\u00e7in durum ve komutlar kald\u0131r\u0131ld\u0131.", + "title": "Radarr entegrasyonundaki de\u011fi\u015fiklikler" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "G\u00f6r\u00fcnt\u00fclenecek yakla\u015fan g\u00fcn say\u0131s\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/en.json b/homeassistant/components/rainmachine/translations/en.json index 9369eeae4c8..3e5d824ee08 100644 --- a/homeassistant/components/rainmachine/translations/en.json +++ b/homeassistant/components/rainmachine/translations/en.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Update any automations or scripts that use this entity to instead use `{replacement_entity_id}`.", + "title": "The {old_entity_id} entity will be removed" + } + } + }, + "title": "The {old_entity_id} entity will be removed" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/tr.json b/homeassistant/components/rainmachine/translations/tr.json index 7db1bffd5c8..fa181de3505 100644 --- a/homeassistant/components/rainmachine/translations/tr.json +++ b/homeassistant/components/rainmachine/translations/tr.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Bunun yerine ` {replacement_entity_id} ` kullanmak i\u00e7in bu varl\u0131\u011f\u0131 kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131 g\u00fcncelleyin.", + "title": "{old_entity_id} varl\u0131\u011f\u0131 kald\u0131r\u0131lacak" + } + } + }, + "title": "{old_entity_id} varl\u0131\u011f\u0131 kald\u0131r\u0131lacak" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json index b4126e202d1..e8597a0cd38 100644 --- a/homeassistant/components/roomba/translations/hu.json +++ b/homeassistant/components/roomba/translations/hu.json @@ -12,14 +12,14 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "Nyomja meg \u00e9s tartsa lenyomva a {name} Home gombj\u00e1t, am\u00edg az eszk\u00f6z hangot ad (kb. k\u00e9t m\u00e1sodperc), majd engedje el 30 m\u00e1sodpercen bel\u00fcl.", + "description": "Gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy az iRobot alkalmaz\u00e1s nem fut egyik eszk\u00f6z\u00f6n sem. Tartsa lenyomva a {name} Home gombot, am\u00edg a k\u00e9sz\u00fcl\u00e9k hangot nem ad ki (kb. k\u00e9t m\u00e1sodpercig), majd 30 m\u00e1sodpercen bel\u00fcl k\u00fcldje be.", "title": "Jelsz\u00f3 lek\u00e9r\u00e9se" }, "link_manual": { "data": { "password": "Jelsz\u00f3" }, - "description": "A jelsz\u00f3t nem siker\u00fclt automatikusan lek\u00e9rni az eszk\u00f6zr\u0151l. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3ban ismertetett l\u00e9p\u00e9seket: {auth_help_url}", + "description": "A jelsz\u00f3t nem siker\u00fclt automatikusan lek\u00e9rdezni a k\u00e9sz\u00fcl\u00e9kr\u0151l. K\u00e9rj\u00fck, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy az iRobot alkalmaz\u00e1s nincs nyitva egyik eszk\u00f6z\u00f6n sem, mik\u00f6zben megpr\u00f3b\u00e1lja lek\u00e9rdezni a jelsz\u00f3t. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3ban le\u00edrt l\u00e9p\u00e9seket a k\u00f6vetkez\u0151 c\u00edmen: {auth_help_url}", "title": "Jelsz\u00f3 megad\u00e1sa" }, "manual": { diff --git a/homeassistant/components/roomba/translations/tr.json b/homeassistant/components/roomba/translations/tr.json index ecdaa8b97be..4440aeb152b 100644 --- a/homeassistant/components/roomba/translations/tr.json +++ b/homeassistant/components/roomba/translations/tr.json @@ -12,14 +12,14 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "Cihaz bir ses \u00e7\u0131karana kadar (yakla\u015f\u0131k iki saniye) {name} \u00fczerindeki Ana Sayfa d\u00fc\u011fmesini bas\u0131l\u0131 tutun, ard\u0131ndan 30 saniye i\u00e7inde g\u00f6nderin.", + "description": "iRobot uygulamas\u0131n\u0131n hi\u00e7bir cihazda \u00e7al\u0131\u015fmad\u0131\u011f\u0131ndan emin olun. Cihaz bir ses \u00e7\u0131karana kadar (yakla\u015f\u0131k iki saniye) {name} \u00fczerindeki Ana Sayfa d\u00fc\u011fmesini bas\u0131l\u0131 tutun, ard\u0131ndan 30 saniye i\u00e7inde g\u00f6nderin.", "title": "\u015eifre Al" }, "link_manual": { "data": { "password": "\u015eifre" }, - "description": "Parola ayg\u0131ttan otomatik olarak al\u0131namad\u0131. L\u00fctfen belgelerde belirtilen ad\u0131mlar\u0131 izleyin: {auth_help_url}", + "description": "\u015eifre cihazdan otomatik olarak al\u0131namad\u0131. L\u00fctfen \u015fifreyi almaya \u00e7al\u0131\u015f\u0131rken iRobot uygulamas\u0131n\u0131n hi\u00e7bir cihazda a\u00e7\u0131k olmad\u0131\u011f\u0131ndan emin olun. L\u00fctfen \u015fu adresteki belgelerde belirtilen ad\u0131mlar\u0131 izleyin: {auth_help_url}", "title": "\u015eifre Girin" }, "manual": { diff --git a/homeassistant/components/season/translations/tr.json b/homeassistant/components/season/translations/tr.json index a625042d5dd..d214692147a 100644 --- a/homeassistant/components/season/translations/tr.json +++ b/homeassistant/components/season/translations/tr.json @@ -10,5 +10,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Season'\u0131 YAML kullanarak yap\u0131land\u0131rma kald\u0131r\u0131ld\u0131. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lm\u0131yor. \n\n YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Sezon YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" + } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/et.json b/homeassistant/components/sensor/translations/et.json index 3519586809e..00343d6ae10 100644 --- a/homeassistant/components/sensor/translations/et.json +++ b/homeassistant/components/sensor/translations/et.json @@ -31,7 +31,8 @@ "is_value": "Praegune {entity_name} v\u00e4\u00e4rtus", "is_volatile_organic_compounds": "Praegune {entity_name} lenduvate orgaaniliste \u00fchendite kontsentratsioonitase", "is_voltage": "Praegune {entity_name}pinge", - "is_volume": "Praegune {entity_name} helitugevus" + "is_volume": "Praegune {entity_name} helitugevus", + "is_weight": "Praegune {entity_name} kaal" }, "trigger_type": { "apparent_power": "{entity_name} n\u00e4iv v\u00f5imsus muutub", @@ -64,7 +65,8 @@ "value": "{entity_name} v\u00e4\u00e4rtus muutub", "volatile_organic_compounds": "{entity_name} lenduvate orgaaniliste \u00fchendite kontsentratsiooni muutused", "voltage": "{entity_name} pingemuutub", - "volume": "{entity_name} helitugevus muutub" + "volume": "{entity_name} helitugevus muutub", + "weight": "{entity_name} kaal muutus" } }, "state": { diff --git a/homeassistant/components/sensor/translations/hu.json b/homeassistant/components/sensor/translations/hu.json index 48fe4a651b8..7e164d9b980 100644 --- a/homeassistant/components/sensor/translations/hu.json +++ b/homeassistant/components/sensor/translations/hu.json @@ -6,6 +6,7 @@ "is_carbon_dioxide": "Jelenlegi {entity_name} sz\u00e9n-dioxid koncentr\u00e1ci\u00f3 szint", "is_carbon_monoxide": "Jelenlegi {entity_name} sz\u00e9n-monoxid koncentr\u00e1ci\u00f3 szint", "is_current": "Jelenlegi {entity_name} \u00e1ram", + "is_distance": "{entity_name} aktu\u00e1lis t\u00e1vols\u00e1ga", "is_energy": "A jelenlegi {entity_name} energia", "is_frequency": "Aktu\u00e1lis {entity_name} gyakoris\u00e1g", "is_gas": "Jelenlegi {entity_name} g\u00e1z", @@ -24,11 +25,14 @@ "is_pressure": "{entity_name} aktu\u00e1lis nyom\u00e1sa", "is_reactive_power": "Aktu\u00e1lis {entity_name} reakt\u00edv teljes\u00edtm\u00e9ny", "is_signal_strength": "{entity_name} aktu\u00e1lis jeler\u0151ss\u00e9ge", + "is_speed": "{entity_name} aktu\u00e1lis sebess\u00e9ge", "is_sulphur_dioxide": "A {entity_name} k\u00e9n-dioxid koncentr\u00e1ci\u00f3 jelenlegi szintje", "is_temperature": "{entity_name} aktu\u00e1lis h\u0151m\u00e9rs\u00e9klete", "is_value": "{entity_name} aktu\u00e1lis \u00e9rt\u00e9ke", "is_volatile_organic_compounds": "Jelenlegi {entity_name} ill\u00e9kony szerves vegy\u00fcletek koncentr\u00e1ci\u00f3s szintje", - "is_voltage": "A jelenlegi {entity_name} fesz\u00fclts\u00e9g" + "is_voltage": "A jelenlegi {entity_name} fesz\u00fclts\u00e9g", + "is_volume": "{entity_name} aktu\u00e1lis hangereje", + "is_weight": "{entity_name} aktu\u00e1lis s\u00falya" }, "trigger_type": { "apparent_power": "{entity_name} l\u00e1tsz\u00f3lagos teljes\u00edtm\u00e9ny v\u00e1ltoz\u00e1sok", @@ -36,6 +40,7 @@ "carbon_dioxide": "{entity_name} sz\u00e9n-dioxid koncentr\u00e1ci\u00f3ja megv\u00e1ltozik", "carbon_monoxide": "{entity_name} sz\u00e9n-monoxid koncentr\u00e1ci\u00f3ja megv\u00e1ltozik", "current": "{entity_name} aktu\u00e1lis v\u00e1ltoz\u00e1sai", + "distance": "{entity_name} t\u00e1vols\u00e1g v\u00e1ltoz\u00e1s", "energy": "{entity_name} energiav\u00e1ltoz\u00e1sa", "frequency": "{entity_name} gyakoris\u00e1gi v\u00e1ltoz\u00e1sok", "gas": "{entity_name} g\u00e1z v\u00e1ltoz\u00e1sok", @@ -54,11 +59,14 @@ "pressure": "{entity_name} nyom\u00e1sa v\u00e1ltozik", "reactive_power": "{entity_name} reakt\u00edv teljes\u00edtm\u00e9ny v\u00e1ltoz\u00e1sok", "signal_strength": "{entity_name} jeler\u0151ss\u00e9ge v\u00e1ltozik", + "speed": "{entity_name} sebess\u00e9gv\u00e1ltoz\u00e1s", "sulphur_dioxide": "{entity_name} k\u00e9n-dioxid koncentr\u00e1ci\u00f3v\u00e1ltoz\u00e1s", "temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klete v\u00e1ltozik", "value": "{entity_name} \u00e9rt\u00e9ke v\u00e1ltozik", "volatile_organic_compounds": "{entity_name} ill\u00e9kony szerves vegy\u00fcletek koncentr\u00e1ci\u00f3j\u00e1nak v\u00e1ltoz\u00e1sai", - "voltage": "{entity_name} fesz\u00fclts\u00e9ge v\u00e1ltozik" + "voltage": "{entity_name} fesz\u00fclts\u00e9ge v\u00e1ltozik", + "volume": "{entity_name} hanger\u0151 v\u00e1ltoz\u00e1s", + "weight": "{entity_name} s\u00falyv\u00e1ltoz\u00e1s" } }, "state": { diff --git a/homeassistant/components/sensor/translations/id.json b/homeassistant/components/sensor/translations/id.json index 8011fed4e3b..b8f85805ebc 100644 --- a/homeassistant/components/sensor/translations/id.json +++ b/homeassistant/components/sensor/translations/id.json @@ -31,7 +31,8 @@ "is_value": "Nilai {entity_name} saat ini", "is_volatile_organic_compounds": "Tingkat konsentrasi senyawa organik volatil {entity_name} saat ini", "is_voltage": "Tegangan {entity_name} saat ini", - "is_volume": "Volume {entity_name} saat ini" + "is_volume": "Volume {entity_name} saat ini", + "is_weight": "Berat {entity_name} saat ini" }, "trigger_type": { "apparent_power": "Perubahan daya nyata {entity_name}", @@ -64,7 +65,8 @@ "value": "Perubahan nilai {entity_name}", "volatile_organic_compounds": "Perubahan konsentrasi senyawa organik volatil {entity_name}", "voltage": "Perubahan tegangan {entity_name}", - "volume": "Perubahan volume {entity_name}" + "volume": "Perubahan volume {entity_name}", + "weight": "Perubahan berat {entity_name}" } }, "state": { diff --git a/homeassistant/components/sensor/translations/tr.json b/homeassistant/components/sensor/translations/tr.json index cc7cf3f39fa..ad2e5110f0c 100644 --- a/homeassistant/components/sensor/translations/tr.json +++ b/homeassistant/components/sensor/translations/tr.json @@ -6,6 +6,7 @@ "is_carbon_dioxide": "Mevcut {entity_name} karbondioksit konsantrasyon seviyesi", "is_carbon_monoxide": "Mevcut {entity_name} karbon monoksit konsantrasyon seviyesi", "is_current": "Mevcut {entity_name} ak\u0131m\u0131", + "is_distance": "Mevcut {entity_name} mesafesi", "is_energy": "Mevcut {entity_name} enerjisi", "is_frequency": "Ge\u00e7erli {entity_name} frekans\u0131", "is_gas": "Mevcut {entity_name} gaz\u0131", @@ -24,11 +25,14 @@ "is_pressure": "Ge\u00e7erli {entity_name} bas\u0131nc\u0131", "is_reactive_power": "Mevcut {entity_name} reaktif g\u00fc\u00e7", "is_signal_strength": "Mevcut {entity_name} sinyal g\u00fcc\u00fc", + "is_speed": "Mevcut {entity_name} h\u0131z\u0131", "is_sulphur_dioxide": "Mevcut {entity_name} k\u00fck\u00fcrt dioksit konsantrasyon seviyesi", "is_temperature": "Mevcut {entity_name} s\u0131cakl\u0131\u011f\u0131", "is_value": "Mevcut {entity_name} de\u011feri", "is_volatile_organic_compounds": "Mevcut {entity_name} u\u00e7ucu organik bile\u015fik konsantrasyon seviyesi", - "is_voltage": "Mevcut {entity_name} voltaj\u0131" + "is_voltage": "Mevcut {entity_name} voltaj\u0131", + "is_volume": "Mevcut {entity_name} birimi", + "is_weight": "Mevcut {entity_name} a\u011f\u0131rl\u0131\u011f\u0131" }, "trigger_type": { "apparent_power": "{entity_name} g\u00f6r\u00fcn\u00fcr g\u00fc\u00e7 de\u011fi\u015fiklikleri", @@ -36,6 +40,7 @@ "carbon_dioxide": "{entity_name} karbondioksit konsantrasyonu de\u011fi\u015fiklikleri", "carbon_monoxide": "{entity_name} karbon monoksit konsantrasyonu de\u011fi\u015fiklikleri", "current": "{entity_name} ak\u0131m de\u011fi\u015fiklikleri", + "distance": "{entity_name} mesafe de\u011fi\u015fiklikleri", "energy": "{entity_name} enerji de\u011fi\u015fiklikleri", "frequency": "{entity_name} frekans de\u011fi\u015fiklikleri", "gas": "{entity_name} gaz de\u011fi\u015fiklikleri", @@ -54,11 +59,14 @@ "pressure": "{entity_name} bas\u0131n\u00e7 de\u011fi\u015fiklikleri", "reactive_power": "{entity_name} reaktif g\u00fc\u00e7 de\u011fi\u015fiklikleri", "signal_strength": "{entity_name} sinyal g\u00fcc\u00fc de\u011fi\u015fiklikleri", + "speed": "{entity_name} h\u0131z de\u011fi\u015fiklikleri", "sulphur_dioxide": "{entity_name} k\u00fck\u00fcrt dioksit konsantrasyonu de\u011fi\u015fiklikleri", "temperature": "{entity_name} s\u0131cakl\u0131k de\u011fi\u015fiklikleri", "value": "{entity_name} de\u011fer de\u011fi\u015fiklikleri", "volatile_organic_compounds": "{entity_name} u\u00e7ucu organik bile\u015fik konsantrasyonu de\u011fi\u015fiklikleri", - "voltage": "{entity_name} voltaj de\u011fi\u015fiklikleri" + "voltage": "{entity_name} voltaj de\u011fi\u015fiklikleri", + "volume": "{entity_name} birim de\u011fi\u015fiklikleri", + "weight": "{entity_name} a\u011f\u0131rl\u0131k de\u011fi\u015fiklikleri" } }, "state": { diff --git a/homeassistant/components/shelly/translations/tr.json b/homeassistant/components/shelly/translations/tr.json index fac805e5134..435d5a3ec56 100644 --- a/homeassistant/components/shelly/translations/tr.json +++ b/homeassistant/components/shelly/translations/tr.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "reauth_unsuccessful": "Yeniden kimlik do\u011frulama ba\u015far\u0131s\u0131z oldu, l\u00fctfen entegrasyonu kald\u0131r\u0131n ve yeniden kurun.", "unsupported_firmware": "Cihaz, desteklenmeyen bir versiyon s\u00fcr\u00fcm\u00fc kullan\u0131yor." }, "error": { @@ -21,6 +23,12 @@ "username": "Kullan\u0131c\u0131 Ad\u0131" } }, + "reauth_confirm": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + }, "user": { "data": { "host": "Sunucu" diff --git a/homeassistant/components/simplisafe/translations/tr.json b/homeassistant/components/simplisafe/translations/tr.json index 02153b0b90c..f7073bb3df7 100644 --- a/homeassistant/components/simplisafe/translations/tr.json +++ b/homeassistant/components/simplisafe/translations/tr.json @@ -39,6 +39,12 @@ } } }, + "issues": { + "deprecated_service": { + "description": "Bu hizmeti kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131, bunun yerine \" {alternate_service} {alternate_target} hizmetini kullanacak \u015fekilde g\u00fcncelleyin. Ard\u0131ndan, bu sorunu \u00e7\u00f6z\u00fcld\u00fc olarak i\u015faretlemek i\u00e7in a\u015fa\u011f\u0131daki G\u00d6NDER'i t\u0131klay\u0131n.", + "title": "{deprecated_service} hizmeti kald\u0131r\u0131l\u0131yor" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/switchbee/translations/tr.json b/homeassistant/components/switchbee/translations/tr.json new file mode 100644 index 00000000000..b3bd4cde1b1 --- /dev/null +++ b/homeassistant/components/switchbee/translations/tr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Sunucu", + "password": "Parola", + "switch_as_light": "Anahtarlar\u0131 \u0131\u015f\u0131k varl\u0131klar\u0131 olarak ba\u015flat", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Home Assistant ile SwitchBee entegrasyonunu kurun." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "devices": "Dahil edilecek cihazlar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/et.json b/homeassistant/components/tasmota/translations/et.json index 09ba6e5c328..7eae8671327 100644 --- a/homeassistant/components/tasmota/translations/et.json +++ b/homeassistant/components/tasmota/translations/et.json @@ -16,5 +16,15 @@ "description": "Kas soovid seadistada Tasmota sidumist?" } } + }, + "issues": { + "topic_duplicated": { + "description": "Mitmed Tasmota seadmed jagavad teemat {topic} . \n\n Selle probleemiga Tasmota seadmed: {offenders} .", + "title": "Mitu Tasmota seadet jagavad sama teemat" + }, + "topic_no_prefix": { + "description": "Tasmota seade {name} koos IP-ga {ip} ei sisalda t\u00e4isteemas '%prefix%'.\n\nSelle seadme olemid on keelatud, kuni konfiguratsioon on parandatud.", + "title": "Tasmota seadmel {name} on sobimatu MQTT teema" + } } } \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/tr.json b/homeassistant/components/tasmota/translations/tr.json index 71c38ef1bc0..2de7d34dcf4 100644 --- a/homeassistant/components/tasmota/translations/tr.json +++ b/homeassistant/components/tasmota/translations/tr.json @@ -16,5 +16,15 @@ "description": "Tasmota'y\u0131 kurmak istiyor musunuz?" } } + }, + "issues": { + "topic_duplicated": { + "description": "Birka\u00e7 Tasmota cihaz\u0131 {topic} konusunu payla\u015f\u0131yor. \n\n Bu soruna sahip Tasmota cihazlar\u0131: {offenders} .", + "title": "Birka\u00e7 Tasmota cihaz\u0131 ayn\u0131 konuyu payla\u015f\u0131yor" + }, + "topic_no_prefix": { + "description": "{ip} IP'sine sahip Tasmota cihaz\u0131 {name} , konusunun tamam\u0131nda ` %prefix% ` i\u00e7ermiyor. \n\n Bu cihazlar i\u00e7in varl\u0131klar, konfig\u00fcrasyon d\u00fczeltilene kadar devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131r.", + "title": "{name} Tasmota cihaz\u0131nda ge\u00e7ersiz bir MQTT konusu var" + } } } \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/tr.json b/homeassistant/components/tautulli/translations/tr.json index b52d2a7abad..e5fd6c14b67 100644 --- a/homeassistant/components/tautulli/translations/tr.json +++ b/homeassistant/components/tautulli/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, diff --git a/homeassistant/components/uptime/translations/tr.json b/homeassistant/components/uptime/translations/tr.json index ed090a38398..72132e5e979 100644 --- a/homeassistant/components/uptime/translations/tr.json +++ b/homeassistant/components/uptime/translations/tr.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "YAML kullanarak \u00c7al\u0131\u015fma S\u00fcresini yap\u0131land\u0131rma kald\u0131r\u0131ld\u0131. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lm\u0131yor. \n\n YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Uptime YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" + } + }, "title": "\u00c7al\u0131\u015fma S\u00fcresi" } \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/et.json b/homeassistant/components/volvooncall/translations/et.json index 740e0bbc68c..9f2912b5d53 100644 --- a/homeassistant/components/volvooncall/translations/et.json +++ b/homeassistant/components/volvooncall/translations/et.json @@ -15,6 +15,7 @@ "password": "Salas\u00f5na", "region": "Piirkond", "scandinavian_miles": "Kasuta Scandinavian Miles", + "unit_system": "\u00dchikute s\u00fcsteem", "username": "Kasutajanimi" } } diff --git a/homeassistant/components/volvooncall/translations/tr.json b/homeassistant/components/volvooncall/translations/tr.json index 0b56c9b67b6..4db94970086 100644 --- a/homeassistant/components/volvooncall/translations/tr.json +++ b/homeassistant/components/volvooncall/translations/tr.json @@ -15,6 +15,7 @@ "password": "Parola", "region": "B\u00f6lge", "scandinavian_miles": "\u0130skandinav Millerini Kullan\u0131n", + "unit_system": "Birim Sistemi", "username": "Kullan\u0131c\u0131 Ad\u0131" } } diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index 6d39f4b6539..8e49e6a335d 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -6,16 +6,64 @@ "usb_probe_failed": "USB seadme k\u00fcsitlemine eba\u00f5nnestus" }, "error": { - "cannot_connect": "\u00dchendamine nurjus" + "cannot_connect": "\u00dchendamine nurjus", + "invalid_backup_json": "Sobimatu varukoopia JSON" }, "flow_title": "{name}", "step": { + "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Vali automaatne varundamine" + }, + "description": "Taasta v\u00f5rgu seaded automaatsest varukoopiast", + "title": "Taastamine automaatsest varukoopiast" + }, + "choose_formation_strategy": { + "description": "Vali raadio v\u00f5rguseaded.", + "menu_options": { + "choose_automatic_backup": "Taastamine automaatsest varukoopiast", + "form_new_network": "Kustuta v\u00f5rgu seaded ja moodusta uus v\u00f5rk", + "reuse_settings": "Raadiov\u00f5rgu s\u00e4tete s\u00e4ilitamine", + "upload_manual_backup": "Varukoopia \u00fcleslaadimine" + }, + "title": "V\u00f5rgu moodustamine" + }, + "choose_serial_port": { + "data": { + "path": "Jadaseadme tee" + }, + "description": "Vali Zigbee raadio jadaport", + "title": "Vali jadaport" + }, "confirm": { "description": "Kas soovid seadistada teenust {name} ?" }, "confirm_hardware": { "description": "Kas seadistada {name} ?" }, + "manual_pick_radio_type": { + "data": { + "radio_type": "Seadme raadio t\u00fc\u00fcp" + }, + "description": "Vali Zigbee raadio t\u00fc\u00fcp", + "title": "Seadme raadio t\u00fc\u00fcp" + }, + "manual_port_config": { + "data": { + "baudrate": "pordi kiirus", + "flow_control": "andmevoo juhtimine", + "path": "Jadaseadme tee" + }, + "description": "Sisesta jadapordi s\u00e4tted", + "title": "Jadapordi s\u00e4tted" + }, + "maybe_confirm_ezsp_restore": { + "data": { + "overwrite_coordinator_ieee": "Raadio IEEE-aadressi p\u00fcsiv asendamine" + }, + "description": "Varukoopial on erinev IEEE aadress kui raadiol. V\u00f5rgu n\u00f5uetekohaseks toimimiseks tuleks muuta ka raadio IEEE aadressi.\n\nSee on p\u00fcsiv toiming.", + "title": "Raadio IEEE aadressi \u00fclekirjutamine" + }, "pick_radio": { "data": { "radio_type": "Seadme raadio t\u00fc\u00fcp" @@ -32,6 +80,13 @@ "description": "Sisesta pordispetsiifilised seaded", "title": "Seaded" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "Faili \u00fcleslaadimine" + }, + "description": "Taasta oma v\u00f5rgus\u00e4tted \u00fcleslaaditud JSON-varufailist. Saad selle alla laadida teisest ZHA paigaldusest **V\u00f5rguseaded** v\u00f5i kasutada Zigbee2MQTT 'coordinator_backup.json' faili.", + "title": "K\u00e4sitsi varundamise \u00fcleslaadimine" + }, "user": { "data": { "path": "Jadaseadme tee" @@ -61,6 +116,8 @@ }, "device_automation": { "action_type": { + "issue_all_led_effect": "K\u00f5igi LED-ide efekt", + "issue_individual_led_effect": "Efekt \u00fcksikute LEDide puhul", "squawk": "Pr\u00e4\u00e4ksata", "warn": "Hoiata" }, @@ -116,8 +173,22 @@ } }, "options": { + "abort": { + "not_zha_device": "See ei ole zha seade", + "single_instance_allowed": "Juba h\u00e4\u00e4lestatud. V\u00f5imalik on ainult \u00fcks sidumine.", + "usb_probe_failed": "USB seadme k\u00fcsitlemine nurjus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_backup_json": "Sobimatu JSON varundus kirje" + }, + "flow_title": "{name}", "step": { "choose_automatic_backup": { + "data": { + "choose_automatic_backup": "Vali automaatse varunduse kirje" + }, + "description": "Taasta v\u00f5rgus\u00e4tted automaatvarunduse kirjest", "title": "Taasta automaatvarundusest" }, "choose_formation_strategy": { @@ -161,12 +232,14 @@ "data": { "overwrite_coordinator_ieee": "Asenda IEEE aadress j\u00e4\u00e4davalt" }, + "description": "Varukoopial on erinev IEEE aadress kui raadiol. V\u00f5rgu n\u00f5uetekohaseks toimimiseks tuleks muuta ka raadio IEEE aadressi.\n\nSee on p\u00fcsiv toiming.", "title": "Kirjuta IEEE aadress \u00fcle" }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Lae kirje \u00fcles" }, + "description": "Taasta oma v\u00f5rgus\u00e4tted \u00fcleslaaditud JSON-varufailist. Saad selle alla laadida teisest ZHA paigaldusest **V\u00f5rguseaded** v\u00f5i kasutada Zigbee2MQTT 'coordinator_backup.json' faili.", "title": "Lae k\u00e4sitsi loodud varukoopia \u00fcles" } } diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index 9061246043a..dac86f55d38 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -116,6 +116,8 @@ }, "device_automation": { "action_type": { + "issue_all_led_effect": "Effekt minden LED-re", + "issue_individual_led_effect": "Effekt egyes LED-ekre", "squawk": "Riaszt\u00e1s", "warn": "Figyelmeztet\u00e9s" }, diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index 6ac69d568a7..5928e60a139 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -116,6 +116,8 @@ }, "device_automation": { "action_type": { + "issue_all_led_effect": "Efekt dla wszystkich LED-\u00f3w", + "issue_individual_led_effect": "Efekt dla poszczeg\u00f3lnych LED-\u00f3w", "squawk": "squawk", "warn": "ostrze\u017cenie" }, diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index 2ec2b97438a..460deeac1f1 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -116,6 +116,8 @@ }, "device_automation": { "action_type": { + "issue_all_led_effect": "Efeito de emiss\u00e3o para todos os LEDs", + "issue_individual_led_effect": "Efeito de emiss\u00e3o para LED individual", "squawk": "Squawk", "warn": "Aviso" }, diff --git a/homeassistant/components/zha/translations/tr.json b/homeassistant/components/zha/translations/tr.json index 3d3859f745c..6ee4ff515ed 100644 --- a/homeassistant/components/zha/translations/tr.json +++ b/homeassistant/components/zha/translations/tr.json @@ -116,6 +116,8 @@ }, "device_automation": { "action_type": { + "issue_all_led_effect": "T\u00fcm LED'ler i\u00e7in sorun efekti", + "issue_individual_led_effect": "Bireysel LED i\u00e7in sorun efekti", "squawk": "Squawk", "warn": "Uyarmak" }, diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index 0fbd233bc60..9f36a3f03a9 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -116,6 +116,8 @@ }, "device_automation": { "action_type": { + "issue_all_led_effect": "\u57f7\u884c\u5168\u90e8 LED \u6548\u679c", + "issue_individual_led_effect": "\u57f7\u884c\u500b\u5225 LED \u6548\u679c", "squawk": "\u61c9\u7b54", "warn": "\u8b66\u544a" }, diff --git a/homeassistant/components/zwave_js/translations/et.json b/homeassistant/components/zwave_js/translations/et.json index ea0686e424f..e74301c8ce4 100644 --- a/homeassistant/components/zwave_js/translations/et.json +++ b/homeassistant/components/zwave_js/translations/et.json @@ -91,6 +91,12 @@ "zwave_js.value_updated.value": "Z-Wave JS v\u00e4\u00e4rtuse muutus" } }, + "issues": { + "invalid_server_version": { + "description": "Z-Wave JS Serveri versioon, mida praegu kasutad, on selle Home Assistanti versiooni jaoks liiga vana. Selle probleemi lahendamiseks v\u00e4rskenda Z-Wave JS Server uusimale versioonile.", + "title": "Vajalik on Z-Wave JS Serveri uuem versioon" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Z-Wave JS lisandmooduli tuvastusteabe hankimine nurjus.", diff --git a/homeassistant/components/zwave_js/translations/tr.json b/homeassistant/components/zwave_js/translations/tr.json index 21d8f03bec6..edf75d1dfb7 100644 --- a/homeassistant/components/zwave_js/translations/tr.json +++ b/homeassistant/components/zwave_js/translations/tr.json @@ -91,6 +91,12 @@ "zwave_js.value_updated.value": "Z-Wave JS De\u011ferinde de\u011fer de\u011fi\u015fikli\u011fi" } }, + "issues": { + "invalid_server_version": { + "description": "\u015eu anda \u00e7al\u0131\u015ft\u0131rd\u0131\u011f\u0131n\u0131z Z-Wave JS Server s\u00fcr\u00fcm\u00fc, Home Assistant'\u0131n bu s\u00fcr\u00fcm\u00fc i\u00e7in \u00e7ok eski. Bu sorunu gidermek i\u00e7in l\u00fctfen Z-Wave JS Sunucusunu en son s\u00fcr\u00fcme g\u00fcncelleyin.", + "title": "Z-Wave JS Sunucusunun daha yeni s\u00fcr\u00fcm\u00fc gerekli" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Z-Wave JS eklenti ke\u015fif bilgileri al\u0131namad\u0131.", From 2688e5b2d404100db86f5d6db6fdc7f95fd2b324 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Thu, 29 Sep 2022 18:07:26 -0700 Subject: [PATCH 044/985] Mask spotify content in owntone library (#79247) --- .../components/forked_daapd/browse_media.py | 4 +++ .../forked_daapd/test_browse_media.py | 28 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/forked_daapd/browse_media.py b/homeassistant/components/forked_daapd/browse_media.py index 88ca9ad60f8..099a042f58a 100644 --- a/homeassistant/components/forked_daapd/browse_media.py +++ b/homeassistant/components/forked_daapd/browse_media.py @@ -229,6 +229,10 @@ def create_browse_media_response( if not children: # Directory searches will pass in subdirectories as children children = [] for item in result: + if item.get("data_kind") == "spotify" or ( + "path" in item and cast(str, item["path"]).startswith("spotify") + ): # Exclude spotify data from Owntone library + continue assert isinstance(item["uri"], str) media_type = OWNTONE_TYPE_TO_MEDIA_TYPE[item["uri"].split(":")[1]] title = item.get("name") or item.get("title") # only tracks use title diff --git a/tests/components/forked_daapd/test_browse_media.py b/tests/components/forked_daapd/test_browse_media.py index 23fc9fcf6eb..957c52a88c5 100644 --- a/tests/components/forked_daapd/test_browse_media.py +++ b/tests/components/forked_daapd/test_browse_media.py @@ -4,7 +4,11 @@ from http import HTTPStatus from unittest.mock import patch from homeassistant.components import media_source, spotify -from homeassistant.components.forked_daapd.browse_media import create_media_content_id +from homeassistant.components.forked_daapd.browse_media import ( + MediaContent, + create_media_content_id, + is_owntone_media_content_id, +) from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType from homeassistant.components.spotify.const import ( MEDIA_PLAYER_PREFIX as SPOTIFY_MEDIA_PLAYER_PREFIX, @@ -111,6 +115,16 @@ async def test_async_browse_media(hass, hass_ws_client, config_entry): "length_ms": 2951554, "uri": "library:artist:3815427709949443149", }, + { + "id": "456", + "name": "Spotify Artist", + "name_sort": "Spotify Artist", + "album_count": 1, + "track_count": 10, + "length_ms": 2254, + "uri": "spotify:artist:abc123", + "data_kind": "spotify", + }, ] mock_api.return_value.get_genres.return_value = [ {"name": "Classical"}, @@ -127,6 +141,13 @@ async def test_async_browse_media(hass, hass_ws_client, config_entry): "smart_playlist": False, "uri": "library:playlist:1", }, + { + "id": 2, + "name": "Spotify Playlist", + "path": "spotify:playlist:abc123", + "smart_playlist": False, + "uri": "library:playlist:2", + }, ] # Request browse root through WebSocket @@ -150,6 +171,11 @@ async def test_async_browse_media(hass, hass_ws_client, config_entry): """Browse the children of this BrowseMedia.""" nonlocal msg_id for child in children: + # Assert Spotify content is not passed through as Owntone media + assert not ( + is_owntone_media_content_id(child["media_content_id"]) + and "Spotify" in MediaContent(child["media_content_id"]).title + ) if child["can_expand"]: await client.send_json( { From aa6f15b1e2342af494ac98dd1d567a6444c52e19 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Fri, 30 Sep 2022 03:51:18 +0200 Subject: [PATCH 045/985] Use SensorDeviceClass.VOLUME in HomeWizard (#79323) Co-authored-by: Paulus Schoutsen --- homeassistant/components/homewizard/sensor.py | 2 +- tests/components/homewizard/test_sensor.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index a66a2664ae1..df4eb0c4880 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -130,7 +130,7 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( key="total_liter_m3", name="Total water usage", native_unit_of_measurement=VOLUME_CUBIC_METERS, - icon="mdi:gauge", + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL_INCREASING, ), ) diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index 145a2719b01..7d350764b2b 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -609,8 +609,8 @@ async def test_sensor_entity_total_liters( assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS - assert ATTR_DEVICE_CLASS not in state.attributes - assert state.attributes.get(ATTR_ICON) == "mdi:gauge" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLUME + assert state.attributes.get(ATTR_ICON) is None async def test_sensor_entity_disabled_when_null( From 6e893d9162331e09895b5b9be21af057c70b9e3d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Sep 2022 22:21:00 -0400 Subject: [PATCH 046/985] Store alternative domain for Zeroconf homekit discovery (#79240) --- homeassistant/components/zeroconf/__init__.py | 10 +++++++++- tests/components/zeroconf/test_init.py | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index e6c635dc308..5a2fc61f897 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -404,6 +404,7 @@ class ZeroconfDiscovery: _LOGGER.debug("Discovered new device %s %s", name, info) props: dict[str, str] = info.properties + domain = None # If we can handle it as a HomeKit discovery, we do that here. if service_type in HOMEKIT_TYPES and ( @@ -458,10 +459,17 @@ class ZeroconfDiscovery: matcher_domain = matcher["domain"] assert isinstance(matcher_domain, str) + context = { + "source": config_entries.SOURCE_ZEROCONF, + } + if domain: + # Domain of integration that offers alternative API to handle this device. + context["alternative_domain"] = domain + discovery_flow.async_create_flow( self.hass, matcher_domain, - {"source": config_entries.SOURCE_ZEROCONF}, + context, info, ) diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 6bc37e10da2..0de9929fcf8 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -327,6 +327,7 @@ async def test_zeroconf_match_macaddress(hass, mock_async_zeroconf): 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" + assert mock_config_flow.mock_calls[0][2]["context"] == {"source": "zeroconf"} async def test_zeroconf_match_manufacturer(hass, mock_async_zeroconf): @@ -533,6 +534,10 @@ async def test_homekit_match_partial_space(hass, mock_async_zeroconf): # One for HKC, and one for LIFX since lifx is local polling assert len(mock_config_flow.mock_calls) == 2 assert mock_config_flow.mock_calls[0][1][0] == "lifx" + assert mock_config_flow.mock_calls[1][2]["context"] == { + "source": "zeroconf", + "alternative_domain": "lifx", + } async def test_homekit_match_partial_dash(hass, mock_async_zeroconf): From 0001270badcfead18d48e004b0ff77eb6f065dcc Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 29 Sep 2022 22:13:09 -0500 Subject: [PATCH 047/985] Add Third Reality to Zigbee Iot standards (#79341) --- homeassistant/brands/third_reality.json | 5 +++++ homeassistant/generated/integrations.json | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 homeassistant/brands/third_reality.json diff --git a/homeassistant/brands/third_reality.json b/homeassistant/brands/third_reality.json new file mode 100644 index 00000000000..172b74c42fc --- /dev/null +++ b/homeassistant/brands/third_reality.json @@ -0,0 +1,5 @@ +{ + "domain": "third_reality", + "name": "Third Reality", + "iot_standards": ["zigbee"] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 5a97ffa2dd0..eb5c1c8fefb 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4422,6 +4422,12 @@ "iot_class": "local_polling", "name": "Thinking Cleaner" }, + "third_reality": { + "name": "Third Reality", + "iot_standards": [ + "zigbee" + ] + }, "thomson": { "config_flow": false, "iot_class": "local_polling", From bc2dffabc477ebb261a4ab312f587f5d15e90e91 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 30 Sep 2022 08:38:44 +0200 Subject: [PATCH 048/985] Improve naming of units used in statistics (#79276) --- homeassistant/components/recorder/core.py | 6 +- .../components/recorder/statistics.py | 12 +-- homeassistant/components/recorder/tasks.py | 6 +- .../components/recorder/websocket_api.py | 15 ++-- tests/components/demo/test_init.py | 4 +- tests/components/recorder/test_statistics.py | 6 +- .../components/recorder/test_websocket_api.py | 46 +++++------ tests/components/sensor/test_recorder.py | 76 +++++++++---------- 8 files changed, 88 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 17828a2e87e..c0f19f2e864 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -485,11 +485,13 @@ class Recorder(threading.Thread): statistic_id: str, start_time: datetime, sum_adjustment: float, - display_unit: str, + adjustment_unit: str, ) -> None: """Adjust statistics.""" self.queue_task( - AdjustStatisticsTask(statistic_id, start_time, sum_adjustment, display_unit) + AdjustStatisticsTask( + statistic_id, start_time, sum_adjustment, adjustment_unit + ) ) @callback diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 6a594827a5c..a0ff73b10fd 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -899,7 +899,7 @@ def list_statistic_ids( result = { meta["statistic_id"]: { - "display_unit_of_measurement": meta["state_unit_of_measurement"], + "state_unit_of_measurement": meta["state_unit_of_measurement"], "has_mean": meta["has_mean"], "has_sum": meta["has_sum"], "name": meta["name"], @@ -926,7 +926,7 @@ def list_statistic_ids( "has_sum": meta["has_sum"], "name": meta["name"], "source": meta["source"], - "display_unit_of_measurement": meta["state_unit_of_measurement"], + "state_unit_of_measurement": meta["state_unit_of_measurement"], "unit_class": _get_unit_class(meta["unit_of_measurement"]), "unit_of_measurement": meta["unit_of_measurement"], } @@ -939,7 +939,7 @@ def list_statistic_ids( "has_sum": info["has_sum"], "name": info.get("name"), "source": info["source"], - "display_unit_of_measurement": info["display_unit_of_measurement"], + "state_unit_of_measurement": info["state_unit_of_measurement"], "statistics_unit_of_measurement": info["unit_of_measurement"], "unit_class": info["unit_class"], } @@ -1605,7 +1605,7 @@ def adjust_statistics( statistic_id: str, start_time: datetime, sum_adjustment: float, - display_unit: str, + adjustment_unit: str, ) -> bool: """Process an add_statistics job.""" @@ -1617,7 +1617,9 @@ def adjust_statistics( return True statistic_unit = metadata[statistic_id][1]["unit_of_measurement"] - convert = _get_display_to_statistic_unit_converter(display_unit, statistic_unit) + convert = _get_display_to_statistic_unit_converter( + adjustment_unit, statistic_unit + ) sum_adjustment = convert(sum_adjustment) _adjust_sum_statistics( diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index 63fb14cc598..4fa3a3cc40c 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -163,7 +163,7 @@ class AdjustStatisticsTask(RecorderTask): statistic_id: str start_time: datetime sum_adjustment: float - display_unit: str + adjustment_unit: str def run(self, instance: Recorder) -> None: """Run statistics task.""" @@ -172,7 +172,7 @@ class AdjustStatisticsTask(RecorderTask): self.statistic_id, self.start_time, self.sum_adjustment, - self.display_unit, + self.adjustment_unit, ): return # Schedule a new adjust statistics task if this one didn't finish @@ -181,7 +181,7 @@ class AdjustStatisticsTask(RecorderTask): self.statistic_id, self.start_time, self.sum_adjustment, - self.display_unit, + self.adjustment_unit, ) ) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index d841233fa5b..02b7519486d 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -291,7 +291,7 @@ def ws_change_statistics_unit( vol.Required("statistic_id"): str, vol.Required("start_time"): str, vol.Required("adjustment"): vol.Any(float, int), - vol.Required("display_unit"): vol.Any(str, None), + vol.Required("adjustment_unit_of_measurement"): vol.Any(str, None), } ) @websocket_api.async_response @@ -320,25 +320,26 @@ async def ws_adjust_sum_statistics( return metadata = metadatas[0] - def valid_units(statistics_unit: str | None, display_unit: str | None) -> bool: - if statistics_unit == display_unit: + def valid_units(statistics_unit: str | None, adjustment_unit: str | None) -> bool: + if statistics_unit == adjustment_unit: return True converter = STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistics_unit) - if converter is not None and display_unit in converter.VALID_UNITS: + if converter is not None and adjustment_unit in converter.VALID_UNITS: return True return False stat_unit = metadata["statistics_unit_of_measurement"] - if not valid_units(stat_unit, msg["display_unit"]): + adjustment_unit = msg["adjustment_unit_of_measurement"] + if not valid_units(stat_unit, adjustment_unit): connection.send_error( msg["id"], "invalid_units", - f"Can't convert {stat_unit} to {msg['display_unit']}", + f"Can't convert {stat_unit} to {adjustment_unit}", ) return get_instance(hass).async_adjust_statistics( - msg["statistic_id"], start_time, msg["adjustment"], msg["display_unit"] + msg["statistic_id"], start_time, msg["adjustment"], adjustment_unit ) connection.send_result(msg["id"]) diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 934321a0ed8..8c3adeb1c98 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -63,21 +63,21 @@ async def test_demo_statistics(hass, recorder_mock): list_statistic_ids, hass ) assert { - "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": "Outdoor temperature", "source": "demo", + "state_unit_of_measurement": "°C", "statistic_id": "demo:temperature_outdoor", "statistics_unit_of_measurement": "°C", "unit_class": "temperature", } in statistic_ids assert { - "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": "Energy consumption 1", "source": "demo", + "state_unit_of_measurement": "kWh", "statistic_id": "demo:energy_consumption_kwh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 4fc98333bf4..c96b984bcf4 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -525,12 +525,12 @@ async def test_import_statistics( statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { - "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, "name": "Total imported energy", "source": source, + "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -621,12 +621,12 @@ async def test_import_statistics( statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { - "display_unit_of_measurement": "MWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, "name": "Total imported energy renamed", "source": source, + "state_unit_of_measurement": "MWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -682,7 +682,7 @@ async def test_import_statistics( "statistic_id": statistic_id, "start_time": period2.isoformat(), "adjustment": 1000.0, - "display_unit": "MWh", + "adjustment_unit_of_measurement": "MWh", } ) response = await client.receive_json() diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index e8d8093e131..4d4a1604a91 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -651,7 +651,7 @@ async def test_list_statistic_ids( "has_sum": has_sum, "name": None, "source": "recorder", - "display_unit_of_measurement": display_unit, + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -673,7 +673,7 @@ async def test_list_statistic_ids( "has_sum": has_sum, "name": None, "source": "recorder", - "display_unit_of_measurement": display_unit, + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -698,7 +698,7 @@ async def test_list_statistic_ids( "has_sum": has_sum, "name": None, "source": "recorder", - "display_unit_of_measurement": display_unit, + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -719,7 +719,7 @@ async def test_list_statistic_ids( "has_sum": has_sum, "name": None, "source": "recorder", - "display_unit_of_measurement": display_unit, + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -903,11 +903,11 @@ async def test_update_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", - "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": "kW", "unit_class": None, } @@ -931,11 +931,11 @@ async def test_update_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", - "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": new_unit, "unit_class": new_unit_class, } @@ -995,11 +995,11 @@ async def test_change_statistics_unit(hass, hass_ws_client, recorder_mock): assert response["result"] == [ { "statistic_id": "sensor.test", - "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": "kW", "unit_class": None, } @@ -1051,11 +1051,11 @@ async def test_change_statistics_unit(hass, hass_ws_client, recorder_mock): assert response["result"] == [ { "statistic_id": "sensor.test", - "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": "W", "unit_class": "power", } @@ -1104,11 +1104,11 @@ async def test_change_statistics_unit_errors( expected_statistic_ids = [ { "statistic_id": "sensor.test", - "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": "kW", "unit_class": None, } @@ -1483,11 +1483,11 @@ async def test_get_statistics_metadata( assert response["result"] == [ { "statistic_id": "test:total_gas", - "display_unit_of_measurement": unit, "has_mean": has_mean, "has_sum": has_sum, "name": "Total imported energy", "source": "test", + "state_unit_of_measurement": unit, "statistics_unit_of_measurement": unit, "unit_class": unit_class, } @@ -1511,11 +1511,11 @@ async def test_get_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", - "display_unit_of_measurement": attributes["unit_of_measurement"], "has_mean": has_mean, "has_sum": has_sum, "name": None, "source": "recorder", + "state_unit_of_measurement": attributes["unit_of_measurement"], "statistics_unit_of_measurement": unit, "unit_class": unit_class, } @@ -1539,11 +1539,11 @@ async def test_get_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", - "display_unit_of_measurement": attributes["unit_of_measurement"], "has_mean": has_mean, "has_sum": has_sum, "name": None, "source": "recorder", + "state_unit_of_measurement": attributes["unit_of_measurement"], "statistics_unit_of_measurement": unit, "unit_class": unit_class, } @@ -1635,12 +1635,12 @@ async def test_import_statistics( statistic_ids = list_statistic_ids(hass) # TODO assert statistic_ids == [ { - "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, "name": "Total imported energy", "source": source, + "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -1864,12 +1864,12 @@ async def test_adjust_sum_statistics_energy( statistic_ids = list_statistic_ids(hass) # TODO assert statistic_ids == [ { - "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, "name": "Total imported energy", "source": source, + "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -1898,7 +1898,7 @@ async def test_adjust_sum_statistics_energy( "statistic_id": statistic_id, "start_time": period2.isoformat(), "adjustment": 1000.0, - "display_unit": "kWh", + "adjustment_unit_of_measurement": "kWh", } ) response = await client.receive_json() @@ -1941,7 +1941,7 @@ async def test_adjust_sum_statistics_energy( "statistic_id": statistic_id, "start_time": period2.isoformat(), "adjustment": 2.0, - "display_unit": "MWh", + "adjustment_unit_of_measurement": "MWh", } ) response = await client.receive_json() @@ -2062,12 +2062,12 @@ async def test_adjust_sum_statistics_gas( statistic_ids = list_statistic_ids(hass) # TODO assert statistic_ids == [ { - "display_unit_of_measurement": "m³", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, "name": "Total imported energy", "source": source, + "state_unit_of_measurement": "m³", "statistics_unit_of_measurement": "m³", "unit_class": "volume", } @@ -2096,7 +2096,7 @@ async def test_adjust_sum_statistics_gas( "statistic_id": statistic_id, "start_time": period2.isoformat(), "adjustment": 1000.0, - "display_unit": "m³", + "adjustment_unit_of_measurement": "m³", } ) response = await client.receive_json() @@ -2139,7 +2139,7 @@ async def test_adjust_sum_statistics_gas( "statistic_id": statistic_id, "start_time": period2.isoformat(), "adjustment": 35.3147, # ~1 m³ - "display_unit": "ft³", + "adjustment_unit_of_measurement": "ft³", } ) response = await client.receive_json() @@ -2276,12 +2276,12 @@ async def test_adjust_sum_statistics_errors( statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { - "display_unit_of_measurement": state_unit, "has_mean": False, "has_sum": True, "statistic_id": statistic_id, "name": "Total imported energy", "source": source, + "state_unit_of_measurement": state_unit, "statistics_unit_of_measurement": statistic_unit, "unit_class": unit_class, } @@ -2311,7 +2311,7 @@ async def test_adjust_sum_statistics_errors( "statistic_id": "sensor.does_not_exist", "start_time": period2.isoformat(), "adjustment": 1000.0, - "display_unit": statistic_unit, + "adjustment_unit_of_measurement": statistic_unit, } ) response = await client.receive_json() @@ -2331,7 +2331,7 @@ async def test_adjust_sum_statistics_errors( "statistic_id": statistic_id, "start_time": period2.isoformat(), "adjustment": 1000.0, - "display_unit": unit, + "adjustment_unit_of_measurement": unit, } ) response = await client.receive_json() @@ -2351,7 +2351,7 @@ async def test_adjust_sum_statistics_errors( "statistic_id": statistic_id, "start_time": period2.isoformat(), "adjustment": 1000.0, - "display_unit": unit, + "adjustment_unit_of_measurement": unit, } ) response = await client.receive_json() diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 9bddaa8af71..f0013874e23 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -136,11 +136,11 @@ def test_compile_hourly_statistics( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -210,12 +210,12 @@ def test_compile_hourly_statistics_purged_state_changes( statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { - "display_unit_of_measurement": display_unit, "statistic_id": "sensor.test1", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -281,31 +281,31 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "°C", "statistics_unit_of_measurement": "°C", "unit_class": "temperature", }, { "statistic_id": "sensor.test6", - "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "°C", "statistics_unit_of_measurement": "°C", "unit_class": "temperature", }, { "statistic_id": "sensor.test7", - "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "°C", "statistics_unit_of_measurement": "°C", "unit_class": "temperature", }, @@ -436,11 +436,11 @@ async def test_compile_hourly_sum_statistics_amount( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -516,7 +516,7 @@ async def test_compile_hourly_sum_statistics_amount( "statistic_id": "sensor.test1", "start_time": period1.isoformat(), "adjustment": 100.0, - "display_unit": display_unit, + "adjustment_unit_of_measurement": display_unit, } ) response = await client.receive_json() @@ -536,7 +536,7 @@ async def test_compile_hourly_sum_statistics_amount( "statistic_id": "sensor.test1", "start_time": period2.isoformat(), "adjustment": -400.0, - "display_unit": display_unit, + "adjustment_unit_of_measurement": display_unit, } ) response = await client.receive_json() @@ -629,11 +629,11 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -730,11 +730,11 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -815,11 +815,11 @@ def test_compile_hourly_sum_statistics_nan_inf_state( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -929,11 +929,11 @@ def test_compile_hourly_sum_statistics_negative_state( wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert { - "name": None, - "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, + "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistic_id": entity_id, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, @@ -1018,11 +1018,11 @@ def test_compile_hourly_sum_statistics_total_no_reset( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -1121,11 +1121,11 @@ def test_compile_hourly_sum_statistics_total_increasing( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -1235,11 +1235,11 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -1330,11 +1330,11 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -1423,31 +1423,31 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", }, { "statistic_id": "sensor.test2", - "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", }, { "statistic_id": "sensor.test3", - "display_unit_of_measurement": "Wh", "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": "Wh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", }, @@ -1807,11 +1807,11 @@ def test_list_statistic_ids( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": statistic_type == "mean", "has_sum": statistic_type == "sum", "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -1822,11 +1822,11 @@ def test_list_statistic_ids( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": statistic_type == "mean", "has_sum": statistic_type == "sum", "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -1913,11 +1913,11 @@ def test_compile_hourly_statistics_changing_units_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -1949,11 +1949,11 @@ def test_compile_hourly_statistics_changing_units_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -2025,11 +2025,11 @@ def test_compile_hourly_statistics_changing_units_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": "cats", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "cats", "statistics_unit_of_measurement": "cats", "unit_class": unit_class, }, @@ -2091,11 +2091,11 @@ def test_compile_hourly_statistics_changing_units_3( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -2127,11 +2127,11 @@ def test_compile_hourly_statistics_changing_units_3( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -2193,11 +2193,11 @@ def test_compile_hourly_statistics_changing_device_class_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": state_unit, "statistics_unit_of_measurement": state_unit, "unit_class": unit_class, }, @@ -2239,11 +2239,11 @@ def test_compile_hourly_statistics_changing_device_class_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": state_unit, "statistics_unit_of_measurement": state_unit, "unit_class": unit_class, }, @@ -2302,11 +2302,11 @@ def test_compile_hourly_statistics_changing_device_class_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": state_unit, "statistics_unit_of_measurement": state_unit, "unit_class": unit_class, }, @@ -2382,11 +2382,11 @@ def test_compile_hourly_statistics_changing_device_class_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistic_unit, "unit_class": unit_class, }, @@ -2432,11 +2432,11 @@ def test_compile_hourly_statistics_changing_device_class_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistic_unit, "unit_class": unit_class, }, @@ -2502,11 +2502,11 @@ def test_compile_hourly_statistics_changing_statistics( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": None, "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": None, "statistics_unit_of_measurement": None, "unit_class": None, }, @@ -2539,11 +2539,11 @@ def test_compile_hourly_statistics_changing_statistics( assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": None, "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": None, "statistics_unit_of_measurement": None, "unit_class": None, }, @@ -2734,41 +2734,41 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): assert statistic_ids == [ { "statistic_id": "sensor.test1", - "display_unit_of_measurement": "%", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "%", "statistics_unit_of_measurement": "%", "unit_class": None, }, { "statistic_id": "sensor.test2", - "display_unit_of_measurement": "%", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "%", "statistics_unit_of_measurement": "%", "unit_class": None, }, { "statistic_id": "sensor.test3", - "display_unit_of_measurement": "%", "has_mean": True, "has_sum": False, "name": None, "source": "recorder", + "state_unit_of_measurement": "%", "statistics_unit_of_measurement": "%", "unit_class": None, }, { "statistic_id": "sensor.test4", - "display_unit_of_measurement": "EUR", "has_mean": False, "has_sum": True, "name": None, "source": "recorder", + "state_unit_of_measurement": "EUR", "statistics_unit_of_measurement": "EUR", "unit_class": None, }, From 1cb5a453797378d796daeef5042d086d32808cab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Sep 2022 08:52:49 +0200 Subject: [PATCH 049/985] Bump actions/cache from 3.0.8 to 3.0.9 (#79344) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 07b3ee148d7..1071c23b4e5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -177,7 +177,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.9 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -190,7 +190,7 @@ jobs: pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.9 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -216,7 +216,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.9 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -227,7 +227,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.9 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -265,7 +265,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.9 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -276,7 +276,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.9 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -317,7 +317,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.9 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -328,7 +328,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.9 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -358,7 +358,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.9 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -369,7 +369,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.9 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -485,7 +485,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.9 with: path: venv key: >- @@ -493,7 +493,7 @@ jobs: needs.info.outputs.python_cache_key }} - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.9 with: path: ${{ env.PIP_CACHE }} key: >- @@ -543,7 +543,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.9 with: path: venv key: >- @@ -575,7 +575,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.9 with: path: venv key: >- @@ -608,7 +608,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.9 with: path: venv key: >- @@ -652,7 +652,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.9 with: path: venv key: >- @@ -700,7 +700,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.9 with: path: venv key: >- @@ -754,7 +754,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.9 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ From 52cdae254c619c63883bce0aad9ed740d5d3c3c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Sep 2022 22:33:14 -1000 Subject: [PATCH 050/985] Bump govee-ble to 0.19.1 to handle another H5181 (#79340) fixes #79188 --- homeassistant/components/govee_ble/manifest.json | 7 ++++++- homeassistant/generated/bluetooth.py | 6 ++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 29af7502ded..1cb1eeaddd8 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -37,6 +37,11 @@ "service_uuid": "00008551-0000-1000-8000-00805f9b34fb", "connectable": false }, + { + "manufacturer_id": 53579, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", + "connectable": false + }, { "manufacturer_id": 43682, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", @@ -68,7 +73,7 @@ "connectable": false } ], - "requirements": ["govee-ble==0.19.0"], + "requirements": ["govee-ble==0.19.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index cffcac7558c..43481ee48f1 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -88,6 +88,12 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "service_uuid": "00008551-0000-1000-8000-00805f9b34fb", "connectable": False, }, + { + "domain": "govee_ble", + "manufacturer_id": 53579, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", + "connectable": False, + }, { "domain": "govee_ble", "manufacturer_id": 43682, diff --git a/requirements_all.txt b/requirements_all.txt index db76ee15df4..3b1914d986b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -786,7 +786,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.19.0 +govee-ble==0.19.1 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d2c3c22a590..4e5e957c2f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -590,7 +590,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.19.0 +govee-ble==0.19.1 # homeassistant.components.gree greeclimate==1.3.0 From fb7079c62c491614ad7f473a6153e6f44e83746e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 30 Sep 2022 10:41:18 +0200 Subject: [PATCH 051/985] Adjust icons with new device classes (#79348) * Adjust icons with new device classes * Fix mysensors tests * Fix mysensors tests --- homeassistant/components/homewizard/sensor.py | 1 + homeassistant/components/litterrobot/sensor.py | 1 - homeassistant/components/mysensors/sensor.py | 2 -- tests/components/homewizard/test_sensor.py | 2 +- tests/components/mysensors/test_sensor.py | 3 ++- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index df4eb0c4880..6fc1d38ec12 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -130,6 +130,7 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( key="total_liter_m3", name="Total water usage", native_unit_of_measurement=VOLUME_CUBIC_METERS, + icon="mdi:gauge", device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL_INCREASING, ), diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index b9d70528cab..1857931143d 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -109,7 +109,6 @@ ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = { RobotSensorEntityDescription[LitterRobot4]( key="pet_weight", name="Pet weight", - icon="mdi:scale", native_unit_of_measurement=MASS_POUNDS, device_class=SensorDeviceClass.WEIGHT, ), diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 59c33a48884..f21d343f9c3 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -95,13 +95,11 @@ SENSORS: dict[str, SensorEntityDescription] = { key="V_WEIGHT", native_unit_of_measurement=MASS_KILOGRAMS, device_class=SensorDeviceClass.WEIGHT, - icon="mdi:weight-kilogram", ), "V_DISTANCE": SensorEntityDescription( key="V_DISTANCE", native_unit_of_measurement=LENGTH_METERS, device_class=SensorDeviceClass.DISTANCE, - icon="mdi:ruler", ), "V_IMPEDANCE": SensorEntityDescription( key="V_IMPEDANCE", diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index 7d350764b2b..c8238c75643 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -610,7 +610,7 @@ async def test_sensor_entity_total_liters( assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLUME - assert state.attributes.get(ATTR_ICON) is None + assert state.attributes.get(ATTR_ICON) == "mdi:gauge" async def test_sensor_entity_disabled_when_null( diff --git a/tests/components/mysensors/test_sensor.py b/tests/components/mysensors/test_sensor.py index 45fe98b98c7..58258682d5b 100644 --- a/tests/components/mysensors/test_sensor.py +++ b/tests/components/mysensors/test_sensor.py @@ -117,7 +117,8 @@ async def test_distance_sensor( assert state assert state.state == "15" - assert state.attributes[ATTR_ICON] == "mdi:ruler" + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.DISTANCE + assert ATTR_ICON not in state.attributes assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "cm" From ac7b4e7569ee755fc294f6a8ddc0fa8c8d4b80e7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 30 Sep 2022 11:07:10 +0200 Subject: [PATCH 052/985] Make temperature conversions private (#79349) --- .../components/mold_indicator/sensor.py | 7 +++--- .../weather_update_coordinator.py | 5 +++- .../components/prometheus/__init__.py | 10 +++++--- homeassistant/util/unit_conversion.py | 24 +++++++++---------- 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index 23c5e639d7f..5685e76fac0 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -22,6 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util.unit_conversion import TemperatureConverter _LOGGER = logging.getLogger(__name__) @@ -218,7 +219,7 @@ class MoldIndicator(SensorEntity): # convert to celsius if necessary if unit == TEMP_FAHRENHEIT: - return util.temperature.fahrenheit_to_celsius(temp) + return TemperatureConverter.convert(temp, TEMP_FAHRENHEIT, TEMP_CELSIUS) if unit == TEMP_CELSIUS: return temp _LOGGER.error( @@ -385,13 +386,13 @@ class MoldIndicator(SensorEntity): } dewpoint = ( - util.temperature.celsius_to_fahrenheit(self._dewpoint) + TemperatureConverter.convert(self._dewpoint, TEMP_CELSIUS, TEMP_FAHRENHEIT) if self._dewpoint is not None else None ) crit_temp = ( - util.temperature.celsius_to_fahrenheit(self._crit_temp) + TemperatureConverter.convert(self._crit_temp, TEMP_CELSIUS, TEMP_FAHRENHEIT) if self._crit_temp is not None else None ) diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index b5435c92680..630250b4701 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -9,6 +9,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, ) +from homeassistant.const import TEMP_CELSIUS, TEMP_KELVIN from homeassistant.helpers import sun from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt @@ -191,7 +192,9 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): def _fmt_dewpoint(dewpoint): """Format the dewpoint data.""" if dewpoint is not None: - return round(TemperatureConverter.kelvin_to_celsius(dewpoint), 1) + return round( + TemperatureConverter.convert(dewpoint, TEMP_KELVIN, TEMP_CELSIUS), 1 + ) return None @staticmethod diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index e5dcebd3ca9..c1573755e11 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -348,7 +348,9 @@ class PrometheusMetrics: with suppress(ValueError): value = self.state_as_number(state) if state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_FAHRENHEIT: - value = TemperatureConverter.fahrenheit_to_celsius(value) + value = TemperatureConverter.convert( + value, TEMP_FAHRENHEIT, TEMP_CELSIUS + ) metric.labels(**self._labels(state)).set(value) def _handle_device_tracker(self, state): @@ -394,7 +396,7 @@ class PrometheusMetrics: def _handle_climate_temp(self, state, attr, metric_name, metric_description): if temp := state.attributes.get(attr): if self._climate_units == TEMP_FAHRENHEIT: - temp = TemperatureConverter.fahrenheit_to_celsius(temp) + temp = TemperatureConverter.convert(temp, TEMP_FAHRENHEIT, TEMP_CELSIUS) metric = self._metric( metric_name, self.prometheus_cli.Gauge, @@ -507,7 +509,9 @@ class PrometheusMetrics: try: value = self.state_as_number(state) if state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_FAHRENHEIT: - value = TemperatureConverter.fahrenheit_to_celsius(value) + value = TemperatureConverter.convert( + value, TEMP_FAHRENHEIT, TEMP_CELSIUS + ) _metric.labels(**self._labels(state)).set(value) except ValueError: pass diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 84a42487498..cb066901b37 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -297,19 +297,19 @@ class TemperatureConverter(BaseUnitConverter): if from_unit == TEMP_CELSIUS: if to_unit == TEMP_FAHRENHEIT: - return cls.celsius_to_fahrenheit(value, interval) + return cls._celsius_to_fahrenheit(value, interval) if to_unit == TEMP_KELVIN: - return cls.celsius_to_kelvin(value, interval) + return cls._celsius_to_kelvin(value, interval) raise HomeAssistantError( UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS) ) if from_unit == TEMP_FAHRENHEIT: if to_unit == TEMP_CELSIUS: - return cls.fahrenheit_to_celsius(value, interval) + return cls._fahrenheit_to_celsius(value, interval) if to_unit == TEMP_KELVIN: - return cls.celsius_to_kelvin( - cls.fahrenheit_to_celsius(value, interval), interval + return cls._celsius_to_kelvin( + cls._fahrenheit_to_celsius(value, interval), interval ) raise HomeAssistantError( UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS) @@ -317,10 +317,10 @@ class TemperatureConverter(BaseUnitConverter): if from_unit == TEMP_KELVIN: if to_unit == TEMP_CELSIUS: - return cls.kelvin_to_celsius(value, interval) + return cls._kelvin_to_celsius(value, interval) if to_unit == TEMP_FAHRENHEIT: - return cls.celsius_to_fahrenheit( - cls.kelvin_to_celsius(value, interval), interval + return cls._celsius_to_fahrenheit( + cls._kelvin_to_celsius(value, interval), interval ) raise HomeAssistantError( UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS) @@ -330,28 +330,28 @@ class TemperatureConverter(BaseUnitConverter): ) @classmethod - def fahrenheit_to_celsius(cls, fahrenheit: float, interval: bool = False) -> float: + def _fahrenheit_to_celsius(cls, fahrenheit: float, interval: bool = False) -> float: """Convert a temperature in Fahrenheit to Celsius.""" if interval: return fahrenheit / 1.8 return (fahrenheit - 32.0) / 1.8 @classmethod - def kelvin_to_celsius(cls, kelvin: float, interval: bool = False) -> float: + def _kelvin_to_celsius(cls, kelvin: float, interval: bool = False) -> float: """Convert a temperature in Kelvin to Celsius.""" if interval: return kelvin return kelvin - 273.15 @classmethod - def celsius_to_fahrenheit(cls, celsius: float, interval: bool = False) -> float: + def _celsius_to_fahrenheit(cls, celsius: float, interval: bool = False) -> float: """Convert a temperature in Celsius to Fahrenheit.""" if interval: return celsius * 1.8 return celsius * 1.8 + 32.0 @classmethod - def celsius_to_kelvin(cls, celsius: float, interval: bool = False) -> float: + def _celsius_to_kelvin(cls, celsius: float, interval: bool = False) -> float: """Convert a temperature in Celsius to Kelvin.""" if interval: return celsius From ed044acca73d25cd4b1f06fbdf318ec1ba3cd7ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huryn?= Date: Fri, 30 Sep 2022 11:37:47 +0200 Subject: [PATCH 053/985] Remove blebox AirQuality platform (#77873) * AirQuality functionality moved to sensors, tests moved accordingly. * Refreshed fixtures comments. --- homeassistant/components/blebox/__init__.py | 1 - .../components/blebox/air_quality.py | 43 --------- homeassistant/components/blebox/manifest.json | 2 +- homeassistant/components/blebox/sensor.py | 48 ++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/blebox/test_air_quality.py | 93 ------------------- tests/components/blebox/test_sensor.py | 67 ++++++++++++- 8 files changed, 109 insertions(+), 149 deletions(-) delete mode 100644 homeassistant/components/blebox/air_quality.py delete mode 100644 tests/components/blebox/test_air_quality.py diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index 0f4bd1c1490..35a334f36f3 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -18,7 +18,6 @@ from .const import DEFAULT_SETUP_TIMEOUT, DOMAIN, PRODUCT _LOGGER = logging.getLogger(__name__) PLATFORMS = [ - Platform.AIR_QUALITY, Platform.BUTTON, Platform.CLIMATE, Platform.COVER, diff --git a/homeassistant/components/blebox/air_quality.py b/homeassistant/components/blebox/air_quality.py deleted file mode 100644 index daadbc831b6..00000000000 --- a/homeassistant/components/blebox/air_quality.py +++ /dev/null @@ -1,43 +0,0 @@ -"""BleBox air quality entity.""" -from datetime import timedelta - -from homeassistant.components.air_quality import AirQualityEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import BleBoxEntity, create_blebox_entities - -SCAN_INTERVAL = timedelta(seconds=5) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up a BleBox air quality entity.""" - create_blebox_entities( - hass, config_entry, async_add_entities, BleBoxAirQualityEntity, "air_qualities" - ) - - -class BleBoxAirQualityEntity(BleBoxEntity, AirQualityEntity): - """Representation of a BleBox air quality feature.""" - - _attr_icon = "mdi:blur" - - @property - def particulate_matter_0_1(self): - """Return the particulate matter 0.1 level.""" - return self._feature.pm1 - - @property - def particulate_matter_2_5(self): - """Return the particulate matter 2.5 level.""" - return self._feature.pm2_5 - - @property - def particulate_matter_10(self): - """Return the particulate matter 10 level.""" - return self._feature.pm10 diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 49d44db8f01..328f15abdac 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -3,7 +3,7 @@ "name": "BleBox devices", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blebox", - "requirements": ["blebox_uniapi==2.0.2"], + "requirements": ["blebox_uniapi==2.1.0"], "codeowners": ["@bbx-a", "@riokuu"], "iot_class": "local_polling", "loggers": ["blebox_uniapi"] diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index 663af970e3e..f3c0c393fd9 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -1,15 +1,46 @@ """BleBox sensor entities.""" -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from dataclasses import dataclass + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BleBoxEntity, create_blebox_entities -BLEBOX_TO_UNIT_MAP = {"celsius": TEMP_CELSIUS} -BLEBOX_TO_SENSOR_DEVICE_CLASS = {"temperature": SensorDeviceClass.TEMPERATURE} +@dataclass +class BleboxSensorEntityDescription(SensorEntityDescription): + """Class describing Blebox sensor entities.""" + + +SENSOR_TYPES = ( + BleboxSensorEntityDescription( + key="pm1", + device_class=SensorDeviceClass.PM1, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), + BleboxSensorEntityDescription( + key="pm2_5", + device_class=SensorDeviceClass.PM25, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), + BleboxSensorEntityDescription( + key="pm10", + device_class=SensorDeviceClass.PM10, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), + BleboxSensorEntityDescription( + key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + ), +) async def async_setup_entry( @@ -30,10 +61,13 @@ class BleBoxSensorEntity(BleBoxEntity, SensorEntity): def __init__(self, feature): """Initialize a BleBox sensor feature.""" super().__init__(feature) - self._attr_native_unit_of_measurement = BLEBOX_TO_UNIT_MAP[feature.unit] - self._attr_device_class = BLEBOX_TO_SENSOR_DEVICE_CLASS[feature.device_class] + + for description in SENSOR_TYPES: + if description.key == feature.device_class: + self.entity_description = description + break @property def native_value(self): """Return the state.""" - return self._feature.current + return self._feature.native_value diff --git a/requirements_all.txt b/requirements_all.txt index 3b1914d986b..788463b3f54 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -419,7 +419,7 @@ bleak-retry-connector==2.1.3 bleak==0.18.1 # homeassistant.components.blebox -blebox_uniapi==2.0.2 +blebox_uniapi==2.1.0 # homeassistant.components.blink blinkpy==0.19.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e5e957c2f1..b0c3f7c4a4a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -343,7 +343,7 @@ bleak-retry-connector==2.1.3 bleak==0.18.1 # homeassistant.components.blebox -blebox_uniapi==2.0.2 +blebox_uniapi==2.1.0 # homeassistant.components.blink blinkpy==0.19.2 diff --git a/tests/components/blebox/test_air_quality.py b/tests/components/blebox/test_air_quality.py deleted file mode 100644 index 8b5bc67d4bc..00000000000 --- a/tests/components/blebox/test_air_quality.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Blebox air_quality tests.""" -import logging -from unittest.mock import AsyncMock, PropertyMock - -import blebox_uniapi -import pytest - -from homeassistant.components.air_quality import ATTR_PM_0_1, ATTR_PM_2_5, ATTR_PM_10 -from homeassistant.const import ATTR_ICON, STATE_UNKNOWN -from homeassistant.helpers import device_registry as dr - -from .conftest import async_setup_entity, mock_feature - - -@pytest.fixture(name="airsensor") -def airsensor_fixture(): - """Return a default air quality fixture.""" - feature = mock_feature( - "air_qualities", - blebox_uniapi.air_quality.AirQuality, - unique_id="BleBox-airSensor-1afe34db9437-0.air", - full_name="airSensor-0.air", - device_class=None, - pm1=None, - pm2_5=None, - pm10=None, - ) - product = feature.product - type(product).name = PropertyMock(return_value="My air sensor") - type(product).model = PropertyMock(return_value="airSensor") - return (feature, "air_quality.airsensor_0_air") - - -async def test_init(airsensor, hass, config): - """Test airSensor default state.""" - - _, entity_id = airsensor - entry = await async_setup_entity(hass, config, entity_id) - assert entry.unique_id == "BleBox-airSensor-1afe34db9437-0.air" - - state = hass.states.get(entity_id) - assert state.name == "airSensor-0.air" - - assert ATTR_PM_0_1 not in state.attributes - assert ATTR_PM_2_5 not in state.attributes - assert ATTR_PM_10 not in state.attributes - - assert state.attributes[ATTR_ICON] == "mdi:blur" - - assert state.state == STATE_UNKNOWN - - device_registry = dr.async_get(hass) - device = device_registry.async_get(entry.device_id) - - assert device.name == "My air sensor" - assert device.identifiers == {("blebox", "abcd0123ef5678")} - assert device.manufacturer == "BleBox" - assert device.model == "airSensor" - assert device.sw_version == "1.23" - - -async def test_update(airsensor, hass, config): - """Test air quality sensor state after update.""" - - feature_mock, entity_id = airsensor - - def initial_update(): - feature_mock.pm1 = 49 - feature_mock.pm2_5 = 222 - feature_mock.pm10 = 333 - - feature_mock.async_update = AsyncMock(side_effect=initial_update) - await async_setup_entity(hass, config, entity_id) - - state = hass.states.get(entity_id) - - assert state.attributes[ATTR_PM_0_1] == 49 - assert state.attributes[ATTR_PM_2_5] == 222 - assert state.attributes[ATTR_PM_10] == 333 - - assert state.state == "222" - - -async def test_update_failure(airsensor, hass, config, caplog): - """Test that update failures are logged.""" - - caplog.set_level(logging.ERROR) - - feature_mock, entity_id = airsensor - feature_mock.async_update = AsyncMock(side_effect=blebox_uniapi.error.ClientError) - await async_setup_entity(hass, config, entity_id) - - assert f"Updating '{feature_mock.full_name}' failed: " in caplog.text diff --git a/tests/components/blebox/test_sensor.py b/tests/components/blebox/test_sensor.py index b7f6d421a12..d876da8b0b6 100644 --- a/tests/components/blebox/test_sensor.py +++ b/tests/components/blebox/test_sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, STATE_UNKNOWN, TEMP_CELSIUS, ) @@ -17,9 +18,27 @@ from homeassistant.helpers import device_registry as dr from .conftest import async_setup_entity, mock_feature +@pytest.fixture(name="airsensor") +def airsensor_fixture(): + """Return a default AirQuality sensor mock.""" + feature = mock_feature( + "sensors", + blebox_uniapi.sensor.AirQuality, + unique_id="BleBox-airSensor-1afe34db9437-0.air", + full_name="airSensor-0.air", + device_class="pm1", + unit="concentration_of_mp", + native_value=None, + ) + product = feature.product + type(product).name = PropertyMock(return_value="My air sensor") + type(product).model = PropertyMock(return_value="airSensor") + return (feature, "sensor.airsensor_0_air") + + @pytest.fixture(name="tempsensor") def tempsensor_fixture(): - """Return a default sensor mock.""" + """Return a default Temperature sensor mock.""" feature = mock_feature( "sensors", blebox_uniapi.sensor.Temperature, @@ -28,6 +47,7 @@ def tempsensor_fixture(): device_class="temperature", unit="celsius", current=None, + native_value=None, ) product = feature.product type(product).name = PropertyMock(return_value="My temperature sensor") @@ -65,7 +85,7 @@ async def test_update(tempsensor, hass, config): feature_mock, entity_id = tempsensor def initial_update(): - feature_mock.current = 25.18 + feature_mock.native_value = 25.18 feature_mock.async_update = AsyncMock(side_effect=initial_update) await async_setup_entity(hass, config, entity_id) @@ -85,3 +105,46 @@ async def test_update_failure(tempsensor, hass, config, caplog): await async_setup_entity(hass, config, entity_id) assert f"Updating '{feature_mock.full_name}' failed: " in caplog.text + + +async def test_airsensor_init(airsensor, hass, config): + """Test airSensor default state.""" + + _, entity_id = airsensor + entry = await async_setup_entity(hass, config, entity_id) + assert entry.unique_id == "BleBox-airSensor-1afe34db9437-0.air" + + state = hass.states.get(entity_id) + assert state.name == "airSensor-0.air" + + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.PM1 + assert state.state == STATE_UNKNOWN + + device_registry = dr.async_get(hass) + device = device_registry.async_get(entry.device_id) + + assert device.name == "My air sensor" + assert device.identifiers == {("blebox", "abcd0123ef5678")} + assert device.manufacturer == "BleBox" + assert device.model == "airSensor" + assert device.sw_version == "1.23" + + +async def test_airsensor_update(airsensor, hass, config): + """Test air quality sensor state after update.""" + + feature_mock, entity_id = airsensor + + def initial_update(): + feature_mock.native_value = 49 + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + + state = hass.states.get(entity_id) + assert ( + state.attributes[ATTR_UNIT_OF_MEASUREMENT] + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + + assert state.state == "49" From ca0cd19dc917db915d3a3cd4d4dc18622a6745e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Sep 2022 02:29:36 -1000 Subject: [PATCH 054/985] Switch to using new esphome bluetooth_proxy_version field (#79331) --- homeassistant/components/esphome/__init__.py | 2 +- .../components/esphome/bluetooth/__init__.py | 14 ++++++++------ homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 90f1bac8de2..8846007374e 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -236,7 +236,7 @@ async def async_setup_entry( # noqa: C901 await cli.subscribe_states(entry_data.async_update_state) await cli.subscribe_service_calls(async_on_service_call) await cli.subscribe_home_assistant_states(async_on_state_subscription) - if entry_data.device_info.has_bluetooth_proxy: + if entry_data.device_info.bluetooth_proxy_version: entry_data.disconnect_callbacks.append( await async_connect_scanner(hass, entry, cli, entry_data) ) diff --git a/homeassistant/components/esphome/bluetooth/__init__.py b/homeassistant/components/esphome/bluetooth/__init__.py index 4061333b4f3..4f3235676a4 100644 --- a/homeassistant/components/esphome/bluetooth/__init__.py +++ b/homeassistant/components/esphome/bluetooth/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations import logging from aioesphomeapi import APIClient -from awesomeversion import AwesomeVersion from homeassistant.components.bluetooth import ( HaBluetoothConnector, @@ -24,7 +23,6 @@ from ..entry_data import RuntimeEntryData from .client import ESPHomeClient from .scanner import ESPHomeScanner -CONNECTABLE_MIN_VERSION = AwesomeVersion("2022.10.0-dev") _LOGGER = logging.getLogger(__name__) @@ -53,10 +51,14 @@ async def async_connect_scanner( assert entry.unique_id is not None source = str(entry.unique_id) new_info_callback = async_get_advertisement_callback(hass) - connectable = bool( - entry_data.device_info - and AwesomeVersion(entry_data.device_info.esphome_version) - >= CONNECTABLE_MIN_VERSION + assert entry_data.device_info is not None + version = entry_data.device_info.bluetooth_proxy_version + connectable = version >= 2 + _LOGGER.debug( + "Connecting scanner for %s, version=%s, connectable=%s", + source, + version, + connectable, ) connector = HaBluetoothConnector( client=ESPHomeClient, diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index d094b8518ef..c6a475b6eea 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==10.14.0"], + "requirements": ["aioesphomeapi==11.0.0"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index 788463b3f54..8e0d5571a26 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -156,7 +156,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.14.0 +aioesphomeapi==11.0.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0c3f7c4a4a..a7bb29b9dea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -143,7 +143,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.14.0 +aioesphomeapi==11.0.0 # homeassistant.components.flo aioflo==2021.11.0 From 6694d06b37e5710dccc674932c9ddb54a05eae0a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Sep 2022 02:46:45 -1000 Subject: [PATCH 055/985] Remove iBeacon devices that rotate their major,minor and mac (#79338) --- homeassistant/components/ibeacon/const.py | 5 ++ .../components/ibeacon/coordinator.py | 42 +++++++++++- .../components/ibeacon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ibeacon/test_coordinator.py | 68 +++++++++++++++++++ 6 files changed, 117 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ibeacon/const.py b/homeassistant/components/ibeacon/const.py index 9b7a5a81dd3..7d1ab15da0a 100644 --- a/homeassistant/components/ibeacon/const.py +++ b/homeassistant/components/ibeacon/const.py @@ -27,4 +27,9 @@ UPDATE_INTERVAL = timedelta(seconds=60) # we will add it to the ignore list since its garbage data. MAX_IDS = 10 +# If a device broadcasts this many major minors for the same uuid +# we will add it to the ignore list since its garbage data. +MAX_IDS_PER_UUID = 50 + CONF_IGNORE_ADDRESSES = "ignore_addresses" +CONF_IGNORE_UUIDS = "ignore_uuids" diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py index 546b40c0c1b..2260624558e 100644 --- a/homeassistant/components/ibeacon/coordinator.py +++ b/homeassistant/components/ibeacon/coordinator.py @@ -23,8 +23,10 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( CONF_IGNORE_ADDRESSES, + CONF_IGNORE_UUIDS, DOMAIN, MAX_IDS, + MAX_IDS_PER_UUID, SIGNAL_IBEACON_DEVICE_NEW, SIGNAL_IBEACON_DEVICE_SEEN, SIGNAL_IBEACON_DEVICE_UNAVAILABLE, @@ -115,6 +117,9 @@ class IBeaconCoordinator: self._ignore_addresses: set[str] = set( entry.data.get(CONF_IGNORE_ADDRESSES, []) ) + # iBeacon devices that do not follow the spec + # and broadcast custom data in the major and minor fields + self._ignore_uuids: set[str] = set(entry.data.get(CONF_IGNORE_UUIDS, [])) # iBeacons with fixed MAC addresses self._last_ibeacon_advertisement_by_unique_id: dict[ @@ -131,6 +136,9 @@ class IBeaconCoordinator: self._last_seen_by_group_id: dict[str, bluetooth.BluetoothServiceInfoBleak] = {} self._unavailable_group_ids: set[str] = set() + # iBeacons with random MAC addresses, fixed UUID, random major/minor + self._major_minor_by_uuid: dict[str, set[tuple[int, int]]] = {} + @callback def _async_handle_unavailable( self, service_info: bluetooth.BluetoothServiceInfoBleak @@ -146,6 +154,25 @@ class IBeaconCoordinator: """Cancel unavailable tracking for an address.""" self._unavailable_trackers.pop(address)() + @callback + def _async_ignore_uuid(self, uuid: str) -> None: + """Ignore an UUID that does not follow the spec and any entities created by it.""" + self._ignore_uuids.add(uuid) + major_minor_by_uuid = self._major_minor_by_uuid.pop(uuid) + unique_ids_to_purge = set() + for major, minor in major_minor_by_uuid: + group_id = f"{uuid}_{major}_{minor}" + if unique_ids := self._unique_ids_by_group_id.pop(group_id, None): + unique_ids_to_purge.update(unique_ids) + for address in self._addresses_by_group_id.pop(group_id, []): + self._async_cancel_unavailable_tracker(address) + self._unique_ids_by_address.pop(address) + self._group_ids_by_address.pop(address) + self._async_purge_untrackable_entities(unique_ids_to_purge) + entry_data = self._entry.data + new_data = entry_data | {CONF_IGNORE_UUIDS: list(self._ignore_uuids)} + self.hass.config_entries.async_update_entry(self._entry, data=new_data) + @callback def _async_ignore_address(self, address: str) -> None: """Ignore an address that does not follow the spec and any entities created by it.""" @@ -203,7 +230,20 @@ class IBeaconCoordinator: return if not (ibeacon_advertisement := parse(service_info)): return - group_id = f"{ibeacon_advertisement.uuid}_{ibeacon_advertisement.major}_{ibeacon_advertisement.minor}" + + uuid_str = str(ibeacon_advertisement.uuid) + if uuid_str in self._ignore_uuids: + return + + major = ibeacon_advertisement.major + minor = ibeacon_advertisement.minor + major_minor_by_uuid = self._major_minor_by_uuid.setdefault(uuid_str, set()) + if len(major_minor_by_uuid) + 1 > MAX_IDS_PER_UUID: + self._async_ignore_uuid(uuid_str) + return + + major_minor_by_uuid.add((major, minor)) + group_id = f"{uuid_str}_{major}_{minor}" if group_id in self._group_ids_random_macs: self._async_update_ibeacon_with_random_mac( diff --git a/homeassistant/components/ibeacon/manifest.json b/homeassistant/components/ibeacon/manifest.json index 9cecb399281..7b4110a7fe4 100644 --- a/homeassistant/components/ibeacon/manifest.json +++ b/homeassistant/components/ibeacon/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ibeacon", "dependencies": ["bluetooth"], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [2, 21] }], - "requirements": ["ibeacon_ble==0.7.1"], + "requirements": ["ibeacon_ble==0.7.2"], "codeowners": ["@bdraco"], "iot_class": "local_push", "loggers": ["bleak"], diff --git a/requirements_all.txt b/requirements_all.txt index 8e0d5571a26..8be2c2295b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -901,7 +901,7 @@ iammeter==0.1.7 iaqualink==0.4.1 # homeassistant.components.ibeacon -ibeacon_ble==0.7.1 +ibeacon_ble==0.7.2 # homeassistant.components.watson_tts ibm-watson==5.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7bb29b9dea..693cdf53d49 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -672,7 +672,7 @@ hyperion-py==0.7.5 iaqualink==0.4.1 # homeassistant.components.ibeacon -ibeacon_ble==0.7.1 +ibeacon_ble==0.7.2 # homeassistant.components.ping icmplib==3.0 diff --git a/tests/components/ibeacon/test_coordinator.py b/tests/components/ibeacon/test_coordinator.py index cb7e0bdefc8..5ea19914ee4 100644 --- a/tests/components/ibeacon/test_coordinator.py +++ b/tests/components/ibeacon/test_coordinator.py @@ -127,3 +127,71 @@ async def test_ignore_default_name(hass): ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids()) == before_entity_count + + +async def test_rotating_major_minor_and_mac(hass): + """Test the different uuid, major, minor from many addresses removes all associated entities.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + + before_entity_count = len(hass.states.async_entity_ids("device_tracker")) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + for i in range(100): + service_info = BluetoothServiceInfo( + name="BlueCharm_177999", + address=f"AA:BB:CC:DD:EE:{i:02X}", + rssi=-63, + service_data={}, + manufacturer_data={ + 76: b"\x02\x15BlueCharmBeacons" + + bytearray([i]) + + b"\xfe" + + bytearray([i]) + + b"U\xc5" + }, + service_uuids=[], + source="local", + ) + inject_bluetooth_service_info(hass, service_info) + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids("device_tracker")) == before_entity_count + + +async def test_rotating_major_minor_and_mac_no_name(hass): + """Test no-name devices with different uuid, major, minor from many addresses removes all associated entities.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + + before_entity_count = len(hass.states.async_entity_ids("device_tracker")) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + for i in range(51): + service_info = BluetoothServiceInfo( + name=f"AA:BB:CC:DD:EE:{i:02X}", + address=f"AA:BB:CC:DD:EE:{i:02X}", + rssi=-63, + service_data={}, + manufacturer_data={ + 76: b"\x02\x15BlueCharmBeacons" + + bytearray([i]) + + b"\xfe" + + bytearray([i]) + + b"U\xc5" + }, + service_uuids=[], + source="local", + ) + inject_bluetooth_service_info(hass, service_info) + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids("device_tracker")) == before_entity_count From 5cdf4220eeb67b5a5837e70f2cba153e68f86888 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 30 Sep 2022 18:41:38 +0200 Subject: [PATCH 056/985] Fjaraskupan stop on 0 percentage (#79367) * Make sure fan turns off on 0 percentage * Remember old percentage --- homeassistant/components/fjaraskupan/fan.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index c037966f0ef..c856a94fa07 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -82,11 +82,18 @@ class Fan(CoordinatorEntity[Coordinator], FanEntity): async def async_set_percentage(self, percentage: int) -> None: """Set speed.""" - new_speed = percentage_to_ordered_list_item( - ORDERED_NAMED_FAN_SPEEDS, percentage - ) + + # Proactively update percentage to mange successive increases + self._percentage = percentage + async with self.coordinator.async_connect_and_update() as device: - await device.send_fan_speed(int(new_speed)) + if percentage == 0: + await device.send_command(COMMAND_STOP_FAN) + else: + new_speed = percentage_to_ordered_list_item( + ORDERED_NAMED_FAN_SPEEDS, percentage + ) + await device.send_fan_speed(int(new_speed)) async def async_turn_on( self, From c70ca1572b6bb7080eac3f0d25ce67bdcc001ae8 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 30 Sep 2022 21:37:58 +0300 Subject: [PATCH 057/985] Make Shelly update sensors disabled by default (#79376) --- homeassistant/components/shelly/update.py | 4 ++-- tests/components/shelly/test_update.py | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index fa37b394b6c..ac4b737a2cc 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -70,7 +70,7 @@ REST_UPDATES: Final = { install=lambda wrapper: wrapper.async_trigger_ota_update(), device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, - entity_registry_enabled_default=True, + entity_registry_enabled_default=False, ), "fwupdate_beta": RestUpdateDescription( name="Beta Firmware Update", @@ -94,7 +94,7 @@ RPC_UPDATES: Final = { install=lambda wrapper: wrapper.async_trigger_ota_update(), device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, - entity_registry_enabled_default=True, + entity_registry_enabled_default=False, ), "fwupdate_beta": RpcUpdateDescription( name="Beta Firmware Update", diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index 70c9b7a8e67..4d863c59390 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -1,7 +1,6 @@ """Tests for Shelly update platform.""" from homeassistant.components.shelly.const import DOMAIN -from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN -from homeassistant.components.update.const import SERVICE_INSTALL +from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import async_update_entity @@ -16,8 +15,8 @@ async def test_block_update(hass: HomeAssistant, coap_wrapper, monkeypatch): entity_registry.async_get_or_create( UPDATE_DOMAIN, DOMAIN, - "test_name_update", - suggested_object_id="test_name_update", + "test-mac-fwupdate", + suggested_object_id="test_name_firmware_update", disabled_by=None, ) hass.async_create_task( @@ -62,8 +61,8 @@ async def test_rpc_update(hass: HomeAssistant, rpc_wrapper, monkeypatch): entity_registry.async_get_or_create( UPDATE_DOMAIN, DOMAIN, - "test_name_update", - suggested_object_id="test_name_update", + "12345678-sys-fwupdate", + suggested_object_id="test_name_firmware_update", disabled_by=None, ) From b649ef8d8719d2dda41fc0a11ad684561bbb1ea8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 30 Sep 2022 20:38:11 +0200 Subject: [PATCH 058/985] Realign util constants with 2022.9.7 (#79357) --- homeassistant/util/distance.py | 24 ++++++++++++++++++++++++ homeassistant/util/speed.py | 2 +- homeassistant/util/volume.py | 2 -- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/homeassistant/util/distance.py b/homeassistant/util/distance.py index 09cd55a9cee..f5dbeaf42d5 100644 --- a/homeassistant/util/distance.py +++ b/homeassistant/util/distance.py @@ -1,6 +1,8 @@ """Distance util functions.""" from __future__ import annotations +from collections.abc import Callable + from homeassistant.const import ( # pylint: disable=unused-import # noqa: F401 LENGTH, LENGTH_CENTIMETERS, @@ -19,6 +21,28 @@ from .unit_conversion import DistanceConverter VALID_UNITS = DistanceConverter.VALID_UNITS +TO_METERS: dict[str, Callable[[float], float]] = { + LENGTH_METERS: lambda meters: meters, + LENGTH_MILES: lambda miles: miles * 1609.344, + LENGTH_YARD: lambda yards: yards * 0.9144, + LENGTH_FEET: lambda feet: feet * 0.3048, + LENGTH_INCHES: lambda inches: inches * 0.0254, + LENGTH_KILOMETERS: lambda kilometers: kilometers * 1000, + LENGTH_CENTIMETERS: lambda centimeters: centimeters * 0.01, + LENGTH_MILLIMETERS: lambda millimeters: millimeters * 0.001, +} + +METERS_TO: dict[str, Callable[[float], float]] = { + LENGTH_METERS: lambda meters: meters, + LENGTH_MILES: lambda meters: meters * 0.000621371, + LENGTH_YARD: lambda meters: meters * 1.09361, + LENGTH_FEET: lambda meters: meters * 3.28084, + LENGTH_INCHES: lambda meters: meters * 39.3701, + LENGTH_KILOMETERS: lambda meters: meters * 0.001, + LENGTH_CENTIMETERS: lambda meters: meters * 100, + LENGTH_MILLIMETERS: lambda meters: meters * 1000, +} + def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" diff --git a/homeassistant/util/speed.py b/homeassistant/util/speed.py index 18410272a7f..76ea873d7fe 100644 --- a/homeassistant/util/speed.py +++ b/homeassistant/util/speed.py @@ -26,7 +26,7 @@ from .unit_conversion import ( # pylint: disable=unused-import # noqa: F401 ) # pylint: disable-next=protected-access -UNIT_CONVERSION = SpeedConverter._UNIT_CONVERSION +UNIT_CONVERSION: dict[str, float] = SpeedConverter._UNIT_CONVERSION VALID_UNITS = SpeedConverter.VALID_UNITS diff --git a/homeassistant/util/volume.py b/homeassistant/util/volume.py index f63f7de2cf3..b468b9e6e0d 100644 --- a/homeassistant/util/volume.py +++ b/homeassistant/util/volume.py @@ -15,8 +15,6 @@ from homeassistant.helpers.frame import report from .unit_conversion import VolumeConverter -# pylint: disable-next=protected-access -UNIT_CONVERSION = VolumeConverter._UNIT_CONVERSION VALID_UNITS = VolumeConverter.VALID_UNITS From 2ce837f588e9211083948ca243d93b9289c3e8ce Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 30 Sep 2022 21:49:59 +0200 Subject: [PATCH 059/985] Resolve late comments to deCONZ sensor (#79380) Resolve late comments to deCONZ sensor #79137 --- homeassistant/components/deconz/sensor.py | 3 --- tests/components/deconz/test_sensor.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 90e19aee11d..66c186e20d7 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -33,7 +33,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_VOLTAGE, - CONCENTRATION_PARTS_PER_BILLION, ENERGY_KILO_WATT_HOUR, LIGHT_LUX, PERCENTAGE, @@ -120,7 +119,6 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( old_unique_id_suffix="ppb", device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, ), DeconzSensorDescription[Consumption]( key="consumption", @@ -196,7 +194,6 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( value_fn=lambda device: dt_util.parse_datetime(device.last_set), instance_check=Time, device_class=SensorDeviceClass.TIMESTAMP, - state_class=SensorStateClass.TOTAL_INCREASING, ), DeconzSensorDescription[SensorResources]( key="battery", diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 1078d888c0e..ac8100caa3d 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -104,7 +104,6 @@ TEST_DATA = [ "state_class": SensorStateClass.MEASUREMENT, "attributes": { "state_class": "measurement", - "unit_of_measurement": "ppb", "device_class": "aqi", "friendly_name": "BOSCH Air quality sensor PPB", }, @@ -521,9 +520,7 @@ TEST_DATA = [ "state": "2020-11-19T08:07:08+00:00", "entity_category": None, "device_class": SensorDeviceClass.TIMESTAMP, - "state_class": SensorStateClass.TOTAL_INCREASING, "attributes": { - "state_class": "total_increasing", "device_class": "timestamp", "friendly_name": "eTRV Séjour", }, From 2a45738d97da799dde8021cf6bb85a317fb771fa Mon Sep 17 00:00:00 2001 From: Jan-Philipp Litza Date: Fri, 30 Sep 2022 22:37:50 +0200 Subject: [PATCH 060/985] Add state_class MEASUREMENT to lacrosse temperature and humidity sensors (#79379) --- homeassistant/components/lacrosse/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/lacrosse/sensor.py b/homeassistant/components/lacrosse/sensor.py index d334fb61d1f..e2f028907d9 100644 --- a/homeassistant/components/lacrosse/sensor.py +++ b/homeassistant/components/lacrosse/sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.const import ( CONF_DEVICE, @@ -185,6 +186,7 @@ class LaCrosseTemperature(LaCrosseSensor): """Implementation of a Lacrosse temperature sensor.""" _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_state_class = SensorStateClass.MEASUREMENT _attr_native_unit_of_measurement = TEMP_CELSIUS @property @@ -197,6 +199,7 @@ class LaCrosseHumidity(LaCrosseSensor): """Implementation of a Lacrosse humidity sensor.""" _attr_native_unit_of_measurement = PERCENTAGE + _attr_state_class = SensorStateClass.MEASUREMENT _attr_icon = "mdi:water-percent" @property From fbcf6cb03c6de575e50ac23f59b6e6646a118cbc Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 1 Oct 2022 00:01:44 +0200 Subject: [PATCH 061/985] Bump fjaraskupan to 2.1.0 (#79383) * Make sure fan turns off on 0 percentage * Remember old percentage * Bump fjaraskupan to 1.2.0 --- homeassistant/components/fjaraskupan/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fjaraskupan/manifest.json b/homeassistant/components/fjaraskupan/manifest.json index 7381fc36a08..6025665ec31 100644 --- a/homeassistant/components/fjaraskupan/manifest.json +++ b/homeassistant/components/fjaraskupan/manifest.json @@ -3,7 +3,7 @@ "name": "Fj\u00e4r\u00e5skupan", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fjaraskupan", - "requirements": ["fjaraskupan==2.0.0"], + "requirements": ["fjaraskupan==2.1.0"], "codeowners": ["@elupus"], "iot_class": "local_polling", "loggers": ["bleak", "fjaraskupan"], diff --git a/requirements_all.txt b/requirements_all.txt index 8be2c2295b9..61c5e9ecc13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -690,7 +690,7 @@ fivem-api==0.1.2 fixerio==1.0.0a0 # homeassistant.components.fjaraskupan -fjaraskupan==2.0.0 +fjaraskupan==2.1.0 # homeassistant.components.flipr flipr-api==1.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 693cdf53d49..8b7bd181170 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -512,7 +512,7 @@ file-read-backwards==2.0.0 fivem-api==0.1.2 # homeassistant.components.fjaraskupan -fjaraskupan==2.0.0 +fjaraskupan==2.1.0 # homeassistant.components.flipr flipr-api==1.4.2 From 3a9ecab98abcc44c0ba123e2411b76c2a65343e7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 1 Oct 2022 00:12:39 +0200 Subject: [PATCH 062/985] Improve iterable typing (1) (#79295) --- homeassistant/components/recorder/filters.py | 14 +++++++------- homeassistant/components/recorder/util.py | 4 ++-- homeassistant/core.py | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 45db64e0097..72e9916b6a7 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -1,7 +1,7 @@ """Provide pre-made queries on top of the recorder component.""" from __future__ import annotations -from collections.abc import Callable, Iterable +from collections.abc import Callable, Collection, Iterable import json from typing import Any @@ -81,13 +81,13 @@ class Filters: def __init__(self) -> None: """Initialise the include and exclude filters.""" - self.excluded_entities: Iterable[str] = [] - self.excluded_domains: Iterable[str] = [] - self.excluded_entity_globs: Iterable[str] = [] + self.excluded_entities: Collection[str] = [] + self.excluded_domains: Collection[str] = [] + self.excluded_entity_globs: Collection[str] = [] - self.included_entities: Iterable[str] = [] - self.included_domains: Iterable[str] = [] - self.included_entity_globs: Iterable[str] = [] + self.included_entities: Collection[str] = [] + self.included_domains: Collection[str] = [] + self.included_entity_globs: Collection[str] = [] def __repr__(self) -> str: """Return human readable excludes/includes.""" diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 139e73199ed..bd95e1f50c3 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -1,7 +1,7 @@ """SQLAlchemy util functions.""" from __future__ import annotations -from collections.abc import Callable, Generator, Iterable +from collections.abc import Callable, Generator from contextlib import contextmanager from datetime import date, datetime, timedelta import functools @@ -180,7 +180,7 @@ def execute_stmt_lambda_element( start_time: datetime | None = None, end_time: datetime | None = None, yield_per: int | None = DEFAULT_YIELD_STATES_ROWS, -) -> Iterable[Row]: +) -> list[Row]: """Execute a StatementLambdaElement. If the time window passed is greater than one day diff --git a/homeassistant/core.py b/homeassistant/core.py index 01c75fb707e..f4cefcb7fff 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -649,7 +649,7 @@ class HomeAssistant: else: await asyncio.sleep(0) - async def _await_and_log_pending(self, pending: Iterable[Awaitable[Any]]) -> None: + async def _await_and_log_pending(self, pending: Collection[Awaitable[Any]]) -> None: """Await and log tasks that take a long time.""" wait_time = 0 while pending: From 249922ba1bff42383d0f3e455d1670f08c154314 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 1 Oct 2022 00:13:15 +0200 Subject: [PATCH 063/985] Improve iterable typing (2) (#79296) * Improve iterable typing (2) * Use collection --- homeassistant/components/homekit/__init__.py | 2 +- homeassistant/components/lifx/discovery.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index ee53c6da665..c203d674710 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -489,7 +489,7 @@ class HomeKit: advertise_ip: str | None, entry_id: str, entry_title: str, - devices: Iterable[str] | None = None, + devices: list[str] | None = None, ) -> None: """Initialize a HomeKit object.""" self.hass = hass diff --git a/homeassistant/components/lifx/discovery.py b/homeassistant/components/lifx/discovery.py index 6e1507c92ca..a4072ee23ef 100644 --- a/homeassistant/components/lifx/discovery.py +++ b/homeassistant/components/lifx/discovery.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Iterable +from collections.abc import Collection, Iterable from aiolifx.aiolifx import LifxDiscovery, Light, ScanManager @@ -17,7 +17,7 @@ from .const import CONF_SERIAL, DOMAIN DEFAULT_TIMEOUT = 8.5 -async def async_discover_devices(hass: HomeAssistant) -> Iterable[Light]: +async def async_discover_devices(hass: HomeAssistant) -> Collection[Light]: """Discover lifx devices.""" all_lights: dict[str, Light] = {} broadcast_addrs = await network.async_get_ipv4_broadcast_addresses(hass) From bd5ec4e1981a273b54cee9989894a6bc9b7db87a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 1 Oct 2022 00:40:08 +0000 Subject: [PATCH 064/985] [ci skip] Translation update --- .../airthings_ble/translations/bg.json | 22 ++++++++++++++++ .../airthings_ble/translations/ca.json | 23 ++++++++++++++++ .../airthings_ble/translations/el.json | 23 ++++++++++++++++ .../airthings_ble/translations/es.json | 23 ++++++++++++++++ .../airthings_ble/translations/et.json | 23 ++++++++++++++++ .../airthings_ble/translations/hu.json | 23 ++++++++++++++++ .../airthings_ble/translations/id.json | 23 ++++++++++++++++ .../airthings_ble/translations/nl.json | 23 ++++++++++++++++ .../airthings_ble/translations/tr.json | 23 ++++++++++++++++ .../airthings_ble/translations/zh-Hant.json | 23 ++++++++++++++++ .../components/apcupsd/translations/el.json | 26 +++++++++++++++++++ .../components/bayesian/translations/el.json | 12 +++++++++ .../components/bayesian/translations/et.json | 12 +++++++++ .../bayesian/translations/zh-Hant.json | 12 +++++++++ .../bluemaestro/translations/hu.json | 2 +- .../components/bluetooth/translations/hu.json | 2 +- .../components/braviatv/translations/el.json | 10 +++++-- .../components/bthome/translations/hu.json | 2 +- .../dsmr_reader/translations/el.json | 18 +++++++++++++ .../components/ezviz/translations/ca.json | 6 ++--- .../forked_daapd/translations/ca.json | 14 +++++----- .../google_sheets/translations/el.json | 4 +++ .../components/govee_ble/translations/hu.json | 2 +- .../components/inkbird/translations/hu.json | 2 +- .../components/kegtron/translations/hu.json | 2 +- .../litterrobot/translations/el.json | 6 +++++ .../components/moat/translations/hu.json | 2 +- .../components/moon/translations/el.json | 6 +++++ .../components/qingping/translations/hu.json | 2 +- .../components/season/translations/el.json | 6 +++++ .../components/sensor/translations/el.json | 12 +++++++-- .../components/sensorpro/translations/hu.json | 2 +- .../sensorpush/translations/hu.json | 2 +- .../components/shelly/translations/el.json | 8 ++++++ .../components/tautulli/translations/el.json | 1 + .../thermobeacon/translations/hu.json | 2 +- .../components/thermopro/translations/hu.json | 2 +- .../components/tilt_ble/translations/hu.json | 2 +- .../components/uptime/translations/el.json | 6 +++++ .../xiaomi_ble/translations/hu.json | 2 +- .../components/zha/translations/el.json | 2 ++ 41 files changed, 390 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/airthings_ble/translations/bg.json create mode 100644 homeassistant/components/airthings_ble/translations/ca.json create mode 100644 homeassistant/components/airthings_ble/translations/el.json create mode 100644 homeassistant/components/airthings_ble/translations/es.json create mode 100644 homeassistant/components/airthings_ble/translations/et.json create mode 100644 homeassistant/components/airthings_ble/translations/hu.json create mode 100644 homeassistant/components/airthings_ble/translations/id.json create mode 100644 homeassistant/components/airthings_ble/translations/nl.json create mode 100644 homeassistant/components/airthings_ble/translations/tr.json create mode 100644 homeassistant/components/airthings_ble/translations/zh-Hant.json create mode 100644 homeassistant/components/apcupsd/translations/el.json create mode 100644 homeassistant/components/bayesian/translations/el.json create mode 100644 homeassistant/components/bayesian/translations/et.json create mode 100644 homeassistant/components/bayesian/translations/zh-Hant.json create mode 100644 homeassistant/components/dsmr_reader/translations/el.json diff --git a/homeassistant/components/airthings_ble/translations/bg.json b/homeassistant/components/airthings_ble/translations/bg.json new file mode 100644 index 00000000000..3c3714804c4 --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0437\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/ca.json b/homeassistant/components/airthings_ble/translations/ca.json new file mode 100644 index 00000000000..1b9d6bd2170 --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "cannot_connect": "Ha fallat la connexi\u00f3", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "unknown": "Error inesperat" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/el.json b/homeassistant/components/airthings_ble/translations/el.json new file mode 100644 index 00000000000..cdb92f71285 --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/el.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/es.json b/homeassistant/components/airthings_ble/translations/es.json new file mode 100644 index 00000000000..e39343de799 --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "cannot_connect": "No se pudo conectar", + "no_devices_found": "No se encontraron dispositivos en la red", + "unknown": "Error inesperado" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/et.json b/homeassistant/components/airthings_ble/translations/et.json new file mode 100644 index 00000000000..2e8cb22443b --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "cannot_connect": "\u00dchendamine nurjus", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/hu.json b/homeassistant/components/airthings_ble/translations/hu.json new file mode 100644 index 00000000000..416831e5b4f --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/id.json b/homeassistant/components/airthings_ble/translations/id.json new file mode 100644 index 00000000000..48b138b2e7b --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal terhubung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/nl.json b/homeassistant/components/airthings_ble/translations/nl.json new file mode 100644 index 00000000000..19c9a433f99 --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Nederlands", + "already_in_progress": "Nederlands", + "cannot_connect": "Nederlands", + "no_devices_found": "Nederlands", + "unknown": "Nederlands" + }, + "flow_title": "Nederlands", + "step": { + "bluetooth_confirm": { + "description": "Nederlands" + }, + "user": { + "data": { + "address": "Nederlands" + }, + "description": "Nederlands" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/tr.json b/homeassistant/components/airthings_ble/translations/tr.json new file mode 100644 index 00000000000..9854002de33 --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/zh-Hant.json b/homeassistant/components/airthings_ble/translations/zh-Hant.json new file mode 100644 index 00000000000..749355e8bdf --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/el.json b/homeassistant/components/apcupsd/translations/el.json new file mode 100644 index 00000000000..a7925a9e814 --- /dev/null +++ b/homeassistant/components/apcupsd/translations/el.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "no_status": "\u0394\u03b5\u03bd \u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03b5\u03c4\u03b1\u03b9 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b1\u03c0\u03cc \u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03c3\u03c4\u03b7\u03bd \u03bf\u03c0\u03bf\u03af\u03b1 \u03b5\u03be\u03c5\u03c0\u03b7\u03c1\u03b5\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03c4\u03bf apcupsd NIS." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 APC UPS Daemon \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 APC UPS Daemon YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 APC UPS Daemon YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bayesian/translations/el.json b/homeassistant/components/bayesian/translations/el.json new file mode 100644 index 00000000000..56e143bd03b --- /dev/null +++ b/homeassistant/components/bayesian/translations/el.json @@ -0,0 +1,12 @@ +{ + "issues": { + "manual_migration": { + "description": "\u0397 Bayesian \u03b5\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c4\u03ce\u03c1\u03b1 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03bd\u03b5\u03b9 \u03b5\u03c0\u03af\u03c3\u03b7\u03c2 \u03c4\u03b7\u03bd \u03c0\u03b9\u03b8\u03b1\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b5\u03ac\u03bd \u03c4\u03bf \u03c0\u03b1\u03c1\u03b1\u03c4\u03b7\u03c1\u03bf\u03cd\u03bc\u03b5\u03bd\u03bf \"to_state\", \"bove\", \"power\" \u03ae \"value_template\" \u03b1\u03be\u03b9\u03bf\u03bb\u03bf\u03b3\u03b7\u03b8\u03b5\u03af \u03c3\u03b5 \"False\" \u03ba\u03b1\u03b9 \u03cc\u03c7\u03b9 \u03bc\u03cc\u03bd\u03bf \u03c3\u03b5 \"True\". \u0395\u03c0\u03bf\u03bc\u03ad\u03bd\u03c9\u03c2, \u03b4\u03b5\u03bd \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03bd\u03b1 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03b4\u03b9\u03c0\u03bb\u03ad\u03c2, \u03c3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03c9\u03bc\u03b1\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03b4\u03c5\u03b1\u03b4\u03b9\u03ba\u03ae \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03bf\u03c0\u03c4\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b9\u03c3\u03b7 \u03b3\u03b9\u03b1 \" {entity} \".", + "title": "\u0391\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b5\u03c0\u03b9\u03b4\u03b9\u03cc\u03c1\u03b8\u03c9\u03c3\u03b7 YAML \u03b3\u03b9\u03b1 Bayesian" + }, + "no_prob_given_false": { + "description": "\u03a3\u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Bayesian \u03b7 \u03bc\u03b5\u03c4\u03b1\u03b2\u03bb\u03b7\u03c4\u03ae `prob_given_false` \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03ce\u03c1\u03b1 \u03bc\u03b9\u03b1 \u03c5\u03c0\u03bf\u03c7\u03c1\u03b5\u03c9\u03c4\u03b9\u03ba\u03ae \u03bc\u03b5\u03c4\u03b1\u03b2\u03bb\u03b7\u03c4\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2, \u03ba\u03b1\u03b8\u03ce\u03c2 \u03b4\u03b5\u03bd \u03c5\u03c0\u03ae\u03c1\u03c7\u03b5 \u03bc\u03b1\u03b8\u03b7\u03bc\u03b1\u03c4\u03b9\u03ba\u03ae \u03bb\u03bf\u03b3\u03b9\u03ba\u03ae \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03c4\u03b9\u03bc\u03ae. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c3\u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `configuration.yml` \u03b3\u03b9\u03b1 \u03c4\u03bf `bayesian/{entity}`. \u0391\u03c5\u03c4\u03ad\u03c2 \u03bf\u03b9 \u03c0\u03b1\u03c1\u03b1\u03c4\u03b7\u03c1\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b8\u03b1 \u03b1\u03b3\u03bd\u03bf\u03b7\u03b8\u03bf\u03cd\u03bd \u03bc\u03ad\u03c7\u03c1\u03b9 \u03bd\u03b1 \u03c4\u03bf \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5.", + "title": "\u0391\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 YAML \u03b3\u03b9\u03b1 Bayesian" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bayesian/translations/et.json b/homeassistant/components/bayesian/translations/et.json new file mode 100644 index 00000000000..80c3c24edec --- /dev/null +++ b/homeassistant/components/bayesian/translations/et.json @@ -0,0 +1,12 @@ +{ + "issues": { + "manual_migration": { + "description": "Bayesiani sidumine v\u00e4rskendab n\u00fc\u00fcd ka t\u00f5en\u00e4osust kui vaadeldud \"oleku_seisund\", \"\u00fcleval\", \"alla\" v\u00f5i \"v\u00e4\u00e4rtusmall\" v\u00e4\u00e4rtus on \"V\u00e4\u00e4r\", mitte ainult \"True\". Seega ei n\u00f5uta enam iga binaaroleku jaoks dubleerivaid \u00fcksteist t\u00e4iendavaid kirjeid. Eemalda \u00fcksuse ` {entity} ` peegelkirje.", + "title": "Bayesiani jaoks on vajalik k\u00e4sitsi YAML-i muutmine" + }, + "no_prob_given_false": { + "description": "Bayesiani sidumises on 'prob_given_false' n\u00fc\u00fcd n\u00f5utav konfiguratsioonimuutuja, kuna eelmisel vaikev\u00e4\u00e4rtusel polnud matemaatilist p\u00f5hjendust. Palun lisa see oma faili `configuration.yaml' 'bayesian/ {entity} ' jaoks. Neid t\u00e4helepanekuid eiratakse seni, kuni seda teed.", + "title": "Bayesiani jaoks on vajalik k\u00e4sitsi YAML-i lisamine" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bayesian/translations/zh-Hant.json b/homeassistant/components/bayesian/translations/zh-Hant.json new file mode 100644 index 00000000000..c56dfa6fcbe --- /dev/null +++ b/homeassistant/components/bayesian/translations/zh-Hant.json @@ -0,0 +1,12 @@ +{ + "issues": { + "manual_migration": { + "description": "\u5047\u5982\u767c\u73fe\u5230 `to_state`\u3001`above`\u3001`below` \u6216 `value_template` \u8a55\u4f30\u70ba `False` \u800c\u975e\u53ea\u662f `True`\uff0cBayesian \u8c9d\u5f0f\u7d71\u8a08\u6574\u5408\u4e5f\u6703\u9032\u884c\u66f4\u65b0\u6982\u7387\u3002 \u56e0\u6b64\u4e0d\u518d\u9700\u8981\u70ba\u6bcf\u500b\u4e8c\u9032\u4f4d\u611f\u6e2c\u5668\u63d0\u4f9b\u91cd\u8907\u3001\u88dc\u5145\u5be6\u9ad4\u3002\u8acb\u79fb\u9664 `{entity}` \u7684\u93e1\u50cf\u5be6\u9ad4\u3002", + "title": "\u9700\u8981\u65bc YAML \u624b\u52d5\u4fee\u6b63 Bayesian \u8c9d\u5f0f\u7d71\u8a08" + }, + "no_prob_given_false": { + "description": "\u65bc Bayesian \u8c9d\u5f0f\u7d71\u8a08\u6574\u5408\u4e4b `prob_given_false`\u3001\u7531\u65bc\u5148\u524d\u7684\u9810\u8a2d\u503c\u4e26\u6c92\u6709\u6578\u5b78\u539f\u7406\u3001\u56e0\u6b64\u73fe\u5728\u5fc5\u9808\u8a2d\u5b9a\u8b8a\u6578\u3002\u8acb\u65bc `configuration.yml` \u4e2d\u65b0\u589e\u4ee5\u7372\u5f97 `bayesian/{entity}`\u3002\u5728\u9032\u884c\u4fee\u6b63\u524d\u3001\u4efb\u4f55\u89c0\u5bdf\u7d50\u679c\u5c07\u88ab\u5ffd\u7565\u3002", + "title": "\u9700\u8981\u65bc YAML \u624b\u52d5\u6dfb\u52a0 Bayesian \u8c9d\u5f0f\u7d71\u8a08" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/hu.json b/homeassistant/components/bluemaestro/translations/hu.json index 97fbb5b9408..4668ffea416 100644 --- a/homeassistant/components/bluemaestro/translations/hu.json +++ b/homeassistant/components/bluemaestro/translations/hu.json @@ -15,7 +15,7 @@ "data": { "address": "Eszk\u00f6z" }, - "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" } } } diff --git a/homeassistant/components/bluetooth/translations/hu.json b/homeassistant/components/bluetooth/translations/hu.json index 79dc3204031..e5b94f070ea 100644 --- a/homeassistant/components/bluetooth/translations/hu.json +++ b/homeassistant/components/bluetooth/translations/hu.json @@ -25,7 +25,7 @@ "data": { "address": "Eszk\u00f6z" }, - "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" } } }, diff --git a/homeassistant/components/braviatv/translations/el.json b/homeassistant/components/braviatv/translations/el.json index 070f546d532..4e479ddad74 100644 --- a/homeassistant/components/braviatv/translations/el.json +++ b/homeassistant/components/braviatv/translations/el.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", - "no_ip_control": "\u039f \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 IP \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03ae \u03b7 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9." + "no_ip_control": "\u039f \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 IP \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03ae \u03b7 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9.", + "not_bravia_device": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 Bravia." }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "invalid_host": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", "unsupported_model": "\u03a4\u03bf \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03c4\u03b7\u03c2 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9." }, "step": { "authorize": { "data": { - "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN" + "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", + "use_psk": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 PSK" }, "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc PIN \u03c0\u03bf\u03c5 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 Sony Bravia. \n\n\u0395\u03ac\u03bd \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03b4\u03b5\u03bd \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae \u03c4\u03bf\u03c5 Home Assistant \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 -> \u0394\u03af\u03ba\u03c4\u03c5\u03bf -> \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 -> \u0394\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae \u03c4\u03b7\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b9\u03c3\u03b7\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2.", "title": "\u0395\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 Sony Bravia TV" }, + "confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7;" + }, "user": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" diff --git a/homeassistant/components/bthome/translations/hu.json b/homeassistant/components/bthome/translations/hu.json index 1bf4fffab68..11a8592dbe5 100644 --- a/homeassistant/components/bthome/translations/hu.json +++ b/homeassistant/components/bthome/translations/hu.json @@ -25,7 +25,7 @@ "data": { "address": "Eszk\u00f6z" }, - "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" } } } diff --git a/homeassistant/components/dsmr_reader/translations/el.json b/homeassistant/components/dsmr_reader/translations/el.json new file mode 100644 index 00000000000..34da88de0ff --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/el.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + }, + "step": { + "confirm": { + "description": "\u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5\u03c2 \u03c4\u03c9\u03bd \u03c0\u03b7\u03b3\u03ce\u03bd \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u00ab\u03b4\u03b9\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \u03b8\u03ad\u03bc\u03b1\u03c4\u03bf\u03c2\u00bb \u03c3\u03c4\u03bf\u03bd \u0391\u03bd\u03b1\u03b3\u03bd\u03ce\u03c3\u03c4\u03b7 DSMR." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03bf\u03b3\u03c1\u03ac\u03bc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2 DSMR \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03bf\u03c5 DSMR Reader \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03bf\u03b3\u03c1\u03ac\u03bc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2 DSMR \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/ca.json b/homeassistant/components/ezviz/translations/ca.json index 7c71de300f6..08c9f2af4d1 100644 --- a/homeassistant/components/ezviz/translations/ca.json +++ b/homeassistant/components/ezviz/translations/ca.json @@ -18,7 +18,7 @@ "username": "Nom d'usuari" }, "description": "Introdueix les credencials RTSP per a la c\u00e0mera Ezviz {serial} amb IP {ip_address}", - "title": "S'ha descobert c\u00e0mera Ezviz" + "title": "S'ha descobert una c\u00e0mera EZVIZ" }, "user": { "data": { @@ -26,7 +26,7 @@ "url": "URL", "username": "Nom d'usuari" }, - "title": "Connexi\u00f3 amb Ezviz Cloud" + "title": "Connexi\u00f3 amb EZVIZ Cloud" }, "user_custom_url": { "data": { @@ -35,7 +35,7 @@ "username": "Nom d'usuari" }, "description": "Especifica manualment l'URL de teva regi\u00f3", - "title": "Connexi\u00f3 amb URL de Ezviz personalitzat" + "title": "Connexi\u00f3 a URL d'EZVIZ personalitzat" } } }, diff --git a/homeassistant/components/forked_daapd/translations/ca.json b/homeassistant/components/forked_daapd/translations/ca.json index f84b0376dd6..e35929915fc 100644 --- a/homeassistant/components/forked_daapd/translations/ca.json +++ b/homeassistant/components/forked_daapd/translations/ca.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", - "not_forked_daapd": "El dispositiu no \u00e9s un servidor de forked-daapd." + "not_forked_daapd": "El dispositiu no \u00e9s un servidor Owntone." }, "error": { - "forbidden": "No s'ha pogut connectar. Comprova els permisos de xarxa de forked-daapd.", + "forbidden": "No s'ha pogut connectar. Comprova els permisos de xarxa d'Owntone.", "unknown_error": "Error inesperat", - "websocket_not_enabled": "El websocket de forked-daapd no est\u00e0 activat.", + "websocket_not_enabled": "El websocket d'Owntone no est\u00e0 activat.", "wrong_host_or_port": "No s'ha pogut connectar, verifica l'amfitri\u00f3 i el port.", "wrong_password": "Contrasenya incorrecta.", - "wrong_server_type": "La integraci\u00f3 forked-daapd necessita un servidor forked-daapd amb versi\u00f3 >= 27.0." + "wrong_server_type": "La integraci\u00f3 Owntone necessita un servidor Owntone amb versi\u00f3 >= 27.0." }, "flow_title": "{name} ({host})", "step": { @@ -21,7 +21,7 @@ "password": "Contrasenya de l'API (deixa-ho en blanc si no t\u00e9 contrasenya)", "port": "Port de l'API" }, - "title": "Configuraci\u00f3 del dispositiu forked-daapd" + "title": "Configuraci\u00f3 de dispositiu Owntone" } } }, @@ -34,8 +34,8 @@ "tts_pause_time": "Segons de pausa abans i despr\u00e9s de TTS", "tts_volume": "Volum TTS (valor 'float' entre [0,1])" }, - "description": "Configura les diferents opcions de la integraci\u00f3 forked-daapd.", - "title": "Configuraci\u00f3 de les opcions de forked-daapd" + "description": "Configura les diferents opcions de la integraci\u00f3 Owntone.", + "title": "Configuraci\u00f3 de les opcions d'Owntone" } } } diff --git a/homeassistant/components/google_sheets/translations/el.json b/homeassistant/components/google_sheets/translations/el.json index e7527dcb7d3..ec84423dfe5 100644 --- a/homeassistant/components/google_sheets/translations/el.json +++ b/homeassistant/components/google_sheets/translations/el.json @@ -25,6 +25,10 @@ }, "pick_implementation": { "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "reauth_confirm": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Google Sheets \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" } } } diff --git a/homeassistant/components/govee_ble/translations/hu.json b/homeassistant/components/govee_ble/translations/hu.json index 7ef0d3a6301..e1673194c6d 100644 --- a/homeassistant/components/govee_ble/translations/hu.json +++ b/homeassistant/components/govee_ble/translations/hu.json @@ -14,7 +14,7 @@ "data": { "address": "Eszk\u00f6z" }, - "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" } } } diff --git a/homeassistant/components/inkbird/translations/hu.json b/homeassistant/components/inkbird/translations/hu.json index 7ef0d3a6301..e1673194c6d 100644 --- a/homeassistant/components/inkbird/translations/hu.json +++ b/homeassistant/components/inkbird/translations/hu.json @@ -14,7 +14,7 @@ "data": { "address": "Eszk\u00f6z" }, - "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" } } } diff --git a/homeassistant/components/kegtron/translations/hu.json b/homeassistant/components/kegtron/translations/hu.json index 97fbb5b9408..4668ffea416 100644 --- a/homeassistant/components/kegtron/translations/hu.json +++ b/homeassistant/components/kegtron/translations/hu.json @@ -15,7 +15,7 @@ "data": { "address": "Eszk\u00f6z" }, - "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" } } } diff --git a/homeassistant/components/litterrobot/translations/el.json b/homeassistant/components/litterrobot/translations/el.json index d5f7cabb2df..f965d1be9ca 100644 --- a/homeassistant/components/litterrobot/translations/el.json +++ b/homeassistant/components/litterrobot/translations/el.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "\u03a4\u03b1 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03c4\u03b7\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03ba\u03bf\u03cd\u03c0\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03ce\u03c1\u03b1 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b1 \u03c9\u03c2 \u03b4\u03b9\u03b1\u03b3\u03bd\u03c9\u03c3\u03c4\u03b9\u03ba\u03bf\u03af \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2.\n\n\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03cc\u03c3\u03c4\u03b5 \u03c4\u03c5\u03c7\u03cc\u03bd \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03ad\u03c7\u03b5\u03c4\u03b5 \u03ba\u03b1\u03b9 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ac \u03c4\u03b1 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac.", + "title": "\u03a4\u03b1 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac Litter-Robot \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03bf\u03b9 \u03b4\u03b9\u03ba\u03bf\u03af \u03c4\u03bf\u03c5\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2" + } } } \ No newline at end of file diff --git a/homeassistant/components/moat/translations/hu.json b/homeassistant/components/moat/translations/hu.json index 7ef0d3a6301..e1673194c6d 100644 --- a/homeassistant/components/moat/translations/hu.json +++ b/homeassistant/components/moat/translations/hu.json @@ -14,7 +14,7 @@ "data": { "address": "Eszk\u00f6z" }, - "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" } } } diff --git a/homeassistant/components/moon/translations/el.json b/homeassistant/components/moon/translations/el.json index 51cde4e9c65..11791927ad2 100644 --- a/homeassistant/components/moon/translations/el.json +++ b/homeassistant/components/moon/translations/el.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 Moon \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af.\n\n\u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant.\n\n\u0391\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Moon YAML \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" + } + }, "title": "\u03a6\u03b5\u03b3\u03b3\u03ac\u03c1\u03b9" } \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/hu.json b/homeassistant/components/qingping/translations/hu.json index 140271f7840..7913c9946c0 100644 --- a/homeassistant/components/qingping/translations/hu.json +++ b/homeassistant/components/qingping/translations/hu.json @@ -15,7 +15,7 @@ "data": { "address": "Eszk\u00f6z" }, - "description": "V\u00e1lasszon ki egy eszk\u00f6zt a be\u00e1ll\u00edt\u00e1shoz" + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" } } } diff --git a/homeassistant/components/season/translations/el.json b/homeassistant/components/season/translations/el.json index 52bf5ca6126..2f3e0ba48bc 100644 --- a/homeassistant/components/season/translations/el.json +++ b/homeassistant/components/season/translations/el.json @@ -10,5 +10,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 Season \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af.\n\n\u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant.\n\n\u0391\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Season YAML \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" + } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/el.json b/homeassistant/components/sensor/translations/el.json index 543ef1c24ad..6acf181aad3 100644 --- a/homeassistant/components/sensor/translations/el.json +++ b/homeassistant/components/sensor/translations/el.json @@ -6,6 +6,7 @@ "is_carbon_dioxide": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03b4\u03b9\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03ac\u03bd\u03b8\u03c1\u03b1\u03ba\u03b1 \u03c4\u03bf\u03c5 {entity_name}", "is_carbon_monoxide": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03bc\u03bf\u03bd\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03ac\u03bd\u03b8\u03c1\u03b1\u03ba\u03b1 \u03c4\u03bf\u03c5 {entity_name}", "is_current": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03c1\u03b5\u03cd\u03bc\u03b1 \u03b3\u03b9\u03b1 {entity_name}", + "is_distance": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b1\u03c0\u03cc\u03c3\u03c4\u03b1\u03c3\u03b7 {entity_name}", "is_energy": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1 {entity_name}", "is_frequency": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c3\u03c5\u03c7\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 {entity_name}", "is_gas": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b1\u03ad\u03c1\u03b9\u03bf {entity_name}", @@ -24,11 +25,14 @@ "is_pressure": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c0\u03af\u03b5\u03c3\u03b7 {entity_name}", "is_reactive_power": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03ac\u03b5\u03c1\u03b3\u03b7 \u03b9\u03c3\u03c7\u03cd\u03c2 {entity_name}", "is_signal_strength": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b9\u03c3\u03c7\u03cd\u03c2 \u03c3\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 {entity_name}", + "is_speed": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c4\u03b1\u03c7\u03cd\u03c4\u03b7\u03c4\u03b1 {entity_name}", "is_sulphur_dioxide": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03b4\u03b9\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b8\u03b5\u03af\u03bf\u03c5 {entity_name}", "is_temperature": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1 {entity_name}", "is_value": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 {entity_name}", "is_volatile_organic_compounds": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03c0\u03c4\u03b7\u03c4\u03b9\u03ba\u03ce\u03bd \u03bf\u03c1\u03b3\u03b1\u03bd\u03b9\u03ba\u03ce\u03bd \u03b5\u03bd\u03ce\u03c3\u03b5\u03c9\u03bd {entity_name}", - "is_voltage": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c4\u03ac\u03c3\u03b7 {entity_name}" + "is_voltage": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c4\u03ac\u03c3\u03b7 {entity_name}", + "is_volume": "\u03a4\u03c1\u03ad\u03c7\u03c9\u03bd \u03cc\u03b3\u03ba\u03bf\u03c2 {entity_name}", + "is_weight": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b2\u03ac\u03c1\u03bf\u03c2 {entity_name}" }, "trigger_type": { "apparent_power": "\u0395\u03bc\u03c6\u03b1\u03bd\u03b5\u03af\u03c2 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2 \u03b9\u03c3\u03c7\u03cd\u03bf\u03c2 {entity_name}", @@ -36,6 +40,7 @@ "carbon_dioxide": "\u0397 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03ac\u03bd\u03b8\u03c1\u03b1\u03ba\u03b1 \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", "carbon_monoxide": "\u0397 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03bc\u03bf\u03bd\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03ac\u03bd\u03b8\u03c1\u03b1\u03ba\u03b1 \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", "current": "{entity_name} \u03c4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b5\u03c2 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2", + "distance": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03b1\u03c0\u03cc\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 {entity_name}", "energy": "\u0397 \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1 \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", "frequency": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2 \u03c3\u03c5\u03c7\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 {entity_name}", "gas": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03b1\u03b5\u03c1\u03af\u03bf\u03c5", @@ -54,11 +59,14 @@ "pressure": "\u0397 \u03c0\u03af\u03b5\u03c3\u03b7 \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", "reactive_power": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2 \u03b1\u03ad\u03c1\u03b3\u03bf\u03c5 \u03b9\u03c3\u03c7\u03cd\u03bf\u03c2 {entity_name}", "signal_strength": "\u0397 \u03b9\u03c3\u03c7\u03cd\u03c2 \u03c4\u03bf\u03c5 \u03c3\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", + "speed": "\u0397 \u03c4\u03b1\u03c7\u03cd\u03c4\u03b7\u03c4\u03b1 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", "sulphur_dioxide": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03b4\u03b9\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b8\u03b5\u03af\u03bf\u03c5", "temperature": "\u0397 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", "value": "\u0397 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", "volatile_organic_compounds": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03c0\u03c4\u03b7\u03c4\u03b9\u03ba\u03ce\u03bd \u03bf\u03c1\u03b3\u03b1\u03bd\u03b9\u03ba\u03ce\u03bd \u03b5\u03bd\u03ce\u03c3\u03b5\u03c9\u03bd {entity_name}", - "voltage": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03c4\u03ac\u03c3\u03b7\u03c2 {entity_name}" + "voltage": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03c4\u03ac\u03c3\u03b7\u03c2 {entity_name}", + "volume": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03cc\u03b3\u03ba\u03bf\u03c5 {entity_name}", + "weight": "\u03a4\u03bf \u03b2\u03ac\u03c1\u03bf\u03c2 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9" } }, "state": { diff --git a/homeassistant/components/sensorpro/translations/hu.json b/homeassistant/components/sensorpro/translations/hu.json index 97fbb5b9408..4668ffea416 100644 --- a/homeassistant/components/sensorpro/translations/hu.json +++ b/homeassistant/components/sensorpro/translations/hu.json @@ -15,7 +15,7 @@ "data": { "address": "Eszk\u00f6z" }, - "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" } } } diff --git a/homeassistant/components/sensorpush/translations/hu.json b/homeassistant/components/sensorpush/translations/hu.json index 7ef0d3a6301..e1673194c6d 100644 --- a/homeassistant/components/sensorpush/translations/hu.json +++ b/homeassistant/components/sensorpush/translations/hu.json @@ -14,7 +14,7 @@ "data": { "address": "Eszk\u00f6z" }, - "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" } } } diff --git a/homeassistant/components/shelly/translations/el.json b/homeassistant/components/shelly/translations/el.json index a5680f9343a..01c7af19be0 100644 --- a/homeassistant/components/shelly/translations/el.json +++ b/homeassistant/components/shelly/translations/el.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "reauth_unsuccessful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b1\u03bd\u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03be\u03b1\u03bd\u03ac.", "unsupported_firmware": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03bc\u03b9\u03b1 \u03bc\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03c5\u03bb\u03b9\u03ba\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03bf\u03cd." }, "error": { @@ -21,6 +23,12 @@ "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" } }, + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + }, "user": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" diff --git a/homeassistant/components/tautulli/translations/el.json b/homeassistant/components/tautulli/translations/el.json index a83662fb6e6..6f105458435 100644 --- a/homeassistant/components/tautulli/translations/el.json +++ b/homeassistant/components/tautulli/translations/el.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." }, diff --git a/homeassistant/components/thermobeacon/translations/hu.json b/homeassistant/components/thermobeacon/translations/hu.json index 97fbb5b9408..4668ffea416 100644 --- a/homeassistant/components/thermobeacon/translations/hu.json +++ b/homeassistant/components/thermobeacon/translations/hu.json @@ -15,7 +15,7 @@ "data": { "address": "Eszk\u00f6z" }, - "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" } } } diff --git a/homeassistant/components/thermopro/translations/hu.json b/homeassistant/components/thermopro/translations/hu.json index 7ef0d3a6301..e1673194c6d 100644 --- a/homeassistant/components/thermopro/translations/hu.json +++ b/homeassistant/components/thermopro/translations/hu.json @@ -14,7 +14,7 @@ "data": { "address": "Eszk\u00f6z" }, - "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" } } } diff --git a/homeassistant/components/tilt_ble/translations/hu.json b/homeassistant/components/tilt_ble/translations/hu.json index 7ef0d3a6301..e1673194c6d 100644 --- a/homeassistant/components/tilt_ble/translations/hu.json +++ b/homeassistant/components/tilt_ble/translations/hu.json @@ -14,7 +14,7 @@ "data": { "address": "Eszk\u00f6z" }, - "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" } } } diff --git a/homeassistant/components/uptime/translations/el.json b/homeassistant/components/uptime/translations/el.json index d70141f2173..7e338844e9d 100644 --- a/homeassistant/components/uptime/translations/el.json +++ b/homeassistant/components/uptime/translations/el.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u0397 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 Uptime \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af.\n\n\u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant.\n\n\u0391\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03bf\u03c5 Uptime \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + } + }, "title": "\u03a7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2" } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/hu.json b/homeassistant/components/xiaomi_ble/translations/hu.json index 044f970038b..fed82381dcb 100644 --- a/homeassistant/components/xiaomi_ble/translations/hu.json +++ b/homeassistant/components/xiaomi_ble/translations/hu.json @@ -41,7 +41,7 @@ "data": { "address": "Eszk\u00f6z" }, - "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" } } } diff --git a/homeassistant/components/zha/translations/el.json b/homeassistant/components/zha/translations/el.json index 69c437a2e06..154eb011da3 100644 --- a/homeassistant/components/zha/translations/el.json +++ b/homeassistant/components/zha/translations/el.json @@ -116,6 +116,8 @@ }, "device_automation": { "action_type": { + "issue_all_led_effect": "\u0395\u03c6\u03ad \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b3\u03b9\u03b1 \u03cc\u03bb\u03b1 \u03c4\u03b1 LED", + "issue_individual_led_effect": "\u0395\u03c6\u03ad \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b3\u03b9\u03b1 \u03bc\u03b5\u03bc\u03bf\u03bd\u03c9\u03bc\u03ad\u03bd\u03b1 LED", "squawk": "\u039a\u03b1\u03ba\u03ac\u03c1\u03b9\u03c3\u03bc\u03b1", "warn": "\u03a0\u03c1\u03bf\u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7" }, From 816af8573ff8576969eaa7358bc7021c2635a58a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 1 Oct 2022 09:52:07 +0200 Subject: [PATCH 065/985] Fix _attr_name issue in Yale Smart Alarm (#79378) Fix name issue --- .../components/yale_smart_alarm/alarm_control_panel.py | 3 ++- homeassistant/components/yale_smart_alarm/lock.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index e2df1b09ebe..8577a2a179f 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -14,6 +14,7 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntityFeature, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -80,7 +81,7 @@ class YaleAlarmDevice(YaleAlarmEntity, AlarmControlPanelEntity): ) except YALE_ALL_ERRORS as error: raise HomeAssistantError( - f"Could not set alarm for {self._attr_name}: {error}" + f"Could not set alarm for {self.coordinator.entry.data[CONF_NAME]}: {error}" ) from error if alarm_state: diff --git a/homeassistant/components/yale_smart_alarm/lock.py b/homeassistant/components/yale_smart_alarm/lock.py index 8f9ed6c9ce1..807ecdecada 100644 --- a/homeassistant/components/yale_smart_alarm/lock.py +++ b/homeassistant/components/yale_smart_alarm/lock.py @@ -46,7 +46,7 @@ class YaleDoorlock(YaleEntity, LockEntity): """Initialize the Yale Lock Device.""" super().__init__(coordinator, data) self._attr_code_format = f"^\\d{code_format}$" - self.lock_name = data["name"] + self.lock_name: str = data["name"] async def async_unlock(self, **kwargs: Any) -> None: """Send unlock command.""" @@ -79,14 +79,14 @@ class YaleDoorlock(YaleEntity, LockEntity): ) except YALE_ALL_ERRORS as error: raise HomeAssistantError( - f"Could not set lock for {self._attr_name}: {error}" + f"Could not set lock for {self.lock_name}: {error}" ) from error if lock_state: self.coordinator.data["lock_map"][self._attr_unique_id] = command self.async_write_ha_state() return - raise HomeAssistantError("Could set lock, check system ready for lock.") + raise HomeAssistantError("Could not set lock, check system ready for lock.") @property def is_locked(self) -> bool | None: From b27f0c70be35c82186a80d53351986711412e909 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 1 Oct 2022 17:22:23 +0300 Subject: [PATCH 066/985] Fix unifiprotect test failing CI (#79406) --- tests/components/unifiprotect/test_media_source.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py index 4b1e47d6b0c..8200a1323ab 100644 --- a/tests/components/unifiprotect/test_media_source.py +++ b/tests/components/unifiprotect/test_media_source.py @@ -711,19 +711,18 @@ async def test_browse_media_eventthumb( @freeze_time("2022-09-15 03:00:00-07:00") async def test_browse_media_day( - hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ): """Test browsing day selector level media.""" start = datetime.fromisoformat("2022-09-03 03:00:00-07:00") + end = datetime.fromisoformat("2022-09-15 03:00:00-07:00") ufp.api.bootstrap._recording_start = dt_util.as_utc(start) ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) await init_entry(hass, ufp, [doorbell], regenerate_ids=False) - base_id = ( - f"test_id:browse:{doorbell.id}:all:range:{fixed_now.year}:{fixed_now.month}" - ) + base_id = f"test_id:browse:{doorbell.id}:all:range:{end.year}:{end.month}" source = await async_get_media_source(hass) media_item = MediaSourceItem(hass, DOMAIN, base_id, None) @@ -731,7 +730,7 @@ async def test_browse_media_day( assert ( browse.title - == f"UnifiProtect > {doorbell.name} > All Events > {fixed_now.strftime('%B %Y')}" + == f"UnifiProtect > {doorbell.name} > All Events > {end.strftime('%B %Y')}" ) assert browse.identifier == base_id assert len(browse.children) == 14 From 8ff12eacd41aa17b73a9aec5d9d345082999ea1d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 1 Oct 2022 14:33:11 +0000 Subject: [PATCH 067/985] Do not use AQI device class for CAQI sensor in Airly integration (#79402) --- homeassistant/components/airly/sensor.py | 2 +- tests/components/airly/test_sensor.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index bbb501ae47b..122990adecc 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -68,7 +68,7 @@ class AirlySensorEntityDescription(SensorEntityDescription): SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = ( AirlySensorEntityDescription( key=ATTR_API_CAQI, - device_class=SensorDeviceClass.AQI, + icon="mdi:air-filter", name=ATTR_API_CAQI, native_unit_of_measurement="CAQI", ), diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py index 9ac10f20fc3..c95d2d895fd 100644 --- a/tests/components/airly/test_sensor.py +++ b/tests/components/airly/test_sensor.py @@ -11,6 +11,7 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, + ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, PERCENTAGE, @@ -37,7 +38,7 @@ async def test_sensor(hass, aioclient_mock): assert state.state == "7" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "CAQI" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.AQI + assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" entry = registry.async_get("sensor.home_caqi") assert entry From ec8901b9af19bc9df7d0ff43e946def6e7783d42 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Oct 2022 04:44:45 -1000 Subject: [PATCH 068/985] Improve robustness of linking homekit yaml to config entries (#79386) --- homeassistant/components/homekit/__init__.py | 71 +++++++++++------- tests/components/homekit/test_homekit.py | 78 +++++++++++++++++++- 2 files changed, 120 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index c203d674710..36c7bac9d0a 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -193,14 +193,21 @@ def _async_all_homekit_instances(hass: HomeAssistant) -> list[HomeKit]: ] -def _async_get_entries_by_name( +def _async_get_imported_entries_indices( current_entries: list[ConfigEntry], -) -> dict[str, ConfigEntry]: - """Return a dict of the entries by name.""" +) -> tuple[dict[str, ConfigEntry], dict[int, ConfigEntry]]: + """Return a dicts of the entries by name and port.""" # For backwards compat, its possible the first bridge is using the default # name. - return {entry.data.get(CONF_NAME, BRIDGE_NAME): entry for entry in current_entries} + entries_by_name: dict[str, ConfigEntry] = {} + entries_by_port: dict[int, ConfigEntry] = {} + for entry in current_entries: + if entry.source != SOURCE_IMPORT: + continue + entries_by_name[entry.data.get(CONF_NAME, BRIDGE_NAME)] = entry + entries_by_port[entry.data.get(CONF_PORT, DEFAULT_PORT)] = entry + return entries_by_name, entries_by_port async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -218,10 +225,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True current_entries = hass.config_entries.async_entries(DOMAIN) - entries_by_name = _async_get_entries_by_name(current_entries) + entries_by_name, entries_by_port = _async_get_imported_entries_indices( + current_entries + ) for index, conf in enumerate(config[DOMAIN]): - if _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf): + if _async_update_config_entry_from_yaml( + hass, entries_by_name, entries_by_port, conf + ): continue conf[CONF_ENTRY_INDEX] = index @@ -237,8 +248,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @callback -def _async_update_config_entry_if_from_yaml( - hass: HomeAssistant, entries_by_name: dict[str, ConfigEntry], conf: ConfigType +def _async_update_config_entry_from_yaml( + hass: HomeAssistant, + entries_by_name: dict[str, ConfigEntry], + entries_by_port: dict[int, ConfigEntry], + conf: ConfigType, ) -> bool: """Update a config entry with the latest yaml. @@ -246,27 +260,24 @@ def _async_update_config_entry_if_from_yaml( Returns False if there is no matching config entry """ - bridge_name = conf[CONF_NAME] - - if ( - bridge_name in entries_by_name - and entries_by_name[bridge_name].source == SOURCE_IMPORT + if not ( + matching_entry := entries_by_name.get(conf.get(CONF_NAME, BRIDGE_NAME)) + or entries_by_port.get(conf.get(CONF_PORT, DEFAULT_PORT)) ): - entry = entries_by_name[bridge_name] - # If they alter the yaml config we import the changes - # since there currently is no practical way to support - # all the options in the UI at this time. - data = conf.copy() - options = {} - for key in CONFIG_OPTIONS: - if key in data: - options[key] = data[key] - del data[key] + return False - hass.config_entries.async_update_entry(entry, data=data, options=options) - return True + # If they alter the yaml config we import the changes + # since there currently is no practical way to support + # all the options in the UI at this time. + data = conf.copy() + options = {} + for key in CONFIG_OPTIONS: + if key in data: + options[key] = data[key] + del data[key] - return False + hass.config_entries.async_update_entry(matching_entry, data=data, options=options) + return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -451,10 +462,14 @@ def _async_register_events_and_services(hass: HomeAssistant) -> None: return current_entries = hass.config_entries.async_entries(DOMAIN) - entries_by_name = _async_get_entries_by_name(current_entries) + entries_by_name, entries_by_port = _async_get_imported_entries_indices( + current_entries + ) for conf in config[DOMAIN]: - _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf) + _async_update_config_entry_from_yaml( + hass, entries_by_name, entries_by_port, conf + ) reload_tasks = [ hass.config_entries.async_reload(entry.entry_id) diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index be514ce2b6a..dbb63ba690a 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -35,7 +35,7 @@ from homeassistant.components.homekit.const import ( from homeassistant.components.homekit.type_triggers import DeviceTriggerAccessory from homeassistant.components.homekit.util import get_persist_fullpath_for_entry_id from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_ZEROCONF from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_DEVICE_ID, @@ -1394,6 +1394,82 @@ async def test_yaml_updates_update_config_entry_for_name(hass, mock_async_zeroco mock_homekit().async_start.assert_called() +async def test_yaml_can_link_with_default_name(hass, mock_async_zeroconf): + """Test async_setup with imported config linked by default name.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_IMPORT, + data={}, + options={}, + ) + entry.add_to_hass(hass) + + with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, patch( + "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" + ): + mock_homekit.return_value = homekit = Mock() + type(homekit).async_start = AsyncMock() + assert await async_setup_component( + hass, + "homekit", + {"homekit": {"entity_config": {"camera.back_camera": {"stream_count": 3}}}}, + ) + await hass.async_block_till_done() + + mock_homekit.reset_mock() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert entry.options["entity_config"]["camera.back_camera"]["stream_count"] == 3 + + +async def test_yaml_can_link_with_port(hass, mock_async_zeroconf): + """Test async_setup with imported config linked by port.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_IMPORT, + data={"name": "random", "port": 12345}, + options={}, + ) + entry.add_to_hass(hass) + entry2 = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_IMPORT, + data={"name": "random", "port": 12346}, + options={}, + ) + entry2.add_to_hass(hass) + entry3 = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_ZEROCONF, + data={"name": "random", "port": 12347}, + options={}, + ) + entry3.add_to_hass(hass) + with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, patch( + "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" + ): + mock_homekit.return_value = homekit = Mock() + type(homekit).async_start = AsyncMock() + assert await async_setup_component( + hass, + "homekit", + { + "homekit": { + "port": 12345, + "entity_config": {"camera.back_camera": {"stream_count": 3}}, + } + }, + ) + await hass.async_block_till_done() + + mock_homekit.reset_mock() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert entry.options["entity_config"]["camera.back_camera"]["stream_count"] == 3 + assert entry2.options == {} + assert entry3.options == {} + + async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_async_zeroconf): """Test HomeKit uses system zeroconf.""" entry = MockConfigEntry( From 062ee75de953986bec7537a9b477728b2eee64f0 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 1 Oct 2022 18:04:11 +0300 Subject: [PATCH 069/985] Bump aioswitcher to 3.0.2 (#79399) Bump aioswitcher to 3.0.2 --- homeassistant/components/switcher_kis/manifest.json | 2 +- homeassistant/components/switcher_kis/switch.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/switcher_kis/conftest.py | 4 ++-- tests/components/switcher_kis/test_services.py | 6 +++--- tests/components/switcher_kis/test_switch.py | 8 ++++---- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index 30a2ac3bb48..508d6a897af 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -3,7 +3,7 @@ "name": "Switcher", "documentation": "https://www.home-assistant.io/integrations/switcher_kis/", "codeowners": ["@tomerfi", "@thecode"], - "requirements": ["aioswitcher==3.0.0"], + "requirements": ["aioswitcher==3.0.2"], "quality_scale": "platinum", "iot_class": "local_push", "config_flow": true, diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 0065038954f..9d1b5d4bdc5 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -6,7 +6,7 @@ from datetime import timedelta import logging from typing import Any -from aioswitcher.api import Command, SwitcherApi, SwitcherBaseResponse +from aioswitcher.api import Command, SwitcherBaseResponse, SwitcherType1Api from aioswitcher.device import DeviceCategory, DeviceState import voluptuous as vol @@ -110,7 +110,7 @@ class SwitcherBaseSwitchEntity( error = None try: - async with SwitcherApi( + async with SwitcherType1Api( self.coordinator.data.ip_address, self.coordinator.data.device_id ) as swapi: response = await getattr(swapi, api)(*args) diff --git a/requirements_all.txt b/requirements_all.txt index 61c5e9ecc13..5e093f3a04e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -267,7 +267,7 @@ aioslimproto==2.1.1 aiosteamist==0.3.2 # homeassistant.components.switcher_kis -aioswitcher==3.0.0 +aioswitcher==3.0.2 # homeassistant.components.syncthing aiosyncthing==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b7bd181170..d96649a343c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -242,7 +242,7 @@ aioslimproto==2.1.1 aiosteamist==0.3.2 # homeassistant.components.switcher_kis -aioswitcher==3.0.0 +aioswitcher==3.0.2 # homeassistant.components.syncthing aiosyncthing==0.5.1 diff --git a/tests/components/switcher_kis/conftest.py b/tests/components/switcher_kis/conftest.py index 3578e3ac6c9..e4c8c7c5acd 100644 --- a/tests/components/switcher_kis/conftest.py +++ b/tests/components/switcher_kis/conftest.py @@ -43,11 +43,11 @@ def mock_api(): patchers = [ patch( - "homeassistant.components.switcher_kis.switch.SwitcherApi.connect", + "homeassistant.components.switcher_kis.switch.SwitcherType1Api.connect", new=api_mock, ), patch( - "homeassistant.components.switcher_kis.switch.SwitcherApi.disconnect", + "homeassistant.components.switcher_kis.switch.SwitcherType1Api.disconnect", new=api_mock, ), ] diff --git a/tests/components/switcher_kis/test_services.py b/tests/components/switcher_kis/test_services.py index 9b0fcee27df..cfc51402be0 100644 --- a/tests/components/switcher_kis/test_services.py +++ b/tests/components/switcher_kis/test_services.py @@ -44,7 +44,7 @@ async def test_turn_on_with_timer_service(hass, mock_bridge, mock_api, monkeypat assert state.state == STATE_OFF with patch( - "homeassistant.components.switcher_kis.switch.SwitcherApi.control_device" + "homeassistant.components.switcher_kis.switch.SwitcherType1Api.control_device" ) as mock_control_device: await hass.services.async_call( DOMAIN, @@ -74,7 +74,7 @@ async def test_set_auto_off_service(hass, mock_bridge, mock_api): entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}" with patch( - "homeassistant.components.switcher_kis.switch.SwitcherApi.set_auto_shutdown" + "homeassistant.components.switcher_kis.switch.SwitcherType1Api.set_auto_shutdown" ) as mock_set_auto_shutdown: await hass.services.async_call( DOMAIN, @@ -99,7 +99,7 @@ async def test_set_auto_off_service_fail(hass, mock_bridge, mock_api, caplog): entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}" with patch( - "homeassistant.components.switcher_kis.switch.SwitcherApi.set_auto_shutdown", + "homeassistant.components.switcher_kis.switch.SwitcherType1Api.set_auto_shutdown", return_value=None, ) as mock_set_auto_shutdown: await hass.services.async_call( diff --git a/tests/components/switcher_kis/test_switch.py b/tests/components/switcher_kis/test_switch.py index a44e0c79611..447de2352fe 100644 --- a/tests/components/switcher_kis/test_switch.py +++ b/tests/components/switcher_kis/test_switch.py @@ -43,7 +43,7 @@ async def test_switch(hass, mock_bridge, mock_api, monkeypatch): # Test turning on with patch( - "homeassistant.components.switcher_kis.switch.SwitcherApi.control_device", + "homeassistant.components.switcher_kis.switch.SwitcherType1Api.control_device", ) as mock_control_device: await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True @@ -56,7 +56,7 @@ async def test_switch(hass, mock_bridge, mock_api, monkeypatch): # Test turning off with patch( - "homeassistant.components.switcher_kis.switch.SwitcherApi.control_device" + "homeassistant.components.switcher_kis.switch.SwitcherType1Api.control_device" ) as mock_control_device: await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True @@ -87,7 +87,7 @@ async def test_switch_control_fail(hass, mock_bridge, mock_api, monkeypatch, cap # Test exception during turn on with patch( - "homeassistant.components.switcher_kis.switch.SwitcherApi.control_device", + "homeassistant.components.switcher_kis.switch.SwitcherType1Api.control_device", side_effect=RuntimeError("fake error"), ) as mock_control_device: await hass.services.async_call( @@ -111,7 +111,7 @@ async def test_switch_control_fail(hass, mock_bridge, mock_api, monkeypatch, cap # Test error response during turn on with patch( - "homeassistant.components.switcher_kis.switch.SwitcherApi.control_device", + "homeassistant.components.switcher_kis.switch.SwitcherType1Api.control_device", return_value=SwitcherBaseResponse(None), ) as mock_control_device: await hass.services.async_call( From 886e6365650295ce085080701e1067e9648e2879 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Sun, 2 Oct 2022 01:05:02 +1000 Subject: [PATCH 070/985] Bump aiolifx to 0.8.6 (#79393) --- homeassistant/components/lifx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 45321f22b66..95718b3ee83 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -3,7 +3,7 @@ "name": "LIFX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lifx", - "requirements": ["aiolifx==0.8.5", "aiolifx_effects==0.2.2"], + "requirements": ["aiolifx==0.8.6", "aiolifx_effects==0.2.2"], "quality_scale": "platinum", "dependencies": ["network"], "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 5e093f3a04e..20f0190aee5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -193,7 +193,7 @@ aiokafka==0.7.2 aiokef==0.2.16 # homeassistant.components.lifx -aiolifx==0.8.5 +aiolifx==0.8.6 # homeassistant.components.lifx aiolifx_effects==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d96649a343c..c47a14ece92 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -171,7 +171,7 @@ aiohue==4.5.0 aiokafka==0.7.2 # homeassistant.components.lifx -aiolifx==0.8.5 +aiolifx==0.8.6 # homeassistant.components.lifx aiolifx_effects==0.2.2 From 31ddf6cc31aaaf08a09cd6afa9eb830450d1817d Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Sat, 1 Oct 2022 17:56:47 +0200 Subject: [PATCH 071/985] Log config_flow errors for waze_travel_time (#79352) --- homeassistant/components/waze_travel_time/helpers.py | 7 ++++++- tests/components/waze_travel_time/test_config_flow.py | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/waze_travel_time/helpers.py b/homeassistant/components/waze_travel_time/helpers.py index 67d8b5674b2..8468bb8ea9a 100644 --- a/homeassistant/components/waze_travel_time/helpers.py +++ b/homeassistant/components/waze_travel_time/helpers.py @@ -1,8 +1,12 @@ """Helpers for Waze Travel Time integration.""" +import logging + from WazeRouteCalculator import WazeRouteCalculator, WRCError from homeassistant.helpers.location import find_coordinates +_LOGGER = logging.getLogger(__name__) + def is_valid_config_entry(hass, origin, destination, region): """Return whether the config entry data is valid.""" @@ -10,6 +14,7 @@ def is_valid_config_entry(hass, origin, destination, region): destination = find_coordinates(hass, destination) try: WazeRouteCalculator(origin, destination, region).calc_all_routes_info() - except WRCError: + except WRCError as error: + _LOGGER.error("Error trying to validate entry: %s", error) return False return True diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py index c4b8144b74d..bc343792218 100644 --- a/tests/components/waze_travel_time/test_config_flow.py +++ b/tests/components/waze_travel_time/test_config_flow.py @@ -176,7 +176,7 @@ async def test_dupe(hass): @pytest.mark.usefixtures("invalidate_config_entry") -async def test_invalid_config_entry(hass): +async def test_invalid_config_entry(hass, caplog): """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -190,3 +190,5 @@ async def test_invalid_config_entry(hass): assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} + + assert "Error trying to validate entry" in caplog.text From 9c9c8b324a47f81f690543b7c28957a0fb4de4ae Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 1 Oct 2022 18:00:54 +0200 Subject: [PATCH 072/985] Ignore an '' value_template result for MQTT sensor (#79417) Do not write state if payload is '' --- homeassistant/components/mqtt/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 7c98fdf51b7..b3869cb8afe 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -271,8 +271,8 @@ class MqttSensor(MqttEntity, RestoreSensor): ) elif self.device_class == SensorDeviceClass.DATE: payload = payload.date() - - self._state = payload + if payload != "": + self._state = payload def _update_last_reset(msg): payload = self._last_reset_template(msg.payload) From f1f01429f47a12a367af7dac1bf216227541f691 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 1 Oct 2022 19:14:55 +0300 Subject: [PATCH 073/985] Add Switcher Breeze support (#78596) * Add switcher Breeze support * Review comments and updates for aioswitcher --- .../components/switcher_kis/__init__.py | 2 +- .../components/switcher_kis/climate.py | 218 ++++++++++++ tests/components/switcher_kis/conftest.py | 8 + tests/components/switcher_kis/consts.py | 29 ++ tests/components/switcher_kis/test_climate.py | 333 ++++++++++++++++++ 5 files changed, 589 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/switcher_kis/climate.py create mode 100644 tests/components/switcher_kis/test_climate.py diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 31273dce23d..890ec65dded 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -29,7 +29,7 @@ from .const import ( ) from .utils import async_start_bridge, async_stop_bridge -PLATFORMS = [Platform.SWITCH, Platform.SENSOR] +PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.SWITCH] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switcher_kis/climate.py b/homeassistant/components/switcher_kis/climate.py new file mode 100644 index 00000000000..75ce386bd39 --- /dev/null +++ b/homeassistant/components/switcher_kis/climate.py @@ -0,0 +1,218 @@ +"""Switcher integration Climate platform.""" +from __future__ import annotations + +import asyncio +from typing import Any, cast + +from aioswitcher.api import SwitcherBaseResponse, SwitcherType2Api +from aioswitcher.api.remotes import SwitcherBreezeRemote, SwitcherBreezeRemoteManager +from aioswitcher.device import ( + DeviceCategory, + DeviceState, + ThermostatFanLevel, + ThermostatMode, + ThermostatSwing, +) + +from homeassistant.components.climate import ( + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + SWING_OFF, + SWING_VERTICAL, + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import SwitcherDataUpdateCoordinator +from .const import SIGNAL_DEVICE_ADD + +DEVICE_MODE_TO_HA = { + ThermostatMode.COOL: HVACMode.COOL, + ThermostatMode.HEAT: HVACMode.HEAT, + ThermostatMode.FAN: HVACMode.FAN_ONLY, + ThermostatMode.DRY: HVACMode.DRY, + ThermostatMode.AUTO: HVACMode.HEAT_COOL, +} + +HA_TO_DEVICE_MODE = {value: key for key, value in DEVICE_MODE_TO_HA.items()} + +DEVICE_FAN_TO_HA = { + ThermostatFanLevel.LOW: FAN_LOW, + ThermostatFanLevel.MEDIUM: FAN_MEDIUM, + ThermostatFanLevel.HIGH: FAN_HIGH, + ThermostatFanLevel.AUTO: FAN_AUTO, +} + +HA_TO_DEVICE_FAN = {value: key for key, value in DEVICE_FAN_TO_HA.items()} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Switcher climate from config entry.""" + remote_manager = SwitcherBreezeRemoteManager() + + async def async_add_climate(coordinator: SwitcherDataUpdateCoordinator) -> None: + """Get remote and add climate from Switcher device.""" + if coordinator.data.device_type.category == DeviceCategory.THERMOSTAT: + remote: SwitcherBreezeRemote = await hass.async_add_executor_job( + remote_manager.get_remote, coordinator.data.remote_id + ) + async_add_entities([SwitcherClimateEntity(coordinator, remote)]) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, SIGNAL_DEVICE_ADD, async_add_climate) + ) + + +class SwitcherClimateEntity( + CoordinatorEntity[SwitcherDataUpdateCoordinator], ClimateEntity +): + """Representation of a Switcher climate entity.""" + + def __init__( + self, coordinator: SwitcherDataUpdateCoordinator, remote: SwitcherBreezeRemote + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._remote = remote + + self._attr_name = coordinator.name + self._attr_unique_id = f"{coordinator.device_id}-{coordinator.mac_address}" + self._attr_device_info = DeviceInfo( + connections={ + (device_registry.CONNECTION_NETWORK_MAC, coordinator.mac_address) + } + ) + + self._attr_min_temp = remote.min_temperature + self._attr_max_temp = remote.max_temperature + self._attr_target_temperature_step = 1 + self._attr_temperature_unit = TEMP_CELSIUS + + self._attr_supported_features = 0 + self._attr_hvac_modes = [HVACMode.OFF] + for mode in remote.modes_features: + self._attr_hvac_modes.append(DEVICE_MODE_TO_HA[mode]) + features = remote.modes_features[mode] + + if features["temperature_control"]: + self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE + + if features["fan_levels"]: + self._attr_supported_features |= ClimateEntityFeature.FAN_MODE + + if features["swing"] and not remote.separated_swing_command: + self._attr_supported_features |= ClimateEntityFeature.SWING_MODE + + self._update_data(True) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_data() + self.async_write_ha_state() + + def _update_data(self, force_update: bool = False) -> None: + """Update data from device.""" + data = self.coordinator.data + features = self._remote.modes_features[data.mode] + + if data.target_temperature == 0 and not force_update: + return + + self._attr_current_temperature = cast(float, data.temperature) + self._attr_target_temperature = float(data.target_temperature) + + self._attr_hvac_mode = HVACMode.OFF + if data.device_state == DeviceState.ON: + self._attr_hvac_mode = DEVICE_MODE_TO_HA[data.mode] + + self._attr_fan_mode = None + self._attr_fan_modes = [] + if features["fan_levels"]: + self._attr_fan_modes = [DEVICE_FAN_TO_HA[x] for x in features["fan_levels"]] + self._attr_fan_mode = DEVICE_FAN_TO_HA[data.fan_level] + + self._attr_swing_mode = None + self._attr_swing_modes = [] + if features["swing"]: + self._attr_swing_mode = SWING_OFF + self._attr_swing_modes = [SWING_VERTICAL, SWING_OFF] + if data.swing == ThermostatSwing.ON: + self._attr_swing_mode = SWING_VERTICAL + + async def _async_control_breeze_device(self, **kwargs: Any) -> None: + """Call Switcher Control Breeze API.""" + response: SwitcherBaseResponse = None + error = None + + try: + async with SwitcherType2Api( + self.coordinator.data.ip_address, self.coordinator.data.device_id + ) as swapi: + response = await swapi.control_breeze_device(self._remote, **kwargs) + except (asyncio.TimeoutError, OSError, RuntimeError) as err: + error = repr(err) + + if error or not response or not response.successful: + self.coordinator.last_update_success = False + self.async_write_ha_state() + raise HomeAssistantError( + f"Call Breeze control for {self.name} failed, " + f"response/error: {response or error}" + ) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + if not self._remote.modes_features[self.coordinator.data.mode][ + "temperature_control" + ]: + raise HomeAssistantError( + "Current mode doesn't support setting Target Temperature" + ) + + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: + raise ValueError("No target temperature provided") + + await self._async_control_breeze_device(target_temp=int(temperature)) + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + if not self._remote.modes_features[self.coordinator.data.mode]["fan_levels"]: + raise HomeAssistantError("Current mode doesn't support setting Fan Mode") + + await self._async_control_breeze_device(fan_mode=HA_TO_DEVICE_FAN[fan_mode]) + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target operation mode.""" + if hvac_mode == hvac_mode.OFF: + await self._async_control_breeze_device(state=DeviceState.OFF) + else: + await self._async_control_breeze_device( + state=DeviceState.ON, mode=HA_TO_DEVICE_MODE[hvac_mode] + ) + + async def async_set_swing_mode(self, swing_mode: str) -> None: + """Set new target swing operation.""" + if not self._remote.modes_features[self.coordinator.data.mode]["swing"]: + raise HomeAssistantError("Current mode doesn't support setting Swing Mode") + + if swing_mode == SWING_VERTICAL: + await self._async_control_breeze_device(swing_mode=ThermostatSwing.ON) + else: + await self._async_control_breeze_device(swing_mode=ThermostatSwing.OFF) diff --git a/tests/components/switcher_kis/conftest.py b/tests/components/switcher_kis/conftest.py index e4c8c7c5acd..7fff1c476fb 100644 --- a/tests/components/switcher_kis/conftest.py +++ b/tests/components/switcher_kis/conftest.py @@ -50,6 +50,14 @@ def mock_api(): "homeassistant.components.switcher_kis.switch.SwitcherType1Api.disconnect", new=api_mock, ), + patch( + "homeassistant.components.switcher_kis.climate.SwitcherType2Api.connect", + new=api_mock, + ), + patch( + "homeassistant.components.switcher_kis.climate.SwitcherType2Api.disconnect", + new=api_mock, + ), ] for patcher in patchers: diff --git a/tests/components/switcher_kis/consts.py b/tests/components/switcher_kis/consts.py index e200d92e026..75a99be2709 100644 --- a/tests/components/switcher_kis/consts.py +++ b/tests/components/switcher_kis/consts.py @@ -4,7 +4,11 @@ from aioswitcher.device import ( DeviceState, DeviceType, SwitcherPowerPlug, + SwitcherThermostat, SwitcherWaterHeater, + ThermostatFanLevel, + ThermostatMode, + ThermostatSwing, ) from homeassistant.components.switcher_kis import ( @@ -18,20 +22,30 @@ DUMMY_AUTO_OFF_SET = "01:30:00" DUMMY_AUTO_SHUT_DOWN = "02:00:00" DUMMY_DEVICE_ID1 = "a123bc" DUMMY_DEVICE_ID2 = "cafe12" +DUMMY_DEVICE_ID3 = "bada77" DUMMY_DEVICE_NAME1 = "Plug 23BC" DUMMY_DEVICE_NAME2 = "Heater FE12" +DUMMY_DEVICE_NAME3 = "Breeze AB39" DUMMY_DEVICE_PASSWORD = "12345678" DUMMY_ELECTRIC_CURRENT1 = 0.5 DUMMY_ELECTRIC_CURRENT2 = 12.8 DUMMY_IP_ADDRESS1 = "192.168.100.157" DUMMY_IP_ADDRESS2 = "192.168.100.158" +DUMMY_IP_ADDRESS3 = "192.168.100.159" DUMMY_MAC_ADDRESS1 = "A1:B2:C3:45:67:D8" DUMMY_MAC_ADDRESS2 = "A1:B2:C3:45:67:D9" +DUMMY_MAC_ADDRESS3 = "A1:B2:C3:45:67:DA" DUMMY_PHONE_ID = "1234" DUMMY_POWER_CONSUMPTION1 = 100 DUMMY_POWER_CONSUMPTION2 = 2780 DUMMY_REMAINING_TIME = "01:29:32" DUMMY_TIMER_MINUTES_SET = "90" +DUMMY_THERMOSTAT_MODE = ThermostatMode.COOL +DUMMY_TEMPERATURE = 24.1 +DUMMY_TARGET_TEMPERATURE = 23 +DUMMY_FAN_LEVEL = ThermostatFanLevel.LOW +DUMMY_SWING = ThermostatSwing.OFF +DUMMY_REMOTE_ID = "ELEC7001" YAML_CONFIG = { DOMAIN: { @@ -65,4 +79,19 @@ DUMMY_WATER_HEATER_DEVICE = SwitcherWaterHeater( DUMMY_AUTO_SHUT_DOWN, ) +DUMMY_THERMOSTAT_DEVICE = SwitcherThermostat( + DeviceType.BREEZE, + DeviceState.ON, + DUMMY_DEVICE_ID3, + DUMMY_IP_ADDRESS3, + DUMMY_MAC_ADDRESS3, + DUMMY_DEVICE_NAME3, + DUMMY_THERMOSTAT_MODE, + DUMMY_TEMPERATURE, + DUMMY_TARGET_TEMPERATURE, + DUMMY_FAN_LEVEL, + DUMMY_SWING, + DUMMY_REMOTE_ID, +) + DUMMY_SWITCHER_DEVICES = [DUMMY_PLUG_DEVICE, DUMMY_WATER_HEATER_DEVICE] diff --git a/tests/components/switcher_kis/test_climate.py b/tests/components/switcher_kis/test_climate.py new file mode 100644 index 00000000000..212ab88746b --- /dev/null +++ b/tests/components/switcher_kis/test_climate.py @@ -0,0 +1,333 @@ +"""Test the Switcher climate platform.""" +from unittest.mock import patch + +from aioswitcher.api import SwitcherBaseResponse +from aioswitcher.device import ( + DeviceState, + ThermostatFanLevel, + ThermostatMode, + ThermostatSwing, +) +import pytest + +from homeassistant.components.climate import ( + ATTR_FAN_MODE, + ATTR_HVAC_MODE, + ATTR_SWING_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_SWING_MODE, + SERVICE_SET_TEMPERATURE, + HVACMode, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNAVAILABLE +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import slugify + +from . import init_integration +from .consts import DUMMY_THERMOSTAT_DEVICE as DEVICE + +ENTITY_ID = f"{CLIMATE_DOMAIN}.{slugify(DEVICE.name)}" + + +@pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) +async def test_climate_hvac_mode(hass, mock_bridge, mock_api, monkeypatch): + """Test climate hvac mode service.""" + await init_integration(hass) + assert mock_bridge + + # Test initial hvac mode - cool + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.COOL + + # Test set hvac mode heat + with patch( + "homeassistant.components.switcher_kis.climate.SwitcherType2Api.control_breeze_device", + ) as mock_control_device: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + + monkeypatch.setattr(DEVICE, "mode", ThermostatMode.HEAT) + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + + assert mock_api.call_count == 2 + mock_control_device.assert_called_once() + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.HEAT + + # Test set hvac mode off + with patch( + "homeassistant.components.switcher_kis.climate.SwitcherType2Api.control_breeze_device", + ) as mock_control_device: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.OFF}, + blocking=True, + ) + + monkeypatch.setattr(DEVICE, "device_state", DeviceState.OFF) + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + + assert mock_api.call_count == 4 + mock_control_device.assert_called_once() + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.OFF + + +@pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) +async def test_climate_temperature(hass, mock_bridge, mock_api, monkeypatch): + """Test climate temperature service.""" + await init_integration(hass) + assert mock_bridge + + # Test initial target temperature + state = hass.states.get(ENTITY_ID) + assert state.attributes["temperature"] == 23 + + # Test set target temperature + with patch( + "homeassistant.components.switcher_kis.climate.SwitcherType2Api.control_breeze_device", + ) as mock_control_device: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 22}, + blocking=True, + ) + + monkeypatch.setattr(DEVICE, "target_temperature", 22) + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + + assert mock_api.call_count == 2 + mock_control_device.assert_called_once() + state = hass.states.get(ENTITY_ID) + assert state.attributes["temperature"] == 22 + + # Test set target temperature - incorrect params + with patch( + "homeassistant.components.switcher_kis.climate.SwitcherType2Api.control_breeze_device", + ) as mock_control_device: + with pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_TARGET_TEMP_LOW: 20, + ATTR_TARGET_TEMP_HIGH: 30, + }, + blocking=True, + ) + + assert mock_api.call_count == 2 + mock_control_device.assert_not_called() + + +@pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) +async def test_climate_fan_level(hass, mock_bridge, mock_api, monkeypatch): + """Test climate fan level service.""" + await init_integration(hass) + assert mock_bridge + + # Test initial fan level - low + state = hass.states.get(ENTITY_ID) + assert state.attributes["fan_mode"] == "low" + + # Test set fan level to high + with patch( + "homeassistant.components.switcher_kis.climate.SwitcherType2Api.control_breeze_device", + ) as mock_control_device: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_MODE: "high"}, + blocking=True, + ) + + monkeypatch.setattr(DEVICE, "fan_level", ThermostatFanLevel.HIGH) + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + + assert mock_api.call_count == 2 + mock_control_device.assert_called_once() + state = hass.states.get(ENTITY_ID) + assert state.attributes["fan_mode"] == "high" + + +@pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) +async def test_climate_swing(hass, mock_bridge, mock_api, monkeypatch): + """Test climate swing service.""" + await init_integration(hass) + assert mock_bridge + + # Test initial swing mode + state = hass.states.get(ENTITY_ID) + assert state.attributes["swing_mode"] == "off" + + # Test set swing mode on + with patch( + "homeassistant.components.switcher_kis.climate.SwitcherType2Api.control_breeze_device", + ) as mock_control_device: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_MODE, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_SWING_MODE: "vertical", + }, + blocking=True, + ) + + monkeypatch.setattr(DEVICE, "swing", ThermostatSwing.ON) + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + + assert mock_api.call_count == 2 + mock_control_device.assert_called_once() + state = hass.states.get(ENTITY_ID) + assert state.attributes["swing_mode"] == "vertical" + + # Test set swing mode off + with patch( + "homeassistant.components.switcher_kis.climate.SwitcherType2Api.control_breeze_device", + ) as mock_control_device: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_SWING_MODE: "off"}, + blocking=True, + ) + + monkeypatch.setattr(DEVICE, "swing", ThermostatSwing.OFF) + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + + assert mock_api.call_count == 4 + mock_control_device.assert_called_once() + state = hass.states.get(ENTITY_ID) + assert state.attributes["swing_mode"] == "off" + + +@pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) +async def test_control_device_fail(hass, mock_bridge, mock_api, monkeypatch): + """Test control device fail.""" + await init_integration(hass) + assert mock_bridge + + # Test initial hvac mode - cool + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.COOL + + # Test exception during set hvac mode + with patch( + "homeassistant.components.switcher_kis.climate.SwitcherType2Api.control_breeze_device", + side_effect=RuntimeError("fake error"), + ) as mock_control_device: + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + + assert mock_api.call_count == 2 + mock_control_device.assert_called_once() + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_UNAVAILABLE + + # Make device available again + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.COOL + + # Test error response during turn on + with patch( + "homeassistant.components.switcher_kis.climate.SwitcherType2Api.control_breeze_device", + return_value=SwitcherBaseResponse(None), + ) as mock_control_device: + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + + assert mock_api.call_count == 4 + mock_control_device.assert_called_once() + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) +async def test_bad_update_discard(hass, mock_bridge, mock_api, monkeypatch): + """Test that a bad update from device is discarded.""" + await init_integration(hass) + assert mock_bridge + + # Test initial hvac mode - cool + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.COOL + + # Device send target temperature with 0 to indicate it doesn't have data + monkeypatch.setattr(DEVICE, "target_temperature", 0) + monkeypatch.setattr(DEVICE, "mode", ThermostatMode.HEAT) + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + + # Validate state did not change + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.COOL + + +@pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) +async def test_climate_control_errors(hass, mock_bridge, mock_api, monkeypatch): + """Test control with settings not supported by device.""" + await init_integration(hass) + assert mock_bridge + + # Dry mode does not support setting fan, temperature, swing + monkeypatch.setattr(DEVICE, "mode", ThermostatMode.DRY) + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + + # Test exception when trying set temperature + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 24}, + blocking=True, + ) + + # Test exception when trying set fan level + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_MODE: "high"}, + blocking=True, + ) + + # Test exception when trying set swing mode + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_SWING_MODE: "off"}, + blocking=True, + ) From 82af726e21de5378b9e67d1a9e86da64670dbeb9 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sat, 1 Oct 2022 09:28:15 -0700 Subject: [PATCH 074/985] Fix onvif snapshot fallback (#79394) Co-authored-by: Franck Nijhof --- homeassistant/components/onvif/camera.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 6c76f98a8da..9a8535f2599 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -142,16 +142,21 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): if self.device.capabilities.snapshot: try: - image = await self.device.device.get_snapshot( + if image := await self.device.device.get_snapshot( self.profile.token, self._basic_auth - ) - return image + ): + return image except ONVIFError as err: LOGGER.error( "Fetch snapshot image failed from %s, falling back to FFmpeg; %s", self.device.name, err, ) + else: + LOGGER.error( + "Fetch snapshot image failed from %s, falling back to FFmpeg", + self.device.name, + ) assert self._stream_uri return await ffmpeg.async_get_image( From e7724a6593cca35940f6f6298bb34e90c98ac1f6 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sat, 1 Oct 2022 18:33:41 +0200 Subject: [PATCH 075/985] Fix low speed cover in Overkiz integration (#79416) Fix low speed cover --- .../components/overkiz/cover_entities/vertical_cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/overkiz/cover_entities/vertical_cover.py b/homeassistant/components/overkiz/cover_entities/vertical_cover.py index f76f3849e83..90ac6428960 100644 --- a/homeassistant/components/overkiz/cover_entities/vertical_cover.py +++ b/homeassistant/components/overkiz/cover_entities/vertical_cover.py @@ -152,7 +152,7 @@ class LowSpeedCover(VerticalCover): ) -> None: """Initialize the device.""" super().__init__(device_url, coordinator) - self._attr_name = f"{self._attr_name} Low Speed" + self._attr_name = "Low speed" self._attr_unique_id = f"{self._attr_unique_id}_low_speed" async def async_set_cover_position(self, **kwargs: Any) -> None: From 4cfcf562b58b1d60075661ed78ec3cec7b3d1d96 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 1 Oct 2022 19:34:47 +0300 Subject: [PATCH 076/985] Bump aioswitcher to 3.0.3 (#79419) --- homeassistant/components/switcher_kis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index 508d6a897af..d498c5f165e 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -3,7 +3,7 @@ "name": "Switcher", "documentation": "https://www.home-assistant.io/integrations/switcher_kis/", "codeowners": ["@tomerfi", "@thecode"], - "requirements": ["aioswitcher==3.0.2"], + "requirements": ["aioswitcher==3.0.3"], "quality_scale": "platinum", "iot_class": "local_push", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 20f0190aee5..04e4320779b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -267,7 +267,7 @@ aioslimproto==2.1.1 aiosteamist==0.3.2 # homeassistant.components.switcher_kis -aioswitcher==3.0.2 +aioswitcher==3.0.3 # homeassistant.components.syncthing aiosyncthing==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c47a14ece92..1dc842fa02f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -242,7 +242,7 @@ aioslimproto==2.1.1 aiosteamist==0.3.2 # homeassistant.components.switcher_kis -aioswitcher==3.0.2 +aioswitcher==3.0.3 # homeassistant.components.syncthing aiosyncthing==0.5.1 From 2de273500e62f785cc8844f9922a012d57f6a57f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 1 Oct 2022 18:55:00 +0200 Subject: [PATCH 077/985] Remove state_unit_of_measurement from metadata DB table (#79370) * Remove state_unit_of_measurement from metadata DB table * Adjust test --- homeassistant/components/demo/__init__.py | 5 - .../components/recorder/db_schema.py | 1 - .../components/recorder/migration.py | 21 +- homeassistant/components/recorder/models.py | 1 - .../components/recorder/statistics.py | 36 +- .../components/recorder/websocket_api.py | 1 - homeassistant/components/sensor/recorder.py | 3 - homeassistant/components/tibber/sensor.py | 1 - tests/components/demo/test_init.py | 3 - tests/components/energy/test_websocket_api.py | 9 - tests/components/recorder/db_schema_29.py | 616 ------------------ tests/components/recorder/test_migrate.py | 115 +--- tests/components/recorder/test_statistics.py | 63 +- .../components/recorder/test_websocket_api.py | 21 - tests/components/sensor/test_recorder.py | 37 -- 15 files changed, 25 insertions(+), 908 deletions(-) delete mode 100644 tests/components/recorder/db_schema_29.py diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 4d0ef03c564..7ed989903e5 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -295,7 +295,6 @@ async def _insert_statistics(hass: HomeAssistant) -> None: metadata: StatisticMetaData = { "source": DOMAIN, "name": "Outdoor temperature", - "state_unit_of_measurement": TEMP_CELSIUS, "statistic_id": f"{DOMAIN}:temperature_outdoor", "unit_of_measurement": TEMP_CELSIUS, "has_mean": True, @@ -309,7 +308,6 @@ async def _insert_statistics(hass: HomeAssistant) -> None: metadata = { "source": DOMAIN, "name": "Energy consumption 1", - "state_unit_of_measurement": ENERGY_KILO_WATT_HOUR, "statistic_id": f"{DOMAIN}:energy_consumption_kwh", "unit_of_measurement": ENERGY_KILO_WATT_HOUR, "has_mean": False, @@ -322,7 +320,6 @@ async def _insert_statistics(hass: HomeAssistant) -> None: metadata = { "source": DOMAIN, "name": "Energy consumption 2", - "state_unit_of_measurement": ENERGY_MEGA_WATT_HOUR, "statistic_id": f"{DOMAIN}:energy_consumption_mwh", "unit_of_measurement": ENERGY_MEGA_WATT_HOUR, "has_mean": False, @@ -337,7 +334,6 @@ async def _insert_statistics(hass: HomeAssistant) -> None: metadata = { "source": DOMAIN, "name": "Gas consumption 1", - "state_unit_of_measurement": VOLUME_CUBIC_METERS, "statistic_id": f"{DOMAIN}:gas_consumption_m3", "unit_of_measurement": VOLUME_CUBIC_METERS, "has_mean": False, @@ -352,7 +348,6 @@ async def _insert_statistics(hass: HomeAssistant) -> None: metadata = { "source": DOMAIN, "name": "Gas consumption 2", - "state_unit_of_measurement": VOLUME_CUBIC_FEET, "statistic_id": f"{DOMAIN}:gas_consumption_ft3", "unit_of_measurement": VOLUME_CUBIC_FEET, "has_mean": False, diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 363604d525b..d76f89068d0 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -494,7 +494,6 @@ class StatisticsMeta(Base): # type: ignore[misc,valid-type] id = Column(Integer, Identity(), primary_key=True) statistic_id = Column(String(255), index=True, unique=True) source = Column(String(32)) - state_unit_of_measurement = Column(String(255)) unit_of_measurement = Column(String(255)) has_mean = Column(Boolean) has_sum = Column(Boolean) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index e2169727382..f82ec7ba1eb 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -748,24 +748,9 @@ def _apply_update( # noqa: C901 session_maker, "statistics_meta", "ix_statistics_meta_statistic_id" ) elif new_version == 30: - _add_columns( - session_maker, - "statistics_meta", - ["state_unit_of_measurement VARCHAR(255)"], - ) - # When querying the database, be careful to only explicitly query for columns - # which were present in schema version 30. If querying the table, SQLAlchemy - # will refer to future columns. - with session_scope(session=session_maker()) as session: - for statistics_meta in session.query( - StatisticsMeta.id, StatisticsMeta.unit_of_measurement - ): - session.query(StatisticsMeta).filter_by(id=statistics_meta.id).update( - { - StatisticsMeta.state_unit_of_measurement: statistics_meta.unit_of_measurement, - }, - synchronize_session=False, - ) + # This added a column to the statistics_meta table, removed again before + # release of HA Core 2022.10.0 + pass else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 4f6d8a990a6..cfc797cf7ea 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -64,7 +64,6 @@ class StatisticMetaData(TypedDict): has_sum: bool name: str | None source: str - state_unit_of_measurement: str | None statistic_id: str unit_of_measurement: str | None diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index a0ff73b10fd..ef066b82060 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -24,6 +24,7 @@ from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import Subquery import voluptuous as vol +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import Event, HomeAssistant, callback, valid_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry @@ -116,7 +117,6 @@ QUERY_STATISTIC_META = [ StatisticsMeta.id, StatisticsMeta.statistic_id, StatisticsMeta.source, - StatisticsMeta.state_unit_of_measurement, StatisticsMeta.unit_of_measurement, StatisticsMeta.has_mean, StatisticsMeta.has_sum, @@ -342,8 +342,6 @@ def _update_or_add_metadata( old_metadata["has_mean"] != new_metadata["has_mean"] or old_metadata["has_sum"] != new_metadata["has_sum"] or old_metadata["name"] != new_metadata["name"] - or old_metadata["state_unit_of_measurement"] - != new_metadata["state_unit_of_measurement"] or old_metadata["unit_of_measurement"] != new_metadata["unit_of_measurement"] ): session.query(StatisticsMeta).filter_by(statistic_id=statistic_id).update( @@ -351,9 +349,6 @@ def _update_or_add_metadata( StatisticsMeta.has_mean: new_metadata["has_mean"], StatisticsMeta.has_sum: new_metadata["has_sum"], StatisticsMeta.name: new_metadata["name"], - StatisticsMeta.state_unit_of_measurement: new_metadata[ - "state_unit_of_measurement" - ], StatisticsMeta.unit_of_measurement: new_metadata["unit_of_measurement"], }, synchronize_session=False, @@ -820,7 +815,6 @@ def get_metadata_with_session( "has_sum": meta["has_sum"], "name": meta["name"], "source": meta["source"], - "state_unit_of_measurement": meta["state_unit_of_measurement"], "statistic_id": meta["statistic_id"], "unit_of_measurement": meta["unit_of_measurement"], }, @@ -899,7 +893,6 @@ def list_statistic_ids( result = { meta["statistic_id"]: { - "state_unit_of_measurement": meta["state_unit_of_measurement"], "has_mean": meta["has_mean"], "has_sum": meta["has_sum"], "name": meta["name"], @@ -926,7 +919,6 @@ def list_statistic_ids( "has_sum": meta["has_sum"], "name": meta["name"], "source": meta["source"], - "state_unit_of_measurement": meta["state_unit_of_measurement"], "unit_class": _get_unit_class(meta["unit_of_measurement"]), "unit_of_measurement": meta["unit_of_measurement"], } @@ -939,7 +931,6 @@ def list_statistic_ids( "has_sum": info["has_sum"], "name": info.get("name"), "source": info["source"], - "state_unit_of_measurement": info["state_unit_of_measurement"], "statistics_unit_of_measurement": info["unit_of_measurement"], "unit_class": info["unit_class"], } @@ -1386,9 +1377,10 @@ def _sorted_statistics_to_dict( # Append all statistic entries, and optionally do unit conversion for meta_id, group in groupby(stats, lambda stat: stat.metadata_id): # type: ignore[no-any-return] - unit = metadata[meta_id]["unit_of_measurement"] - state_unit = metadata[meta_id]["state_unit_of_measurement"] + state_unit = unit = metadata[meta_id]["unit_of_measurement"] statistic_id = metadata[meta_id]["statistic_id"] + if state := hass.states.get(statistic_id): + state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if unit is not None and convert_units: convert = _get_statistic_to_display_unit_converter(unit, state_unit, units) else: @@ -1470,18 +1462,6 @@ def _async_import_statistics( get_instance(hass).async_import_statistics(metadata, statistics) -def _validate_units(statistics_unit: str | None, state_unit: str | None) -> None: - """Raise if the statistics unit and state unit are not compatible.""" - if statistics_unit == state_unit: - return - if ( - unit_converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistics_unit) - ) is None: - raise HomeAssistantError(f"Invalid units {statistics_unit},{state_unit}") - if state_unit not in unit_converter.VALID_UNITS: - raise HomeAssistantError(f"Invalid units {statistics_unit},{state_unit}") - - @callback def async_import_statistics( hass: HomeAssistant, @@ -1499,10 +1479,6 @@ def async_import_statistics( if not metadata["source"] or metadata["source"] != DOMAIN: raise HomeAssistantError("Invalid source") - _validate_units( - metadata["unit_of_measurement"], metadata["state_unit_of_measurement"] - ) - _async_import_statistics(hass, metadata, statistics) @@ -1525,10 +1501,6 @@ def async_add_external_statistics( if not metadata["source"] or metadata["source"] != domain: raise HomeAssistantError("Invalid source") - _validate_units( - metadata["unit_of_measurement"], metadata["state_unit_of_measurement"] - ) - _async_import_statistics(hass, metadata, statistics) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 02b7519486d..c583577ec8f 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -376,7 +376,6 @@ def ws_import_statistics( """Import statistics.""" metadata = msg["metadata"] stats = msg["stats"] - metadata["state_unit_of_measurement"] = metadata["unit_of_measurement"] if valid_entity_id(metadata["statistic_id"]): async_import_statistics(hass, metadata, stats) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 5241b123185..4380efbd2c3 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -480,7 +480,6 @@ def _compile_statistics( # noqa: C901 "has_sum": "sum" in wanted_statistics[entity_id], "name": None, "source": RECORDER_DOMAIN, - "state_unit_of_measurement": state_unit, "statistic_id": entity_id, "unit_of_measurement": normalized_unit, } @@ -621,7 +620,6 @@ def list_statistic_ids( "has_sum": "sum" in provided_statistics, "name": None, "source": RECORDER_DOMAIN, - "state_unit_of_measurement": state_unit, "statistic_id": state.entity_id, "unit_of_measurement": state_unit, } @@ -637,7 +635,6 @@ def list_statistic_ids( "has_sum": "sum" in provided_statistics, "name": None, "source": RECORDER_DOMAIN, - "state_unit_of_measurement": state_unit, "statistic_id": state.entity_id, "unit_of_measurement": statistics_unit, } diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 93fdba107ed..ca0c253590f 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -642,7 +642,6 @@ class TibberDataCoordinator(DataUpdateCoordinator): has_sum=True, name=f"{home.name} {sensor_type}", source=TIBBER_DOMAIN, - state_unit_of_measurement=unit, statistic_id=statistic_id, unit_of_measurement=unit, ) diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 8c3adeb1c98..f7a89fe63c1 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -67,7 +67,6 @@ async def test_demo_statistics(hass, recorder_mock): "has_sum": False, "name": "Outdoor temperature", "source": "demo", - "state_unit_of_measurement": "°C", "statistic_id": "demo:temperature_outdoor", "statistics_unit_of_measurement": "°C", "unit_class": "temperature", @@ -77,7 +76,6 @@ async def test_demo_statistics(hass, recorder_mock): "has_sum": True, "name": "Energy consumption 1", "source": "demo", - "state_unit_of_measurement": "kWh", "statistic_id": "demo:energy_consumption_kwh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", @@ -96,7 +94,6 @@ async def test_demo_statistics_growth(hass, recorder_mock): metadata = { "source": DOMAIN, "name": "Energy consumption 1", - "state_unit_of_measurement": "m³", "statistic_id": statistic_id, "unit_of_measurement": "m³", "has_mean": False, diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index cbe0e47124f..ab785291f91 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -336,7 +336,6 @@ async def test_fossil_energy_consumption_no_co2(hass, hass_ws_client, recorder_m "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_1", "unit_of_measurement": "kWh", } @@ -371,7 +370,6 @@ async def test_fossil_energy_consumption_no_co2(hass, hass_ws_client, recorder_m "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_2", "unit_of_measurement": "kWh", } @@ -499,7 +497,6 @@ async def test_fossil_energy_consumption_hole(hass, hass_ws_client, recorder_moc "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_1", "unit_of_measurement": "kWh", } @@ -534,7 +531,6 @@ async def test_fossil_energy_consumption_hole(hass, hass_ws_client, recorder_moc "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_2", "unit_of_measurement": "kWh", } @@ -660,7 +656,6 @@ async def test_fossil_energy_consumption_no_data(hass, hass_ws_client, recorder_ "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_1", "unit_of_measurement": "kWh", } @@ -695,7 +690,6 @@ async def test_fossil_energy_consumption_no_data(hass, hass_ws_client, recorder_ "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_2", "unit_of_measurement": "kWh", } @@ -812,7 +806,6 @@ async def test_fossil_energy_consumption(hass, hass_ws_client, recorder_mock): "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_1", "unit_of_measurement": "kWh", } @@ -847,7 +840,6 @@ async def test_fossil_energy_consumption(hass, hass_ws_client, recorder_mock): "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_2", "unit_of_measurement": "kWh", } @@ -878,7 +870,6 @@ async def test_fossil_energy_consumption(hass, hass_ws_client, recorder_mock): "has_sum": False, "name": "Fossil percentage", "source": "test", - "state_unit_of_measurement": "%", "statistic_id": "test:fossil_percentage", "unit_of_measurement": "%", } diff --git a/tests/components/recorder/db_schema_29.py b/tests/components/recorder/db_schema_29.py deleted file mode 100644 index 54aa4b2b13c..00000000000 --- a/tests/components/recorder/db_schema_29.py +++ /dev/null @@ -1,616 +0,0 @@ -"""Models for SQLAlchemy. - -This file contains the model definitions for schema version 28. -It is used to test the schema migration logic. -""" -from __future__ import annotations - -from collections.abc import Callable -from datetime import datetime, timedelta -import logging -from typing import Any, TypeVar, cast - -import ciso8601 -from fnvhash import fnv1a_32 -from sqlalchemy import ( - JSON, - BigInteger, - Boolean, - Column, - DateTime, - Float, - ForeignKey, - Identity, - Index, - Integer, - SmallInteger, - String, - Text, - distinct, - type_coerce, -) -from sqlalchemy.dialects import mysql, oracle, postgresql, sqlite -from sqlalchemy.ext.declarative import declared_attr -from sqlalchemy.orm import aliased, declarative_base, relationship -from sqlalchemy.orm.session import Session - -from homeassistant.components.recorder.const import ALL_DOMAIN_EXCLUDE_ATTRS -from homeassistant.components.recorder.models import ( - StatisticData, - StatisticMetaData, - process_timestamp, -) -from homeassistant.const import ( - MAX_LENGTH_EVENT_CONTEXT_ID, - MAX_LENGTH_EVENT_EVENT_TYPE, - MAX_LENGTH_EVENT_ORIGIN, - MAX_LENGTH_STATE_ENTITY_ID, - MAX_LENGTH_STATE_STATE, -) -from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id -from homeassistant.helpers.json import ( - JSON_DECODE_EXCEPTIONS, - JSON_DUMP, - json_bytes, - json_loads, -) -import homeassistant.util.dt as dt_util - -# SQLAlchemy Schema -# pylint: disable=invalid-name -Base = declarative_base() - -SCHEMA_VERSION = 29 - -_StatisticsBaseSelfT = TypeVar("_StatisticsBaseSelfT", bound="StatisticsBase") - -_LOGGER = logging.getLogger(__name__) - -TABLE_EVENTS = "events" -TABLE_EVENT_DATA = "event_data" -TABLE_STATES = "states" -TABLE_STATE_ATTRIBUTES = "state_attributes" -TABLE_RECORDER_RUNS = "recorder_runs" -TABLE_SCHEMA_CHANGES = "schema_changes" -TABLE_STATISTICS = "statistics" -TABLE_STATISTICS_META = "statistics_meta" -TABLE_STATISTICS_RUNS = "statistics_runs" -TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" - -ALL_TABLES = [ - TABLE_STATES, - TABLE_STATE_ATTRIBUTES, - TABLE_EVENTS, - TABLE_EVENT_DATA, - TABLE_RECORDER_RUNS, - TABLE_SCHEMA_CHANGES, - TABLE_STATISTICS, - TABLE_STATISTICS_META, - TABLE_STATISTICS_RUNS, - TABLE_STATISTICS_SHORT_TERM, -] - -TABLES_TO_CHECK = [ - TABLE_STATES, - TABLE_EVENTS, - TABLE_RECORDER_RUNS, - TABLE_SCHEMA_CHANGES, -] - -LAST_UPDATED_INDEX = "ix_states_last_updated" -ENTITY_ID_LAST_UPDATED_INDEX = "ix_states_entity_id_last_updated" -EVENTS_CONTEXT_ID_INDEX = "ix_events_context_id" -STATES_CONTEXT_ID_INDEX = "ix_states_context_id" - - -class FAST_PYSQLITE_DATETIME(sqlite.DATETIME): # type: ignore[misc] - """Use ciso8601 to parse datetimes instead of sqlalchemy built-in regex.""" - - def result_processor(self, dialect, coltype): # type: ignore[no-untyped-def] - """Offload the datetime parsing to ciso8601.""" - return lambda value: None if value is None else ciso8601.parse_datetime(value) - - -JSON_VARIENT_CAST = Text().with_variant( - postgresql.JSON(none_as_null=True), "postgresql" -) -JSONB_VARIENT_CAST = Text().with_variant( - postgresql.JSONB(none_as_null=True), "postgresql" -) -DATETIME_TYPE = ( - DateTime(timezone=True) - .with_variant(mysql.DATETIME(timezone=True, fsp=6), "mysql") - .with_variant(FAST_PYSQLITE_DATETIME(), "sqlite") -) -DOUBLE_TYPE = ( - Float() - .with_variant(mysql.DOUBLE(asdecimal=False), "mysql") - .with_variant(oracle.DOUBLE_PRECISION(), "oracle") - .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") -) - - -class JSONLiteral(JSON): # type: ignore[misc] - """Teach SA how to literalize json.""" - - def literal_processor(self, dialect: str) -> Callable[[Any], str]: - """Processor to convert a value to JSON.""" - - def process(value: Any) -> str: - """Dump json.""" - return JSON_DUMP(value) - - return process - - -EVENT_ORIGIN_ORDER = [EventOrigin.local, EventOrigin.remote] -EVENT_ORIGIN_TO_IDX = {origin: idx for idx, origin in enumerate(EVENT_ORIGIN_ORDER)} - - -class Events(Base): # type: ignore[misc,valid-type] - """Event history data.""" - - __table_args__ = ( - # Used for fetching events at a specific time - # see logbook - Index("ix_events_event_type_time_fired", "event_type", "time_fired"), - {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, - ) - __tablename__ = TABLE_EVENTS - event_id = Column(Integer, Identity(), primary_key=True) - event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) - event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) - origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) # no longer used for new rows - origin_idx = Column(SmallInteger) - time_fired = Column(DATETIME_TYPE, index=True) - context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) - context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) - context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) - data_id = Column(Integer, ForeignKey("event_data.data_id"), index=True) - event_data_rel = relationship("EventData") - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - @staticmethod - def from_event(event: Event) -> Events: - """Create an event database object from a native event.""" - return Events( - event_type=event.event_type, - event_data=None, - origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), - time_fired=event.time_fired, - context_id=event.context.id, - context_user_id=event.context.user_id, - context_parent_id=event.context.parent_id, - ) - - def to_native(self, validate_entity_id: bool = True) -> Event | None: - """Convert to a native HA Event.""" - context = Context( - id=self.context_id, - user_id=self.context_user_id, - parent_id=self.context_parent_id, - ) - try: - return Event( - self.event_type, - json_loads(self.event_data) if self.event_data else {}, - EventOrigin(self.origin) - if self.origin - else EVENT_ORIGIN_ORDER[self.origin_idx], - process_timestamp(self.time_fired), - context=context, - ) - except JSON_DECODE_EXCEPTIONS: - # When json_loads fails - _LOGGER.exception("Error converting to event: %s", self) - return None - - -class EventData(Base): # type: ignore[misc,valid-type] - """Event data history.""" - - __table_args__ = ( - {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, - ) - __tablename__ = TABLE_EVENT_DATA - data_id = Column(Integer, Identity(), primary_key=True) - hash = Column(BigInteger, index=True) - # Note that this is not named attributes to avoid confusion with the states table - shared_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - @staticmethod - def from_event(event: Event) -> EventData: - """Create object from an event.""" - shared_data = json_bytes(event.data) - return EventData( - shared_data=shared_data.decode("utf-8"), - hash=EventData.hash_shared_data_bytes(shared_data), - ) - - @staticmethod - def shared_data_bytes_from_event(event: Event) -> bytes: - """Create shared_data from an event.""" - return json_bytes(event.data) - - @staticmethod - def hash_shared_data_bytes(shared_data_bytes: bytes) -> int: - """Return the hash of json encoded shared data.""" - return cast(int, fnv1a_32(shared_data_bytes)) - - def to_native(self) -> dict[str, Any]: - """Convert to an HA state object.""" - try: - return cast(dict[str, Any], json_loads(self.shared_data)) - except JSON_DECODE_EXCEPTIONS: - _LOGGER.exception("Error converting row to event data: %s", self) - return {} - - -class States(Base): # type: ignore[misc,valid-type] - """State change history.""" - - __table_args__ = ( - # Used for fetching the state of entities at a specific time - # (get_states in history.py) - Index(ENTITY_ID_LAST_UPDATED_INDEX, "entity_id", "last_updated"), - {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, - ) - __tablename__ = TABLE_STATES - state_id = Column(Integer, Identity(), primary_key=True) - entity_id = Column(String(MAX_LENGTH_STATE_ENTITY_ID)) - state = Column(String(MAX_LENGTH_STATE_STATE)) - attributes = Column( - Text().with_variant(mysql.LONGTEXT, "mysql") - ) # no longer used for new rows - event_id = Column( # no longer used for new rows - Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True - ) - last_changed = Column(DATETIME_TYPE) - last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) - old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) - attributes_id = Column( - Integer, ForeignKey("state_attributes.attributes_id"), index=True - ) - context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) - context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) - context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) - origin_idx = Column(SmallInteger) # 0 is local, 1 is remote - old_state = relationship("States", remote_side=[state_id]) - state_attributes = relationship("StateAttributes") - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - @staticmethod - def from_event(event: Event) -> States: - """Create object from a state_changed event.""" - entity_id = event.data["entity_id"] - state: State | None = event.data.get("new_state") - dbstate = States( - entity_id=entity_id, - attributes=None, - context_id=event.context.id, - context_user_id=event.context.user_id, - context_parent_id=event.context.parent_id, - origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), - ) - - # None state means the state was removed from the state machine - if state is None: - dbstate.state = "" - dbstate.last_updated = event.time_fired - dbstate.last_changed = None - return dbstate - - dbstate.state = state.state - dbstate.last_updated = state.last_updated - if state.last_updated == state.last_changed: - dbstate.last_changed = None - else: - dbstate.last_changed = state.last_changed - - return dbstate - - def to_native(self, validate_entity_id: bool = True) -> State | None: - """Convert to an HA state object.""" - context = Context( - id=self.context_id, - user_id=self.context_user_id, - parent_id=self.context_parent_id, - ) - try: - attrs = json_loads(self.attributes) if self.attributes else {} - except JSON_DECODE_EXCEPTIONS: - # When json_loads fails - _LOGGER.exception("Error converting row to state: %s", self) - return None - if self.last_changed is None or self.last_changed == self.last_updated: - last_changed = last_updated = process_timestamp(self.last_updated) - else: - last_updated = process_timestamp(self.last_updated) - last_changed = process_timestamp(self.last_changed) - return State( - self.entity_id, - self.state, - # Join the state_attributes table on attributes_id to get the attributes - # for newer states - attrs, - last_changed, - last_updated, - context=context, - validate_entity_id=validate_entity_id, - ) - - -class StateAttributes(Base): # type: ignore[misc,valid-type] - """State attribute change history.""" - - __table_args__ = ( - {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, - ) - __tablename__ = TABLE_STATE_ATTRIBUTES - attributes_id = Column(Integer, Identity(), primary_key=True) - hash = Column(BigInteger, index=True) - # Note that this is not named attributes to avoid confusion with the states table - shared_attrs = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - @staticmethod - def from_event(event: Event) -> StateAttributes: - """Create object from a state_changed event.""" - state: State | None = event.data.get("new_state") - # None state means the state was removed from the state machine - attr_bytes = b"{}" if state is None else json_bytes(state.attributes) - dbstate = StateAttributes(shared_attrs=attr_bytes.decode("utf-8")) - dbstate.hash = StateAttributes.hash_shared_attrs_bytes(attr_bytes) - return dbstate - - @staticmethod - def shared_attrs_bytes_from_event( - event: Event, exclude_attrs_by_domain: dict[str, set[str]] - ) -> bytes: - """Create shared_attrs from a state_changed event.""" - state: State | None = event.data.get("new_state") - # None state means the state was removed from the state machine - if state is None: - return b"{}" - domain = split_entity_id(state.entity_id)[0] - exclude_attrs = ( - exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS - ) - return json_bytes( - {k: v for k, v in state.attributes.items() if k not in exclude_attrs} - ) - - @staticmethod - def hash_shared_attrs_bytes(shared_attrs_bytes: bytes) -> int: - """Return the hash of json encoded shared attributes.""" - return cast(int, fnv1a_32(shared_attrs_bytes)) - - def to_native(self) -> dict[str, Any]: - """Convert to an HA state object.""" - try: - return cast(dict[str, Any], json_loads(self.shared_attrs)) - except JSON_DECODE_EXCEPTIONS: - # When json_loads fails - _LOGGER.exception("Error converting row to state attributes: %s", self) - return {} - - -class StatisticsBase: - """Statistics base class.""" - - id = Column(Integer, Identity(), primary_key=True) - created = Column(DATETIME_TYPE, default=dt_util.utcnow) - - @declared_attr # type: ignore[misc] - def metadata_id(self) -> Column: - """Define the metadata_id column for sub classes.""" - return Column( - Integer, - ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), - index=True, - ) - - start = Column(DATETIME_TYPE, index=True) - mean = Column(DOUBLE_TYPE) - min = Column(DOUBLE_TYPE) - max = Column(DOUBLE_TYPE) - last_reset = Column(DATETIME_TYPE) - state = Column(DOUBLE_TYPE) - sum = Column(DOUBLE_TYPE) - - @classmethod - def from_stats( - cls: type[_StatisticsBaseSelfT], metadata_id: int, stats: StatisticData - ) -> _StatisticsBaseSelfT: - """Create object from a statistics.""" - return cls( # type: ignore[call-arg,misc] - metadata_id=metadata_id, - **stats, - ) - - -class Statistics(Base, StatisticsBase): # type: ignore[misc,valid-type] - """Long term statistics.""" - - duration = timedelta(hours=1) - - __table_args__ = ( - # Used for fetching statistics for a certain entity at a specific time - Index("ix_statistics_statistic_id_start", "metadata_id", "start", unique=True), - ) - __tablename__ = TABLE_STATISTICS - - -class StatisticsShortTerm(Base, StatisticsBase): # type: ignore[misc,valid-type] - """Short term statistics.""" - - duration = timedelta(minutes=5) - - __table_args__ = ( - # Used for fetching statistics for a certain entity at a specific time - Index( - "ix_statistics_short_term_statistic_id_start", - "metadata_id", - "start", - unique=True, - ), - ) - __tablename__ = TABLE_STATISTICS_SHORT_TERM - - -class StatisticsMeta(Base): # type: ignore[misc,valid-type] - """Statistics meta data.""" - - __table_args__ = ( - {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, - ) - __tablename__ = TABLE_STATISTICS_META - id = Column(Integer, Identity(), primary_key=True) - statistic_id = Column(String(255), index=True, unique=True) - source = Column(String(32)) - unit_of_measurement = Column(String(255)) - has_mean = Column(Boolean) - has_sum = Column(Boolean) - name = Column(String(255)) - - @staticmethod - def from_meta(meta: StatisticMetaData) -> StatisticsMeta: - """Create object from meta data.""" - return StatisticsMeta(**meta) - - -class RecorderRuns(Base): # type: ignore[misc,valid-type] - """Representation of recorder run.""" - - __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) - __tablename__ = TABLE_RECORDER_RUNS - run_id = Column(Integer, Identity(), primary_key=True) - start = Column(DateTime(timezone=True), default=dt_util.utcnow) - end = Column(DateTime(timezone=True)) - closed_incorrect = Column(Boolean, default=False) - created = Column(DateTime(timezone=True), default=dt_util.utcnow) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - end = ( - f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None - ) - return ( - f"" - ) - - def entity_ids(self, point_in_time: datetime | None = None) -> list[str]: - """Return the entity ids that existed in this run. - - Specify point_in_time if you want to know which existed at that point - in time inside the run. - """ - session = Session.object_session(self) - - assert session is not None, "RecorderRuns need to be persisted" - - query = session.query(distinct(States.entity_id)).filter( - States.last_updated >= self.start - ) - - if point_in_time is not None: - query = query.filter(States.last_updated < point_in_time) - elif self.end is not None: - query = query.filter(States.last_updated < self.end) - - return [row[0] for row in query] - - def to_native(self, validate_entity_id: bool = True) -> RecorderRuns: - """Return self, native format is this model.""" - return self - - -class SchemaChanges(Base): # type: ignore[misc,valid-type] - """Representation of schema version changes.""" - - __tablename__ = TABLE_SCHEMA_CHANGES - change_id = Column(Integer, Identity(), primary_key=True) - schema_version = Column(Integer) - changed = Column(DateTime(timezone=True), default=dt_util.utcnow) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - -class StatisticsRuns(Base): # type: ignore[misc,valid-type] - """Representation of statistics run.""" - - __tablename__ = TABLE_STATISTICS_RUNS - run_id = Column(Integer, Identity(), primary_key=True) - start = Column(DateTime(timezone=True), index=True) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - -EVENT_DATA_JSON = type_coerce( - EventData.shared_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) -) -OLD_FORMAT_EVENT_DATA_JSON = type_coerce( - Events.event_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) -) - -SHARED_ATTRS_JSON = type_coerce( - StateAttributes.shared_attrs.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) -) -OLD_FORMAT_ATTRS_JSON = type_coerce( - States.attributes.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) -) - -ENTITY_ID_IN_EVENT: Column = EVENT_DATA_JSON["entity_id"] -OLD_ENTITY_ID_IN_EVENT: Column = OLD_FORMAT_EVENT_DATA_JSON["entity_id"] -DEVICE_ID_IN_EVENT: Column = EVENT_DATA_JSON["device_id"] -OLD_STATE = aliased(States, name="old_state") diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index cbba4dab26b..9e0609de5b6 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -21,21 +21,18 @@ from sqlalchemy.pool import StaticPool from homeassistant.bootstrap import async_setup_component from homeassistant.components import persistent_notification as pn, recorder from homeassistant.components.recorder import db_schema, migration -from homeassistant.components.recorder.const import SQLITE_URL_PREFIX from homeassistant.components.recorder.db_schema import ( SCHEMA_VERSION, RecorderRuns, States, ) -from homeassistant.components.recorder.statistics import get_start_time from homeassistant.components.recorder.util import session_scope from homeassistant.helpers import recorder as recorder_helper -from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util -from .common import async_wait_recording_done, create_engine_test, wait_recording_done +from .common import async_wait_recording_done, create_engine_test -from tests.common import async_fire_time_changed, get_test_home_assistant +from tests.common import async_fire_time_changed ORIG_TZ = dt_util.DEFAULT_TIME_ZONE @@ -363,114 +360,6 @@ async def test_schema_migrate(hass, start_version, live): assert recorder.util.async_migration_in_progress(hass) is not True -def test_set_state_unit(caplog, tmpdir): - """Test state unit column is initialized.""" - - def _create_engine_29(*args, **kwargs): - """Test version of create_engine that initializes with old schema. - - This simulates an existing db with the old schema. - """ - module = "tests.components.recorder.db_schema_29" - importlib.import_module(module) - old_db_schema = sys.modules[module] - engine = create_engine(*args, **kwargs) - old_db_schema.Base.metadata.create_all(engine) - with Session(engine) as session: - session.add(recorder.db_schema.StatisticsRuns(start=get_start_time())) - session.add( - recorder.db_schema.SchemaChanges( - schema_version=old_db_schema.SCHEMA_VERSION - ) - ) - session.commit() - return engine - - test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") - dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" - - module = "tests.components.recorder.db_schema_29" - importlib.import_module(module) - old_db_schema = sys.modules[module] - - external_energy_metadata_1 = { - "has_mean": False, - "has_sum": True, - "name": "Total imported energy", - "source": "test", - "statistic_id": "test:total_energy_import_tariff_1", - "unit_of_measurement": "kWh", - } - external_co2_metadata = { - "has_mean": True, - "has_sum": False, - "name": "Fossil percentage", - "source": "test", - "statistic_id": "test:fossil_percentage", - "unit_of_measurement": "%", - } - - # Create some statistics_meta with schema version 29 - with patch.object(recorder, "db_schema", old_db_schema), patch.object( - recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION - ), patch( - "homeassistant.components.recorder.core.create_engine", new=_create_engine_29 - ): - hass = get_test_home_assistant() - recorder_helper.async_initialize_recorder(hass) - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) - wait_recording_done(hass) - wait_recording_done(hass) - - with session_scope(hass=hass) as session: - session.add( - recorder.db_schema.StatisticsMeta.from_meta(external_energy_metadata_1) - ) - session.add( - recorder.db_schema.StatisticsMeta.from_meta(external_co2_metadata) - ) - - with session_scope(hass=hass) as session: - tmp = session.query(recorder.db_schema.StatisticsMeta).all() - assert len(tmp) == 2 - assert tmp[0].id == 1 - assert tmp[0].statistic_id == "test:total_energy_import_tariff_1" - assert tmp[0].unit_of_measurement == "kWh" - assert not hasattr(tmp[0], "state_unit_of_measurement") - assert tmp[1].id == 2 - assert tmp[1].statistic_id == "test:fossil_percentage" - assert tmp[1].unit_of_measurement == "%" - assert not hasattr(tmp[1], "state_unit_of_measurement") - - hass.stop() - dt_util.DEFAULT_TIME_ZONE = ORIG_TZ - - # Test that the state_unit column is initialized during migration from schema 28 - hass = get_test_home_assistant() - recorder_helper.async_initialize_recorder(hass) - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) - hass.start() - wait_recording_done(hass) - wait_recording_done(hass) - - with session_scope(hass=hass) as session: - tmp = session.query(recorder.db_schema.StatisticsMeta).all() - assert len(tmp) == 2 - assert tmp[0].id == 1 - assert tmp[0].statistic_id == "test:total_energy_import_tariff_1" - assert tmp[0].unit_of_measurement == "kWh" - assert hasattr(tmp[0], "state_unit_of_measurement") - assert tmp[0].state_unit_of_measurement == "kWh" - assert tmp[1].id == 2 - assert tmp[1].statistic_id == "test:fossil_percentage" - assert hasattr(tmp[1], "state_unit_of_measurement") - assert tmp[1].state_unit_of_measurement == "%" - assert tmp[1].state_unit_of_measurement == "%" - - hass.stop() - dt_util.DEFAULT_TIME_ZONE = ORIG_TZ - - def test_invalid_update(hass): """Test that an invalid new version raises an exception.""" with pytest.raises(ValueError): diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index c96b984bcf4..6d96b97b89c 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -159,7 +159,6 @@ def mock_sensor_statistics(): "has_mean": True, "has_sum": False, "name": None, - "state_unit_of_measurement": "dogs", "statistic_id": entity_id, "unit_of_measurement": "dogs", }, @@ -488,7 +487,6 @@ async def test_import_statistics( "has_sum": True, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": "kWh", "statistic_id": statistic_id, "unit_of_measurement": "kWh", } @@ -530,7 +528,6 @@ async def test_import_statistics( "statistic_id": statistic_id, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -544,7 +541,6 @@ async def test_import_statistics( "has_sum": True, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": "kWh", "statistic_id": statistic_id, "unit_of_measurement": "kWh", }, @@ -604,7 +600,7 @@ async def test_import_statistics( ] } - # Update the previously inserted statistics + rename and change display unit + # Update the previously inserted statistics + rename external_statistics = { "start": period1, "max": 1, @@ -615,7 +611,6 @@ async def test_import_statistics( "sum": 5, } external_metadata["name"] = "Total imported energy renamed" - external_metadata["state_unit_of_measurement"] = "MWh" import_fn(hass, external_metadata, (external_statistics,)) await async_wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) @@ -626,7 +621,6 @@ async def test_import_statistics( "statistic_id": statistic_id, "name": "Total imported energy renamed", "source": source, - "state_unit_of_measurement": "MWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -640,7 +634,6 @@ async def test_import_statistics( "has_sum": True, "name": "Total imported energy renamed", "source": source, - "state_unit_of_measurement": "MWh", "statistic_id": statistic_id, "unit_of_measurement": "kWh", }, @@ -653,12 +646,12 @@ async def test_import_statistics( "statistic_id": statistic_id, "start": period1.isoformat(), "end": (period1 + timedelta(hours=1)).isoformat(), - "max": approx(1.0 / 1000), - "mean": approx(2.0 / 1000), - "min": approx(3.0 / 1000), + "max": approx(1.0), + "mean": approx(2.0), + "min": approx(3.0), "last_reset": last_reset_utc_str, - "state": approx(4.0 / 1000), - "sum": approx(5.0 / 1000), + "state": approx(4.0), + "sum": approx(5.0), }, { "statistic_id": statistic_id, @@ -668,13 +661,13 @@ async def test_import_statistics( "mean": None, "min": None, "last_reset": last_reset_utc_str, - "state": approx(1.0 / 1000), - "sum": approx(3.0 / 1000), + "state": approx(1.0), + "sum": approx(3.0), }, ] } - # Adjust the statistics + # Adjust the statistics in a different unit await client.send_json( { "id": 1, @@ -696,12 +689,12 @@ async def test_import_statistics( "statistic_id": statistic_id, "start": period1.isoformat(), "end": (period1 + timedelta(hours=1)).isoformat(), - "max": approx(1.0 / 1000), - "mean": approx(2.0 / 1000), - "min": approx(3.0 / 1000), + "max": approx(1.0), + "mean": approx(2.0), + "min": approx(3.0), "last_reset": last_reset_utc_str, - "state": approx(4.0 / 1000), - "sum": approx(5.0 / 1000), + "state": approx(4.0), + "sum": approx(5.0), }, { "statistic_id": statistic_id, @@ -711,8 +704,8 @@ async def test_import_statistics( "mean": None, "min": None, "last_reset": last_reset_utc_str, - "state": approx(1.0 / 1000), - "sum": approx(1000 + 3.0 / 1000), + "state": approx(1.0), + "sum": approx(1000 * 1000 + 3.0), }, ] } @@ -741,7 +734,6 @@ def test_external_statistics_errors(hass_recorder, caplog): "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import", "unit_of_measurement": "kWh", } @@ -805,16 +797,6 @@ def test_external_statistics_errors(hass_recorder, caplog): assert list_statistic_ids(hass) == [] assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} - # Attempt to insert statistics with an invalid unit combination - external_metadata = {**_external_metadata, "state_unit_of_measurement": "cats"} - external_statistics = {**_external_statistics} - with pytest.raises(HomeAssistantError): - async_add_external_statistics(hass, external_metadata, (external_statistics,)) - wait_recording_done(hass) - assert statistics_during_period(hass, zero, period="hour") == {} - assert list_statistic_ids(hass) == [] - assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} - def test_import_statistics_errors(hass_recorder, caplog): """Test validation of imported statistics.""" @@ -839,7 +821,6 @@ def test_import_statistics_errors(hass_recorder, caplog): "has_sum": True, "name": "Total imported energy", "source": "recorder", - "state_unit_of_measurement": "kWh", "statistic_id": "sensor.total_energy_import", "unit_of_measurement": "kWh", } @@ -903,16 +884,6 @@ def test_import_statistics_errors(hass_recorder, caplog): assert list_statistic_ids(hass) == [] assert get_metadata(hass, statistic_ids=("sensor.total_energy_import",)) == {} - # Attempt to insert statistics with an invalid unit combination - external_metadata = {**_external_metadata, "state_unit_of_measurement": "cats"} - external_statistics = {**_external_statistics} - with pytest.raises(HomeAssistantError): - async_import_statistics(hass, external_metadata, (external_statistics,)) - wait_recording_done(hass) - assert statistics_during_period(hass, zero, period="hour") == {} - assert list_statistic_ids(hass) == [] - assert get_metadata(hass, statistic_ids=("sensor.total_energy_import",)) == {} - @pytest.mark.parametrize("timezone", ["America/Regina", "Europe/Vienna", "UTC"]) @pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") @@ -962,7 +933,6 @@ def test_monthly_statistics(hass_recorder, caplog, timezone): "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import", "unit_of_measurement": "kWh", } @@ -1081,7 +1051,6 @@ def test_duplicate_statistics_handle_integrity_error(hass_recorder, caplog): "has_sum": True, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": "kWh", "statistic_id": "test:total_energy_import_tariff_1", "unit_of_measurement": "kWh", } diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 4d4a1604a91..58893ee3bb1 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -651,7 +651,6 @@ async def test_list_statistic_ids( "has_sum": has_sum, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -673,7 +672,6 @@ async def test_list_statistic_ids( "has_sum": has_sum, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -698,7 +696,6 @@ async def test_list_statistic_ids( "has_sum": has_sum, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -719,7 +716,6 @@ async def test_list_statistic_ids( "has_sum": has_sum, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -907,7 +903,6 @@ async def test_update_statistics_metadata( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": "kW", "unit_class": None, } @@ -935,7 +930,6 @@ async def test_update_statistics_metadata( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": new_unit, "unit_class": new_unit_class, } @@ -999,7 +993,6 @@ async def test_change_statistics_unit(hass, hass_ws_client, recorder_mock): "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": "kW", "unit_class": None, } @@ -1055,7 +1048,6 @@ async def test_change_statistics_unit(hass, hass_ws_client, recorder_mock): "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": "W", "unit_class": "power", } @@ -1108,7 +1100,6 @@ async def test_change_statistics_unit_errors( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "kW", "statistics_unit_of_measurement": "kW", "unit_class": None, } @@ -1461,7 +1452,6 @@ async def test_get_statistics_metadata( "has_sum": has_sum, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": unit, "statistic_id": "test:total_gas", "unit_of_measurement": unit, } @@ -1487,7 +1477,6 @@ async def test_get_statistics_metadata( "has_sum": has_sum, "name": "Total imported energy", "source": "test", - "state_unit_of_measurement": unit, "statistics_unit_of_measurement": unit, "unit_class": unit_class, } @@ -1515,7 +1504,6 @@ async def test_get_statistics_metadata( "has_sum": has_sum, "name": None, "source": "recorder", - "state_unit_of_measurement": attributes["unit_of_measurement"], "statistics_unit_of_measurement": unit, "unit_class": unit_class, } @@ -1543,7 +1531,6 @@ async def test_get_statistics_metadata( "has_sum": has_sum, "name": None, "source": "recorder", - "state_unit_of_measurement": attributes["unit_of_measurement"], "statistics_unit_of_measurement": unit, "unit_class": unit_class, } @@ -1640,7 +1627,6 @@ async def test_import_statistics( "statistic_id": statistic_id, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -1654,7 +1640,6 @@ async def test_import_statistics( "has_sum": True, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": "kWh", "statistic_id": statistic_id, "unit_of_measurement": "kWh", }, @@ -1869,7 +1854,6 @@ async def test_adjust_sum_statistics_energy( "statistic_id": statistic_id, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -1883,7 +1867,6 @@ async def test_adjust_sum_statistics_energy( "has_sum": True, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": "kWh", "statistic_id": statistic_id, "unit_of_measurement": "kWh", }, @@ -2067,7 +2050,6 @@ async def test_adjust_sum_statistics_gas( "statistic_id": statistic_id, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": "m³", "statistics_unit_of_measurement": "m³", "unit_class": "volume", } @@ -2081,7 +2063,6 @@ async def test_adjust_sum_statistics_gas( "has_sum": True, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": "m³", "statistic_id": statistic_id, "unit_of_measurement": "m³", }, @@ -2281,7 +2262,6 @@ async def test_adjust_sum_statistics_errors( "statistic_id": statistic_id, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": state_unit, "statistics_unit_of_measurement": statistic_unit, "unit_class": unit_class, } @@ -2295,7 +2275,6 @@ async def test_adjust_sum_statistics_errors( "has_sum": True, "name": "Total imported energy", "source": source, - "state_unit_of_measurement": state_unit, "statistic_id": statistic_id, "unit_of_measurement": statistic_unit, }, diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index f0013874e23..637d17e21a8 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -140,7 +140,6 @@ def test_compile_hourly_statistics( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -215,7 +214,6 @@ def test_compile_hourly_statistics_purged_state_changes( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -285,7 +283,6 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "°C", "statistics_unit_of_measurement": "°C", "unit_class": "temperature", }, @@ -295,7 +292,6 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "°C", "statistics_unit_of_measurement": "°C", "unit_class": "temperature", }, @@ -305,7 +301,6 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "°C", "statistics_unit_of_measurement": "°C", "unit_class": "temperature", }, @@ -440,7 +435,6 @@ async def test_compile_hourly_sum_statistics_amount( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -633,7 +627,6 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -734,7 +727,6 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -819,7 +811,6 @@ def test_compile_hourly_sum_statistics_nan_inf_state( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -933,7 +924,6 @@ def test_compile_hourly_sum_statistics_negative_state( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistic_id": entity_id, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, @@ -1022,7 +1012,6 @@ def test_compile_hourly_sum_statistics_total_no_reset( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -1125,7 +1114,6 @@ def test_compile_hourly_sum_statistics_total_increasing( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -1239,7 +1227,6 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, } @@ -1334,7 +1321,6 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", } @@ -1427,7 +1413,6 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", }, @@ -1437,7 +1422,6 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": "kWh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", }, @@ -1447,7 +1431,6 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": "Wh", "statistics_unit_of_measurement": "kWh", "unit_class": "energy", }, @@ -1811,7 +1794,6 @@ def test_list_statistic_ids( "has_sum": statistic_type == "sum", "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -1826,7 +1808,6 @@ def test_list_statistic_ids( "has_sum": statistic_type == "sum", "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -1917,7 +1898,6 @@ def test_compile_hourly_statistics_changing_units_1( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -1953,7 +1933,6 @@ def test_compile_hourly_statistics_changing_units_1( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -2029,7 +2008,6 @@ def test_compile_hourly_statistics_changing_units_2( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "cats", "statistics_unit_of_measurement": "cats", "unit_class": unit_class, }, @@ -2095,7 +2073,6 @@ def test_compile_hourly_statistics_changing_units_3( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -2131,7 +2108,6 @@ def test_compile_hourly_statistics_changing_units_3( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistics_unit, "unit_class": unit_class, }, @@ -2197,7 +2173,6 @@ def test_compile_hourly_statistics_changing_device_class_1( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": state_unit, "statistics_unit_of_measurement": state_unit, "unit_class": unit_class, }, @@ -2243,7 +2218,6 @@ def test_compile_hourly_statistics_changing_device_class_1( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": state_unit, "statistics_unit_of_measurement": state_unit, "unit_class": unit_class, }, @@ -2306,7 +2280,6 @@ def test_compile_hourly_statistics_changing_device_class_1( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": state_unit, "statistics_unit_of_measurement": state_unit, "unit_class": unit_class, }, @@ -2386,7 +2359,6 @@ def test_compile_hourly_statistics_changing_device_class_2( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistic_unit, "unit_class": unit_class, }, @@ -2436,7 +2408,6 @@ def test_compile_hourly_statistics_changing_device_class_2( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": display_unit, "statistics_unit_of_measurement": statistic_unit, "unit_class": unit_class, }, @@ -2506,7 +2477,6 @@ def test_compile_hourly_statistics_changing_statistics( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": None, "statistics_unit_of_measurement": None, "unit_class": None, }, @@ -2520,7 +2490,6 @@ def test_compile_hourly_statistics_changing_statistics( "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": None, "statistic_id": "sensor.test1", "unit_of_measurement": None, }, @@ -2543,7 +2512,6 @@ def test_compile_hourly_statistics_changing_statistics( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": None, "statistics_unit_of_measurement": None, "unit_class": None, }, @@ -2557,7 +2525,6 @@ def test_compile_hourly_statistics_changing_statistics( "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": None, "statistic_id": "sensor.test1", "unit_of_measurement": None, }, @@ -2738,7 +2705,6 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "%", "statistics_unit_of_measurement": "%", "unit_class": None, }, @@ -2748,7 +2714,6 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "%", "statistics_unit_of_measurement": "%", "unit_class": None, }, @@ -2758,7 +2723,6 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): "has_sum": False, "name": None, "source": "recorder", - "state_unit_of_measurement": "%", "statistics_unit_of_measurement": "%", "unit_class": None, }, @@ -2768,7 +2732,6 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): "has_sum": True, "name": None, "source": "recorder", - "state_unit_of_measurement": "EUR", "statistics_unit_of_measurement": "EUR", "unit_class": None, }, From 94c825cf4fd11632413081b639a67d9cbbb18f36 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Sat, 1 Oct 2022 20:58:57 +0200 Subject: [PATCH 078/985] vicare: Add additional temperature sensors (#79426) Add additional temperature sensors New datapoints in PyVicare API --- homeassistant/components/vicare/sensor.py | 48 +++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index e1deef0df00..86f2f393138 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -86,6 +86,38 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), + ViCareSensorEntityDescription( + key="primary_circuit_supply_temperature", + name="Primary Circuit Supply Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + value_getter=lambda api: api.getSupplyTemperaturePrimaryCircuit(), + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + ViCareSensorEntityDescription( + key="primary_circuit_return_temperature", + name="Primary Circuit Return Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + value_getter=lambda api: api.getReturnTemperaturePrimaryCircuit(), + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + ViCareSensorEntityDescription( + key="secondary_circuit_supply_temperature", + name="Secondary Circuit Supply Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + value_getter=lambda api: api.getSupplyTemperatureSecondaryCircuit(), + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + ViCareSensorEntityDescription( + key="secondary_circuit_return_temperature", + name="Secondary Circuit Return Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + value_getter=lambda api: api.getReturnTemperatureSecondaryCircuit(), + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), ViCareSensorEntityDescription( key="hotwater_out_temperature", name="Hot Water Out Temperature", @@ -94,6 +126,22 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), + ViCareSensorEntityDescription( + key="hotwater_max_temperature", + name="Hot Water Max Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + value_getter=lambda api: api.getDomesticHotWaterMaxTemperature(), + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + ViCareSensorEntityDescription( + key="hotwater_min_temperature", + name="Hot Water Min Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + value_getter=lambda api: api.getDomesticHotWaterMinTemperature(), + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), ViCareSensorEntityDescription( key="hotwater_gas_consumption_today", name="Hot water gas consumption today", From 35fa73eee9a472b74f387b526055b80df53c7c10 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Sat, 1 Oct 2022 21:17:25 +0200 Subject: [PATCH 079/985] vicare: Don't create unsupportedd button entites (#79425) Button entities should only be offered when the datapoint exists on the API. --- homeassistant/components/vicare/__init__.py | 8 ++++++++ homeassistant/components/vicare/button.py | 20 ++++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index 20d237ee4e4..b177a4c524f 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -34,6 +34,14 @@ class ViCareRequiredKeysMixin: value_getter: Callable[[Device], bool] +@dataclass() +class ViCareRequiredKeysMixinWithSet: + """Mixin for required keys with setter.""" + + value_getter: Callable[[Device], bool] + value_setter: Callable[[Device], bool] + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from config entry.""" _LOGGER.debug("Setting up ViCare component") diff --git a/homeassistant/components/vicare/button.py b/homeassistant/components/vicare/button.py index b691c01796b..6f94c7102c9 100644 --- a/homeassistant/components/vicare/button.py +++ b/homeassistant/components/vicare/button.py @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ViCareRequiredKeysMixin +from . import ViCareRequiredKeysMixinWithSet from .const import DOMAIN, VICARE_API, VICARE_DEVICE_CONFIG, VICARE_NAME _LOGGER = logging.getLogger(__name__) @@ -27,7 +27,9 @@ BUTTON_DHW_ACTIVATE_ONETIME_CHARGE = "activate_onetimecharge" @dataclass -class ViCareButtonEntityDescription(ButtonEntityDescription, ViCareRequiredKeysMixin): +class ViCareButtonEntityDescription( + ButtonEntityDescription, ViCareRequiredKeysMixinWithSet +): """Describes ViCare button sensor entity.""" @@ -37,7 +39,8 @@ BUTTON_DESCRIPTIONS: tuple[ViCareButtonEntityDescription, ...] = ( name="Activate one-time charge", icon="mdi:shower-head", entity_category=EntityCategory.CONFIG, - value_getter=lambda api: api.activateOneTimeCharge(), + value_getter=lambda api: api.getOneTimeCharge(), + value_setter=lambda api: api.activateOneTimeCharge(), ), ) @@ -54,6 +57,15 @@ async def async_setup_entry( entities = [] for description in BUTTON_DESCRIPTIONS: + try: + description.value_getter(api) + _LOGGER.debug("Found entity %s", description.name) + except PyViCareNotSupportedFeatureError: + _LOGGER.info("Feature not supported %s", description.name) + continue + except AttributeError: + _LOGGER.debug("Attribute Error %s", name) + continue entity = ViCareButton( f"{name} {description.name}", api, @@ -83,7 +95,7 @@ class ViCareButton(ButtonEntity): """Handle the button press.""" try: with suppress(PyViCareNotSupportedFeatureError): - self.entity_description.value_getter(self._api) + self.entity_description.value_setter(self._api) except requests.exceptions.ConnectionError: _LOGGER.error("Unable to retrieve data from ViCare server") except ValueError: From 9058b5b9c3ad9f294cb81d9cd1801d27af94a603 Mon Sep 17 00:00:00 2001 From: Garrett <7310260+G-Two@users.noreply.github.com> Date: Sat, 1 Oct 2022 18:25:49 -0400 Subject: [PATCH 080/985] Update sensors for Subaru integration (#66996) * Update sensor.py * Change "EV Time to Fully Charged" type to datetime object (HA 2022.2) * Validate types before accessing dict entries * Test handling of invalid data from Subaru * Bump to subarulink 0.4.2 * Incorporate style suggestion * Update sensor.py to use SensorEntity * isort tests * Remove SubaruSensor.current_value * Fix isort errors * Resolve conflict from previous PR (add locks) * Fix linting errors in config_flow.py * Incorporate PR review comments for sensor * Incorporate PR review comments for sensor * Make 3rd party library responsible for API data parsing * Add type annotations to sensor.py * Incorporate PR review comments * Incorporate PR review comments * Set _attr_has_entity_name = True for sensors --- .../components/subaru/config_flow.py | 22 +- homeassistant/components/subaru/const.py | 2 +- homeassistant/components/subaru/entity.py | 35 -- homeassistant/components/subaru/manifest.json | 2 +- homeassistant/components/subaru/sensor.py | 341 +++++++++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/subaru/api_responses.py | 66 ++-- tests/components/subaru/conftest.py | 3 +- tests/components/subaru/test_sensor.py | 22 +- 10 files changed, 225 insertions(+), 272 deletions(-) delete mode 100644 homeassistant/components/subaru/entity.py diff --git a/homeassistant/components/subaru/config_flow.py b/homeassistant/components/subaru/config_flow.py index 79c412c8f85..6d1d5015ed3 100644 --- a/homeassistant/components/subaru/config_flow.py +++ b/homeassistant/components/subaru/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import datetime import logging +from typing import Any from subarulink import ( Controller as SubaruAPI, @@ -16,6 +17,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import CONF_COUNTRY, CONF_UPDATE_ENABLED, DOMAIN @@ -36,7 +38,9 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.config_data = {CONF_PIN: None} self.controller = None - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the start of the config flow.""" error = None @@ -117,7 +121,9 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.debug("Successfully authenticated with Subaru API") self.config_data.update(data) - async def async_step_two_factor(self, user_input=None): + async def async_step_two_factor( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Select contact method and request 2FA code from Subaru.""" error = None if user_input: @@ -143,7 +149,9 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="two_factor", data_schema=data_schema, errors=error ) - async def async_step_two_factor_validate(self, user_input=None): + async def async_step_two_factor_validate( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Validate received 2FA code with Subaru.""" error = None if user_input: @@ -166,7 +174,9 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="two_factor_validate", data_schema=data_schema, errors=error ) - async def async_step_pin(self, user_input=None): + async def async_step_pin( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle second part of config flow, if required.""" error = None if user_input and self.controller.update_saved_pin(user_input[CONF_PIN]): @@ -193,7 +203,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Initialize options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle options flow.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/subaru/const.py b/homeassistant/components/subaru/const.py index 3ad7dd58af5..dc9a2224860 100644 --- a/homeassistant/components/subaru/const.py +++ b/homeassistant/components/subaru/const.py @@ -31,7 +31,7 @@ VEHICLE_STATUS = "status" API_GEN_1 = "g1" API_GEN_2 = "g2" -MANUFACTURER = "Subaru Corp." +MANUFACTURER = "Subaru" PLATFORMS = [ Platform.LOCK, diff --git a/homeassistant/components/subaru/entity.py b/homeassistant/components/subaru/entity.py deleted file mode 100644 index 2bdb1425b2d..00000000000 --- a/homeassistant/components/subaru/entity.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Base class for all Subaru Entities.""" -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import DOMAIN, MANUFACTURER, VEHICLE_NAME, VEHICLE_VIN - - -class SubaruEntity(CoordinatorEntity): - """Representation of a Subaru Entity.""" - - def __init__(self, vehicle_info, coordinator): - """Initialize the Subaru Entity.""" - super().__init__(coordinator) - self.car_name = vehicle_info[VEHICLE_NAME] - self.vin = vehicle_info[VEHICLE_VIN] - self.entity_type = "entity" - - @property - def name(self): - """Return name.""" - return f"{self.car_name} {self.entity_type}" - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return f"{self.vin}_{self.entity_type}" - - @property - def device_info(self) -> DeviceInfo: - """Return the device_info of the device.""" - return DeviceInfo( - identifiers={(DOMAIN, self.vin)}, - manufacturer=MANUFACTURER, - name=self.car_name, - ) diff --git a/homeassistant/components/subaru/manifest.json b/homeassistant/components/subaru/manifest.json index 0123f26f916..6bae6e8422d 100644 --- a/homeassistant/components/subaru/manifest.json +++ b/homeassistant/components/subaru/manifest.json @@ -3,7 +3,7 @@ "name": "Subaru", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/subaru", - "requirements": ["subarulink==0.5.0"], + "requirements": ["subarulink==0.6.0"], "codeowners": ["@G-Two"], "iot_class": "cloud_polling", "loggers": ["stdiomask", "subarulink"] diff --git a/homeassistant/components/subaru/sensor.py b/homeassistant/components/subaru/sensor.py index f1a1ea96382..672fe6b0dcc 100644 --- a/homeassistant/components/subaru/sensor.py +++ b/homeassistant/components/subaru/sensor.py @@ -1,10 +1,16 @@ """Support for Subaru sensors.""" +from __future__ import annotations + +import logging +from typing import Any + import subarulink.const as sc from homeassistant.components.sensor import ( - DEVICE_CLASSES, SensorDeviceClass, SensorEntity, + SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -14,128 +20,133 @@ from homeassistant.const import ( PERCENTAGE, PRESSURE_HPA, TEMP_CELSIUS, - TIME_MINUTES, VOLUME_GALLONS, VOLUME_LITERS, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util.unit_conversion import DistanceConverter, VolumeConverter -from homeassistant.util.unit_system import ( - IMPERIAL_SYSTEM, - LENGTH_UNITS, - PRESSURE_UNITS, - TEMPERATURE_UNITS, +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, ) +from homeassistant.util.unit_conversion import DistanceConverter, VolumeConverter +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, LENGTH_UNITS, PRESSURE_UNITS +from . import get_device_info from .const import ( API_GEN_2, DOMAIN, ENTRY_COORDINATOR, ENTRY_VEHICLES, - ICONS, VEHICLE_API_GEN, VEHICLE_HAS_EV, VEHICLE_HAS_SAFETY_SERVICE, VEHICLE_STATUS, + VEHICLE_VIN, ) -from .entity import SubaruEntity + +_LOGGER = logging.getLogger(__name__) + +# Fuel consumption units +FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS = "L/100km" +FUEL_CONSUMPTION_MILES_PER_GALLON = "mi/gal" L_PER_GAL = VolumeConverter.convert(1, VOLUME_GALLONS, VOLUME_LITERS) KM_PER_MI = DistanceConverter.convert(1, LENGTH_MILES, LENGTH_KILOMETERS) -# Fuel Economy Constants -FUEL_CONSUMPTION_L_PER_100KM = "L/100km" -FUEL_CONSUMPTION_MPG = "mi/gal" -FUEL_CONSUMPTION_UNITS = [FUEL_CONSUMPTION_L_PER_100KM, FUEL_CONSUMPTION_MPG] - -SENSOR_TYPE = "type" -SENSOR_CLASS = "class" -SENSOR_FIELD = "field" -SENSOR_UNITS = "units" - -# Sensor data available to "Subaru Safety Plus" subscribers with Gen1 or Gen2 vehicles +# Sensor available to "Subaru Safety Plus" subscribers with Gen1 or Gen2 vehicles SAFETY_SENSORS = [ - { - SENSOR_TYPE: "Odometer", - SENSOR_CLASS: None, - SENSOR_FIELD: sc.ODOMETER, - SENSOR_UNITS: LENGTH_KILOMETERS, - }, + SensorEntityDescription( + key=sc.ODOMETER, + icon="mdi:road-variant", + name="Odometer", + native_unit_of_measurement=LENGTH_KILOMETERS, + state_class=SensorStateClass.TOTAL_INCREASING, + ), ] -# Sensor data available to "Subaru Safety Plus" subscribers with Gen2 vehicles +# Sensors available to "Subaru Safety Plus" subscribers with Gen2 vehicles API_GEN_2_SENSORS = [ - { - SENSOR_TYPE: "Avg Fuel Consumption", - SENSOR_CLASS: None, - SENSOR_FIELD: sc.AVG_FUEL_CONSUMPTION, - SENSOR_UNITS: FUEL_CONSUMPTION_L_PER_100KM, - }, - { - SENSOR_TYPE: "Range", - SENSOR_CLASS: None, - SENSOR_FIELD: sc.DIST_TO_EMPTY, - SENSOR_UNITS: LENGTH_KILOMETERS, - }, - { - SENSOR_TYPE: "Tire Pressure FL", - SENSOR_CLASS: SensorDeviceClass.PRESSURE, - SENSOR_FIELD: sc.TIRE_PRESSURE_FL, - SENSOR_UNITS: PRESSURE_HPA, - }, - { - SENSOR_TYPE: "Tire Pressure FR", - SENSOR_CLASS: SensorDeviceClass.PRESSURE, - SENSOR_FIELD: sc.TIRE_PRESSURE_FR, - SENSOR_UNITS: PRESSURE_HPA, - }, - { - SENSOR_TYPE: "Tire Pressure RL", - SENSOR_CLASS: SensorDeviceClass.PRESSURE, - SENSOR_FIELD: sc.TIRE_PRESSURE_RL, - SENSOR_UNITS: PRESSURE_HPA, - }, - { - SENSOR_TYPE: "Tire Pressure RR", - SENSOR_CLASS: SensorDeviceClass.PRESSURE, - SENSOR_FIELD: sc.TIRE_PRESSURE_RR, - SENSOR_UNITS: PRESSURE_HPA, - }, - { - SENSOR_TYPE: "External Temp", - SENSOR_CLASS: SensorDeviceClass.TEMPERATURE, - SENSOR_FIELD: sc.EXTERNAL_TEMP, - SENSOR_UNITS: TEMP_CELSIUS, - }, - { - SENSOR_TYPE: "12V Battery Voltage", - SENSOR_CLASS: SensorDeviceClass.VOLTAGE, - SENSOR_FIELD: sc.BATTERY_VOLTAGE, - SENSOR_UNITS: ELECTRIC_POTENTIAL_VOLT, - }, + SensorEntityDescription( + key=sc.AVG_FUEL_CONSUMPTION, + icon="mdi:leaf", + name="Avg Fuel Consumption", + native_unit_of_measurement=FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=sc.DIST_TO_EMPTY, + icon="mdi:gas-station", + name="Range", + native_unit_of_measurement=LENGTH_KILOMETERS, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=sc.TIRE_PRESSURE_FL, + device_class=SensorDeviceClass.PRESSURE, + name="Tire Pressure FL", + native_unit_of_measurement=PRESSURE_HPA, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=sc.TIRE_PRESSURE_FR, + device_class=SensorDeviceClass.PRESSURE, + name="Tire Pressure FR", + native_unit_of_measurement=PRESSURE_HPA, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=sc.TIRE_PRESSURE_RL, + device_class=SensorDeviceClass.PRESSURE, + name="Tire Pressure RL", + native_unit_of_measurement=PRESSURE_HPA, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=sc.TIRE_PRESSURE_RR, + device_class=SensorDeviceClass.PRESSURE, + name="Tire Pressure RR", + native_unit_of_measurement=PRESSURE_HPA, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=sc.EXTERNAL_TEMP, + device_class=SensorDeviceClass.TEMPERATURE, + name="External Temp", + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=sc.BATTERY_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, + name="12V Battery Voltage", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), ] -# Sensor data available to "Subaru Safety Plus" subscribers with PHEV vehicles +# Sensors available to "Subaru Safety Plus" subscribers with PHEV vehicles EV_SENSORS = [ - { - SENSOR_TYPE: "EV Range", - SENSOR_CLASS: None, - SENSOR_FIELD: sc.EV_DISTANCE_TO_EMPTY, - SENSOR_UNITS: LENGTH_MILES, - }, - { - SENSOR_TYPE: "EV Battery Level", - SENSOR_CLASS: SensorDeviceClass.BATTERY, - SENSOR_FIELD: sc.EV_STATE_OF_CHARGE_PERCENT, - SENSOR_UNITS: PERCENTAGE, - }, - { - SENSOR_TYPE: "EV Time to Full Charge", - SENSOR_CLASS: SensorDeviceClass.TIMESTAMP, - SENSOR_FIELD: sc.EV_TIME_TO_FULLY_CHARGED, - SENSOR_UNITS: TIME_MINUTES, - }, + SensorEntityDescription( + key=sc.EV_DISTANCE_TO_EMPTY, + icon="mdi:ev-station", + name="EV Range", + native_unit_of_measurement=LENGTH_MILES, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=sc.EV_STATE_OF_CHARGE_PERCENT, + device_class=SensorDeviceClass.BATTERY, + name="EV Battery Level", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=sc.EV_TIME_TO_FULLY_CHARGED_UTC, + device_class=SensorDeviceClass.TIMESTAMP, + name="EV Time to Full Charge", + state_class=SensorStateClass.MEASUREMENT, + ), ] @@ -145,123 +156,111 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Subaru sensors by config_entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id][ENTRY_COORDINATOR] - vehicle_info = hass.data[DOMAIN][config_entry.entry_id][ENTRY_VEHICLES] + entry = hass.data[DOMAIN][config_entry.entry_id] + coordinator = entry[ENTRY_COORDINATOR] + vehicle_info = entry[ENTRY_VEHICLES] entities = [] - for vin in vehicle_info: - entities.extend(create_vehicle_sensors(vehicle_info[vin], coordinator)) - async_add_entities(entities, True) + for info in vehicle_info.values(): + entities.extend(create_vehicle_sensors(info, coordinator)) + async_add_entities(entities) -def create_vehicle_sensors(vehicle_info, coordinator): +def create_vehicle_sensors( + vehicle_info, coordinator: DataUpdateCoordinator +) -> list[SubaruSensor]: """Instantiate all available sensors for the vehicle.""" - sensors_to_add = [] + sensor_descriptions_to_add = [] if vehicle_info[VEHICLE_HAS_SAFETY_SERVICE]: - sensors_to_add.extend(SAFETY_SENSORS) + sensor_descriptions_to_add.extend(SAFETY_SENSORS) if vehicle_info[VEHICLE_API_GEN] == API_GEN_2: - sensors_to_add.extend(API_GEN_2_SENSORS) + sensor_descriptions_to_add.extend(API_GEN_2_SENSORS) if vehicle_info[VEHICLE_HAS_EV]: - sensors_to_add.extend(EV_SENSORS) + sensor_descriptions_to_add.extend(EV_SENSORS) return [ SubaruSensor( vehicle_info, coordinator, - s[SENSOR_TYPE], - s[SENSOR_CLASS], - s[SENSOR_FIELD], - s[SENSOR_UNITS], + description, ) - for s in sensors_to_add + for description in sensor_descriptions_to_add ] -class SubaruSensor(SubaruEntity, SensorEntity): +class SubaruSensor( + CoordinatorEntity[DataUpdateCoordinator[dict[str, Any]]], SensorEntity +): """Class for Subaru sensors.""" + _attr_has_entity_name = True + def __init__( - self, vehicle_info, coordinator, entity_type, sensor_class, data_field, api_unit - ): + self, + vehicle_info: dict, + coordinator: DataUpdateCoordinator, + description: SensorEntityDescription, + ) -> None: """Initialize the sensor.""" - super().__init__(vehicle_info, coordinator) - self.hass_type = "sensor" - self.current_value = None - self.entity_type = entity_type - self.sensor_class = sensor_class - self.data_field = data_field - self.api_unit = api_unit + super().__init__(coordinator) + self.vin = vehicle_info[VEHICLE_VIN] + self.entity_description = description + self._attr_device_info = get_device_info(vehicle_info) + self._attr_unique_id = f"{self.vin}_{description.name}" @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - if self.sensor_class in DEVICE_CLASSES: - return self.sensor_class - return None - - @property - def icon(self): - """Return the icon of the sensor.""" - if not self.device_class: - return ICONS.get(self.entity_type) - return None - - @property - def native_value(self): + def native_value(self) -> None | int | float: """Return the state of the sensor.""" - self.current_value = self.get_current_value() + vehicle_data = self.coordinator.data[self.vin] + current_value = vehicle_data[VEHICLE_STATUS].get(self.entity_description.key) + unit = self.entity_description.native_unit_of_measurement + unit_system = self.hass.config.units - if self.current_value is None: + if current_value is None: return None - if self.api_unit in TEMPERATURE_UNITS: - return round( - self.hass.config.units.temperature(self.current_value, self.api_unit), 1 - ) + if unit in LENGTH_UNITS: + return round(unit_system.length(current_value, unit), 1) - if self.api_unit in LENGTH_UNITS: + if unit in PRESSURE_UNITS and unit_system == IMPERIAL_SYSTEM: return round( - self.hass.config.units.length(self.current_value, self.api_unit), 1 - ) - - if ( - self.api_unit in PRESSURE_UNITS - and self.hass.config.units == IMPERIAL_SYSTEM - ): - return round( - self.hass.config.units.pressure(self.current_value, self.api_unit), + unit_system.pressure(current_value, unit), 1, ) if ( - self.api_unit in FUEL_CONSUMPTION_UNITS - and self.hass.config.units == IMPERIAL_SYSTEM + unit + in [ + FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS, + FUEL_CONSUMPTION_MILES_PER_GALLON, + ] + and unit_system == IMPERIAL_SYSTEM ): - return round((100.0 * L_PER_GAL) / (KM_PER_MI * self.current_value), 1) + return round((100.0 * L_PER_GAL) / (KM_PER_MI * current_value), 1) - return self.current_value + return current_value @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str | None: """Return the unit_of_measurement of the device.""" - if self.api_unit in TEMPERATURE_UNITS: - return self.hass.config.units.temperature_unit + unit = self.entity_description.native_unit_of_measurement - if self.api_unit in LENGTH_UNITS: + if unit in LENGTH_UNITS: return self.hass.config.units.length_unit - if self.api_unit in PRESSURE_UNITS: + if unit in PRESSURE_UNITS: if self.hass.config.units == IMPERIAL_SYSTEM: return self.hass.config.units.pressure_unit - return PRESSURE_HPA - if self.api_unit in FUEL_CONSUMPTION_UNITS: + if unit in [ + FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS, + FUEL_CONSUMPTION_MILES_PER_GALLON, + ]: if self.hass.config.units == IMPERIAL_SYSTEM: - return FUEL_CONSUMPTION_MPG - return FUEL_CONSUMPTION_L_PER_100KM + return FUEL_CONSUMPTION_MILES_PER_GALLON - return self.api_unit + return unit @property def available(self) -> bool: @@ -270,15 +269,3 @@ class SubaruSensor(SubaruEntity, SensorEntity): if last_update_success and self.vin not in self.coordinator.data: return False return last_update_success - - def get_current_value(self): - """Get raw value from the coordinator.""" - value = self.coordinator.data[self.vin][VEHICLE_STATUS].get(self.data_field) - if value in sc.BAD_SENSOR_VALUES: - value = None - if isinstance(value, str): - if "." in value: - value = float(value) - else: - value = int(value) - return value diff --git a/requirements_all.txt b/requirements_all.txt index 04e4320779b..11a7b3630a8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2332,7 +2332,7 @@ streamlabswater==1.0.1 stringcase==1.2.0 # homeassistant.components.subaru -subarulink==0.5.0 +subarulink==0.6.0 # homeassistant.components.solarlog sunwatcher==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1dc842fa02f..76970ca4307 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1611,7 +1611,7 @@ stookalert==0.1.4 stringcase==1.2.0 # homeassistant.components.subaru -subarulink==0.5.0 +subarulink==0.6.0 # homeassistant.components.solarlog sunwatcher==0.2.1 diff --git a/tests/components/subaru/api_responses.py b/tests/components/subaru/api_responses.py index b6a79ab8829..bd107f4bb37 100644 --- a/tests/components/subaru/api_responses.py +++ b/tests/components/subaru/api_responses.py @@ -1,5 +1,7 @@ """Sample API response data for tests.""" +from datetime import datetime, timezone + from homeassistant.components.subaru.const import ( API_GEN_1, API_GEN_2, @@ -46,10 +48,12 @@ VEHICLE_DATA = { }, } +MOCK_DATETIME = datetime.fromtimestamp(1595560000, timezone.utc) + VEHICLE_STATUS_EV = { "status": { "AVG_FUEL_CONSUMPTION": 2.3, - "BATTERY_VOLTAGE": "12.0", + "BATTERY_VOLTAGE": 12.0, "DISTANCE_TO_EMPTY_FUEL": 707, "DOOR_BOOT_LOCK_STATUS": "UNKNOWN", "DOOR_BOOT_POSITION": "CLOSED", @@ -63,21 +67,17 @@ VEHICLE_STATUS_EV = { "DOOR_REAR_LEFT_POSITION": "CLOSED", "DOOR_REAR_RIGHT_LOCK_STATUS": "UNKNOWN", "DOOR_REAR_RIGHT_POSITION": "CLOSED", - "EV_CHARGER_STATE_TYPE": "CHARGING_STOPPED", + "EV_CHARGER_STATE_TYPE": "CHARGING", "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM", "EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1", - "EV_DISTANCE_TO_EMPTY": 17, + "EV_DISTANCE_TO_EMPTY": 1, "EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED", "EV_STATE_OF_CHARGE_MODE": "EV_MODE", - "EV_STATE_OF_CHARGE_PERCENT": "100", - "EV_TIME_TO_FULLY_CHARGED": "65535", - "EV_VEHICLE_TIME_DAYOFWEEK": "6", - "EV_VEHICLE_TIME_HOUR": "14", - "EV_VEHICLE_TIME_MINUTE": "20", - "EV_VEHICLE_TIME_SECOND": "39", - "EXT_EXTERNAL_TEMP": "21.5", + "EV_STATE_OF_CHARGE_PERCENT": 20, + "EV_TIME_TO_FULLY_CHARGED_UTC": MOCK_DATETIME, + "EXT_EXTERNAL_TEMP": 21.5, "ODOMETER": 1234, - "POSITION_HEADING_DEGREE": "150", + "POSITION_HEADING_DEGREE": 150, "POSITION_SPEED_KMPH": "0", "POSITION_TIMESTAMP": 1595560000.0, "SEAT_BELT_STATUS_FRONT_LEFT": "BELTED", @@ -100,7 +100,7 @@ VEHICLE_STATUS_EV = { "SEAT_OCCUPATION_STATUS_THIRD_RIGHT": "UNKNOWN", "TIMESTAMP": 1595560000.0, "TRANSMISSION_MODE": "UNKNOWN", - "TYRE_PRESSURE_FRONT_LEFT": 2550, + "TYRE_PRESSURE_FRONT_LEFT": 0, "TYRE_PRESSURE_FRONT_RIGHT": 2550, "TYRE_PRESSURE_REAR_LEFT": 2450, "TYRE_PRESSURE_REAR_RIGHT": 2350, @@ -121,10 +121,11 @@ VEHICLE_STATUS_EV = { } } + VEHICLE_STATUS_G2 = { "status": { "AVG_FUEL_CONSUMPTION": 2.3, - "BATTERY_VOLTAGE": "12.0", + "BATTERY_VOLTAGE": 12.0, "DISTANCE_TO_EMPTY_FUEL": 707, "DOOR_BOOT_LOCK_STATUS": "UNKNOWN", "DOOR_BOOT_POSITION": "CLOSED", @@ -138,9 +139,9 @@ VEHICLE_STATUS_G2 = { "DOOR_REAR_LEFT_POSITION": "CLOSED", "DOOR_REAR_RIGHT_LOCK_STATUS": "UNKNOWN", "DOOR_REAR_RIGHT_POSITION": "CLOSED", - "EXT_EXTERNAL_TEMP": "21.5", + "EXT_EXTERNAL_TEMP": None, "ODOMETER": 1234, - "POSITION_HEADING_DEGREE": "150", + "POSITION_HEADING_DEGREE": 150, "POSITION_SPEED_KMPH": "0", "POSITION_TIMESTAMP": 1595560000.0, "SEAT_BELT_STATUS_FRONT_LEFT": "BELTED", @@ -188,18 +189,14 @@ EXPECTED_STATE_EV_IMPERIAL = { "AVG_FUEL_CONSUMPTION": "102.3", "BATTERY_VOLTAGE": "12.0", "DISTANCE_TO_EMPTY_FUEL": "439.3", - "EV_CHARGER_STATE_TYPE": "CHARGING_STOPPED", + "EV_CHARGER_STATE_TYPE": "CHARGING", "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM", "EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1", - "EV_DISTANCE_TO_EMPTY": "17", + "EV_DISTANCE_TO_EMPTY": "1", "EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED", "EV_STATE_OF_CHARGE_MODE": "EV_MODE", - "EV_STATE_OF_CHARGE_PERCENT": "100", - "EV_TIME_TO_FULLY_CHARGED": "unknown", - "EV_VEHICLE_TIME_DAYOFWEEK": "6", - "EV_VEHICLE_TIME_HOUR": "14", - "EV_VEHICLE_TIME_MINUTE": "20", - "EV_VEHICLE_TIME_SECOND": "39", + "EV_STATE_OF_CHARGE_PERCENT": "20", + "EV_TIME_TO_FULLY_CHARGED_UTC": "2020-07-24T03:06:40+00:00", "EXT_EXTERNAL_TEMP": "70.7", "ODOMETER": "766.8", "POSITION_HEADING_DEGREE": "150", @@ -207,7 +204,7 @@ EXPECTED_STATE_EV_IMPERIAL = { "POSITION_TIMESTAMP": 1595560000.0, "TIMESTAMP": 1595560000.0, "TRANSMISSION_MODE": "UNKNOWN", - "TYRE_PRESSURE_FRONT_LEFT": "37.0", + "TYRE_PRESSURE_FRONT_LEFT": "0.0", "TYRE_PRESSURE_FRONT_RIGHT": "37.0", "TYRE_PRESSURE_REAR_LEFT": "35.5", "TYRE_PRESSURE_REAR_RIGHT": "34.1", @@ -221,18 +218,14 @@ EXPECTED_STATE_EV_METRIC = { "AVG_FUEL_CONSUMPTION": "2.3", "BATTERY_VOLTAGE": "12.0", "DISTANCE_TO_EMPTY_FUEL": "707", - "EV_CHARGER_STATE_TYPE": "CHARGING_STOPPED", + "EV_CHARGER_STATE_TYPE": "CHARGING", "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM", "EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1", - "EV_DISTANCE_TO_EMPTY": "27.4", + "EV_DISTANCE_TO_EMPTY": "1.6", "EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED", "EV_STATE_OF_CHARGE_MODE": "EV_MODE", - "EV_STATE_OF_CHARGE_PERCENT": "100", - "EV_TIME_TO_FULLY_CHARGED": "unknown", - "EV_VEHICLE_TIME_DAYOFWEEK": "6", - "EV_VEHICLE_TIME_HOUR": "14", - "EV_VEHICLE_TIME_MINUTE": "20", - "EV_VEHICLE_TIME_SECOND": "39", + "EV_STATE_OF_CHARGE_PERCENT": "20", + "EV_TIME_TO_FULLY_CHARGED_UTC": "2020-07-24T03:06:40+00:00", "EXT_EXTERNAL_TEMP": "21.5", "ODOMETER": "1234", "POSITION_HEADING_DEGREE": "150", @@ -240,7 +233,7 @@ EXPECTED_STATE_EV_METRIC = { "POSITION_TIMESTAMP": 1595560000.0, "TIMESTAMP": 1595560000.0, "TRANSMISSION_MODE": "UNKNOWN", - "TYRE_PRESSURE_FRONT_LEFT": "2550", + "TYRE_PRESSURE_FRONT_LEFT": "0", "TYRE_PRESSURE_FRONT_RIGHT": "2550", "TYRE_PRESSURE_REAR_LEFT": "2450", "TYRE_PRESSURE_REAR_RIGHT": "2350", @@ -250,6 +243,7 @@ EXPECTED_STATE_EV_METRIC = { "longitude": -100.0, } + EXPECTED_STATE_EV_UNAVAILABLE = { "AVG_FUEL_CONSUMPTION": "unavailable", "BATTERY_VOLTAGE": "unavailable", @@ -261,11 +255,7 @@ EXPECTED_STATE_EV_UNAVAILABLE = { "EV_IS_PLUGGED_IN": "unavailable", "EV_STATE_OF_CHARGE_MODE": "unavailable", "EV_STATE_OF_CHARGE_PERCENT": "unavailable", - "EV_TIME_TO_FULLY_CHARGED": "unavailable", - "EV_VEHICLE_TIME_DAYOFWEEK": "unavailable", - "EV_VEHICLE_TIME_HOUR": "unavailable", - "EV_VEHICLE_TIME_MINUTE": "unavailable", - "EV_VEHICLE_TIME_SECOND": "unavailable", + "EV_TIME_TO_FULLY_CHARGED_UTC": "unavailable", "EXT_EXTERNAL_TEMP": "unavailable", "ODOMETER": "unavailable", "POSITION_HEADING_DEGREE": "unavailable", diff --git a/tests/components/subaru/conftest.py b/tests/components/subaru/conftest.py index 53bd04e7e55..492ec06c1e8 100644 --- a/tests/components/subaru/conftest.py +++ b/tests/components/subaru/conftest.py @@ -71,7 +71,8 @@ TEST_OPTIONS = { CONF_UPDATE_ENABLED: True, } -TEST_ENTITY_ID = "sensor.test_vehicle_2_odometer" +TEST_DEVICE_NAME = "test_vehicle_2" +TEST_ENTITY_ID = f"sensor.{TEST_DEVICE_NAME}_odometer" def advance_time_to_next_fetch(hass): diff --git a/tests/components/subaru/test_sensor.py b/tests/components/subaru/test_sensor.py index 6ad5e729290..3f0cb773461 100644 --- a/tests/components/subaru/test_sensor.py +++ b/tests/components/subaru/test_sensor.py @@ -1,13 +1,10 @@ """Test Subaru sensors.""" from unittest.mock import patch -from homeassistant.components.subaru.const import VEHICLE_NAME from homeassistant.components.subaru.sensor import ( API_GEN_2_SENSORS, EV_SENSORS, SAFETY_SENSORS, - SENSOR_FIELD, - SENSOR_TYPE, ) from homeassistant.util import slugify from homeassistant.util.unit_system import IMPERIAL_SYSTEM @@ -16,13 +13,14 @@ from .api_responses import ( EXPECTED_STATE_EV_IMPERIAL, EXPECTED_STATE_EV_METRIC, EXPECTED_STATE_EV_UNAVAILABLE, - TEST_VIN_2_EV, - VEHICLE_DATA, VEHICLE_STATUS_EV, ) -from .conftest import MOCK_API_FETCH, MOCK_API_GET_DATA, advance_time_to_next_fetch - -VEHICLE_NAME = VEHICLE_DATA[TEST_VIN_2_EV][VEHICLE_NAME] +from .conftest import ( + MOCK_API_FETCH, + MOCK_API_GET_DATA, + TEST_DEVICE_NAME, + advance_time_to_next_fetch, +) async def test_sensors_ev_imperial(hass, ev_entry): @@ -59,9 +57,9 @@ def _assert_data(hass, expected_state): expected_states = {} for item in sensor_list: expected_states[ - f"sensor.{slugify(f'{VEHICLE_NAME} {item[SENSOR_TYPE]}')}" - ] = expected_state[item[SENSOR_FIELD]] + f"sensor.{slugify(f'{TEST_DEVICE_NAME} {item.name}')}" + ] = expected_state[item.key] - for sensor in expected_states: + for sensor, value in expected_states.items(): actual = hass.states.get(sensor) - assert actual.state == expected_states[sensor] + assert actual.state == value From f64a4f4a954bc22e372b82816263e49bf65bce3b Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 2 Oct 2022 01:32:27 +0300 Subject: [PATCH 081/985] Bump aiowebostv to 0.2.1 (#79423) --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index 81c4d04901f..2547663be28 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -3,7 +3,7 @@ "name": "LG webOS Smart TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": ["aiowebostv==0.2.0"], + "requirements": ["aiowebostv==0.2.1"], "codeowners": ["@bendavid", "@thecode"], "ssdp": [{ "st": "urn:lge-com:service:webos-second-screen:1" }], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 11a7b3630a8..3d83ae4175e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -285,7 +285,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.2.0 +aiowebostv==0.2.1 # homeassistant.components.yandex_transport aioymaps==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 76970ca4307..50a9c82c46c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -260,7 +260,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.2.0 +aiowebostv==0.2.1 # homeassistant.components.yandex_transport aioymaps==1.2.2 From 083db97476d7e1f746f7aa3e68fe5b154fe42224 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sun, 2 Oct 2022 00:36:03 +0200 Subject: [PATCH 082/985] Sort motioneye media items in media browser (#79408) * Sort media * KEY_MEDIA_SORT_ATTR should be in const * Changes after review --- .../components/motioneye/media_source.py | 8 +++++- .../components/motioneye/test_media_source.py | 28 +++++++++---------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/motioneye/media_source.py b/homeassistant/components/motioneye/media_source.py index 85fe3985b93..20fc4359ab2 100644 --- a/homeassistant/components/motioneye/media_source.py +++ b/homeassistant/components/motioneye/media_source.py @@ -284,7 +284,13 @@ class MotionEyeMediaSource(MediaSource): sub_dirs: set[str] = set() parts = parsed_path.parts - for media in resp.get(KEY_MEDIA_LIST, []): + media_list = resp.get(KEY_MEDIA_LIST, []) + + def get_media_sort_key(media: dict) -> str: + """Get media sort key.""" + return media.get(KEY_PATH, "") + + for media in sorted(media_list, key=get_media_sort_key): if ( KEY_PATH not in media or KEY_MIME_TYPE not in media diff --git a/tests/components/motioneye/test_media_source.py b/tests/components/motioneye/test_media_source.py index 541db872b51..b4803c52a6d 100644 --- a/tests/components/motioneye/test_media_source.py +++ b/tests/components/motioneye/test_media_source.py @@ -250,6 +250,20 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "children_media_class": "video", "thumbnail": None, "children": [ + { + "title": "00-02-27.mp4", + "media_class": "video", + "media_content_type": "video/mp4", + "media_content_id": ( + "media-source://motioneye" + f"/74565ad414754616000674c87bdc876c#{device.id}#movies#" + "/2021-04-25/00-02-27.mp4" + ), + "can_play": True, + "can_expand": False, + "thumbnail": "http://movie", + "children_media_class": None, + }, { "title": "00-26-22.mp4", "media_class": "video", @@ -278,20 +292,6 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "thumbnail": "http://movie", "children_media_class": None, }, - { - "title": "00-02-27.mp4", - "media_class": "video", - "media_content_type": "video/mp4", - "media_content_id": ( - "media-source://motioneye" - f"/74565ad414754616000674c87bdc876c#{device.id}#movies#" - "/2021-04-25/00-02-27.mp4" - ), - "can_play": True, - "can_expand": False, - "thumbnail": "http://movie", - "children_media_class": None, - }, ], "not_shown": 0, } From 944d70401160ff77ccf2e6d4620f35cb82c97d57 Mon Sep 17 00:00:00 2001 From: Matrix Date: Sun, 2 Oct 2022 06:56:36 +0800 Subject: [PATCH 083/985] Fix mqtt reconnect fail when token expired (#79428) * fix mqtt reconnect fail when token expired * suggest change --- homeassistant/components/yolink/__init__.py | 14 +++++++++++++- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 3257d64d265..06e6fd6472a 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -12,7 +12,7 @@ from yolink.model import BRDP from yolink.mqtt_client import MqttClient from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow @@ -110,11 +110,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_coordinators[device.device_id] = device_coordinator hass.data[DOMAIN][entry.entry_id][ATTR_COORDINATORS] = device_coordinators await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + async def shutdown_subscription(event) -> None: + """Shutdown mqtt message subscription.""" + await yolink_mqtt_client.shutdown_home_subscription() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_subscription) + ) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + await hass.data[DOMAIN][entry.entry_id][ + ATTR_MQTT_CLIENT + ].shutdown_home_subscription() hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index 0db736938f7..665b17d9f22 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -3,7 +3,7 @@ "name": "YoLink", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yolink", - "requirements": ["yolink-api==0.0.9"], + "requirements": ["yolink-api==0.1.0"], "dependencies": ["auth", "application_credentials"], "codeowners": ["@matrixd2"], "iot_class": "cloud_push" diff --git a/requirements_all.txt b/requirements_all.txt index 3d83ae4175e..1eb0cd84056 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2580,7 +2580,7 @@ yeelight==0.7.10 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.0.9 +yolink-api==0.1.0 # homeassistant.components.youless youless-api==0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 50a9c82c46c..7f1a98c564d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1787,7 +1787,7 @@ yalexs==1.2.4 yeelight==0.7.10 # homeassistant.components.yolink -yolink-api==0.0.9 +yolink-api==0.1.0 # homeassistant.components.youless youless-api==0.16 From 50952f8a1c6dca146b58f4e4cb992f5f6e7d0972 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Sun, 2 Oct 2022 11:16:16 +1100 Subject: [PATCH 084/985] Powerview bump aiopvapi to 2.0.2 (#79274) --- homeassistant/components/hunterdouglas_powerview/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index 2dbec2aba1c..930e80733e0 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -2,7 +2,7 @@ "domain": "hunterdouglas_powerview", "name": "Hunter Douglas PowerView", "documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview", - "requirements": ["aiopvapi==2.0.1"], + "requirements": ["aiopvapi==2.0.2"], "codeowners": ["@bdraco", "@kingy444", "@trullock"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 1eb0cd84056..a8ea997b280 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -229,7 +229,7 @@ aioopenexchangerates==0.4.0 aiopulse==0.4.3 # homeassistant.components.hunterdouglas_powerview -aiopvapi==2.0.1 +aiopvapi==2.0.2 # homeassistant.components.pvpc_hourly_pricing aiopvpc==3.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f1a98c564d..a6d66a4f6c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -204,7 +204,7 @@ aioopenexchangerates==0.4.0 aiopulse==0.4.3 # homeassistant.components.hunterdouglas_powerview -aiopvapi==2.0.1 +aiopvapi==2.0.2 # homeassistant.components.pvpc_hourly_pricing aiopvpc==3.0.0 From 5055d3ff4bfa79e298e74929ed3582185fe665b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Oct 2022 14:17:45 -1000 Subject: [PATCH 085/985] Enable delete device support for iBeacon (#79339) --- homeassistant/components/ibeacon/__init__.py | 14 +++- .../components/ibeacon/coordinator.py | 8 +++ tests/components/ibeacon/test_init.py | 70 +++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 tests/components/ibeacon/test_init.py diff --git a/homeassistant/components/ibeacon/__init__.py b/homeassistant/components/ibeacon/__init__.py index bf618c4ca12..2c191211910 100644 --- a/homeassistant/components/ibeacon/__init__.py +++ b/homeassistant/components/ibeacon/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import async_get +from homeassistant.helpers.device_registry import DeviceEntry, async_get from .const import DOMAIN, PLATFORMS from .coordinator import IBeaconCoordinator @@ -22,3 +22,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data.pop(DOMAIN) return unload_ok + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + """Remove iBeacon config entry from a device.""" + coordinator: IBeaconCoordinator = hass.data[DOMAIN] + return not any( + identifier + for identifier in device_entry.identifiers + if identifier[0] == DOMAIN and coordinator.async_device_id_seen(identifier[1]) + ) diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py index 2260624558e..9979cdf4fa8 100644 --- a/homeassistant/components/ibeacon/coordinator.py +++ b/homeassistant/components/ibeacon/coordinator.py @@ -139,6 +139,14 @@ class IBeaconCoordinator: # iBeacons with random MAC addresses, fixed UUID, random major/minor self._major_minor_by_uuid: dict[str, set[tuple[int, int]]] = {} + @callback + def async_device_id_seen(self, device_id: str) -> bool: + """Return True if the device_id has been seen since boot.""" + return bool( + device_id in self._last_ibeacon_advertisement_by_unique_id + or device_id in self._last_seen_by_group_id + ) + @callback def _async_handle_unavailable( self, service_info: bluetooth.BluetoothServiceInfoBleak diff --git a/tests/components/ibeacon/test_init.py b/tests/components/ibeacon/test_init.py new file mode 100644 index 00000000000..a04799e3cd4 --- /dev/null +++ b/tests/components/ibeacon/test_init.py @@ -0,0 +1,70 @@ +"""Test the ibeacon init.""" + +import pytest + +from homeassistant.components.ibeacon.const import DOMAIN +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component + +from . import BLUECHARM_BEACON_SERVICE_INFO + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" + + +async def remove_device(ws_client, device_id, config_entry_id): + """Remove config entry from a device.""" + await ws_client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry_id, + "device_id": device_id, + } + ) + response = await ws_client.receive_json() + return response["success"] + + +async def test_device_remove_devices(hass, hass_ws_client): + """Test we can only remove a device that no longer exists.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + assert await async_setup_component(hass, "config", {}) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + inject_bluetooth_service_info(hass, BLUECHARM_BEACON_SERVICE_INFO) + await hass.async_block_till_done() + device_registry = dr.async_get(hass) + + device_entry = device_registry.async_get_device( + { + ( + DOMAIN, + "426c7565-4368-6172-6d42-6561636f6e73_3838_4949_61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + }, + {}, + ) + assert ( + await remove_device(await hass_ws_client(hass), device_entry.id, entry.entry_id) + is False + ) + dead_device_entry = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, "not_seen")}, + ) + assert ( + await remove_device( + await hass_ws_client(hass), dead_device_entry.id, entry.entry_id + ) + is True + ) From 13c8d22bafe74b3be5d039a0a12f23335195dd30 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 2 Oct 2022 00:37:00 +0000 Subject: [PATCH 086/985] [ci skip] Translation update --- .../airthings_ble/translations/he.json | 23 ++++++++++++++ .../airthings_ble/translations/ru.json | 23 ++++++++++++++ .../airvisual/translations/sensor.he.json | 3 +- .../android_ip_webcam/translations/he.json | 21 +++++++++++++ .../components/apcupsd/translations/he.json | 18 +++++++++++ .../components/apcupsd/translations/nl.json | 19 ++++++++++++ .../components/apcupsd/translations/ru.json | 26 ++++++++++++++++ .../components/awair/translations/he.json | 6 ++++ .../components/bayesian/translations/ru.json | 12 +++++++ .../components/bthome/translations/he.json | 6 ++++ .../dsmr_reader/translations/he.json | 7 +++++ .../components/ecowitt/translations/he.json | 7 +++++ .../fully_kiosk/translations/he.json | 20 ++++++++++++ .../google_sheets/translations/he.json | 3 ++ .../here_travel_time/translations/he.json | 3 +- .../components/icloud/translations/he.json | 6 ++++ .../justnimbus/translations/he.json | 19 ++++++++++++ .../components/lametric/translations/he.json | 7 +++++ .../components/led_ble/translations/he.json | 8 +++++ .../litterrobot/translations/he.json | 9 +++++- .../components/pushover/translations/he.json | 12 +++++++ .../components/qingping/translations/he.json | 5 +++ .../components/risco/translations/he.json | 14 +++++++++ .../components/schedule/translations/he.json | 8 +++++ .../components/sensor/translations/he.json | 31 +++++++++++++++++-- .../components/sensor/translations/ru.json | 10 ++++-- .../components/sensorpro/translations/he.json | 6 ++++ .../components/skybell/translations/he.json | 5 +++ .../components/switchbot/translations/he.json | 5 +++ .../components/tautulli/translations/he.json | 1 + .../thermobeacon/translations/he.json | 6 ++++ .../volvooncall/translations/he.json | 18 +++++++++++ .../yalexs_ble/translations/he.json | 16 ++++++++++ .../components/zha/translations/he.json | 22 +++++++++++++ 34 files changed, 398 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/airthings_ble/translations/he.json create mode 100644 homeassistant/components/airthings_ble/translations/ru.json create mode 100644 homeassistant/components/android_ip_webcam/translations/he.json create mode 100644 homeassistant/components/apcupsd/translations/he.json create mode 100644 homeassistant/components/apcupsd/translations/nl.json create mode 100644 homeassistant/components/apcupsd/translations/ru.json create mode 100644 homeassistant/components/bayesian/translations/ru.json create mode 100644 homeassistant/components/dsmr_reader/translations/he.json create mode 100644 homeassistant/components/ecowitt/translations/he.json create mode 100644 homeassistant/components/fully_kiosk/translations/he.json create mode 100644 homeassistant/components/justnimbus/translations/he.json create mode 100644 homeassistant/components/led_ble/translations/he.json create mode 100644 homeassistant/components/pushover/translations/he.json create mode 100644 homeassistant/components/schedule/translations/he.json create mode 100644 homeassistant/components/volvooncall/translations/he.json create mode 100644 homeassistant/components/yalexs_ble/translations/he.json diff --git a/homeassistant/components/airthings_ble/translations/he.json b/homeassistant/components/airthings_ble/translations/he.json new file mode 100644 index 00000000000..3ba358c4465 --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/he.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/ru.json b/homeassistant/components/airthings_ble/translations/ru.json new file mode 100644 index 00000000000..f12ea86e777 --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \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.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\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 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.he.json b/homeassistant/components/airvisual/translations/sensor.he.json index 5745fb051f6..86d7ee80905 100644 --- a/homeassistant/components/airvisual/translations/sensor.he.json +++ b/homeassistant/components/airvisual/translations/sensor.he.json @@ -6,7 +6,8 @@ "airvisual__pollutant_level": { "good": "\u05d8\u05d5\u05d1", "unhealthy": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0", - "unhealthy_sensitive": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d5\u05ea \u05e8\u05d2\u05d9\u05e9\u05d5\u05ea" + "unhealthy_sensitive": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d5\u05ea \u05e8\u05d2\u05d9\u05e9\u05d5\u05ea", + "very_unhealthy": "\u05de\u05d0\u05d5\u05d3 \u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0" } } } \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/he.json b/homeassistant/components/android_ip_webcam/translations/he.json new file mode 100644 index 00000000000..7d1847cdf4b --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/he.json b/homeassistant/components/apcupsd/translations/he.json new file mode 100644 index 00000000000..c3a67844fdd --- /dev/null +++ b/homeassistant/components/apcupsd/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/nl.json b/homeassistant/components/apcupsd/translations/nl.json new file mode 100644 index 00000000000..622bb5180f9 --- /dev/null +++ b/homeassistant/components/apcupsd/translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "no_status": "Er is geen status gerapporteerd van Host" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Poort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/ru.json b/homeassistant/components/apcupsd/translations/ru.json new file mode 100644 index 00000000000..a73c29d265a --- /dev/null +++ b/homeassistant/components/apcupsd/translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "no_status": "\u0425\u043e\u0441\u0442 \u043d\u0435 \u0441\u043e\u043e\u0431\u0449\u0430\u0435\u0442 \u043e \u0441\u0432\u043e\u0451\u043c \u0441\u0442\u0430\u0442\u0443\u0441\u0435." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0445\u043e\u0441\u0442\u0435, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0437\u0430\u043f\u0443\u0449\u0435\u043d apcupsd NIS." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 APC UPS Daemon \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 APC UPS Daemon \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/he.json b/homeassistant/components/awair/translations/he.json index 2494d0bbd28..56e562de0c5 100644 --- a/homeassistant/components/awair/translations/he.json +++ b/homeassistant/components/awair/translations/he.json @@ -10,6 +10,12 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { + "local_pick": { + "data": { + "device": "\u05d4\u05ea\u05e7\u05df", + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP" + } + }, "reauth": { "data": { "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4", diff --git a/homeassistant/components/bayesian/translations/ru.json b/homeassistant/components/bayesian/translations/ru.json new file mode 100644 index 00000000000..0c99a16aecf --- /dev/null +++ b/homeassistant/components/bayesian/translations/ru.json @@ -0,0 +1,12 @@ +{ + "issues": { + "manual_migration": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Bayesian \u0442\u0435\u043f\u0435\u0440\u044c \u0442\u0430\u043a\u0436\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442 \u0432\u0435\u0440\u043e\u044f\u0442\u043d\u043e\u0441\u0442\u044c, \u0435\u0441\u043b\u0438 \u043d\u0430\u0431\u043b\u044e\u0434\u0430\u0435\u043c\u044b\u0435 `to_state`, `above`, `below` \u0438\u043b\u0438 `value_template` \u043e\u0446\u0435\u043d\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u043a\u0430\u043a `False`, \u0430 \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e `True`. \u0422\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c, \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\u0443\u0431\u043b\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0431\u0438\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0437\u0435\u0440\u043a\u0430\u043b\u044c\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c \u0434\u043b\u044f `{entity}`.", + "title": "\u0414\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Bayesian \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e YAML" + }, + "no_prob_given_false": { + "description": "\u0412 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Bayesian `prob_given_false` \u0442\u0435\u043f\u0435\u0440\u044c \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0434\u043b\u044f \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0435\u0433\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043d\u0435 \u0431\u044b\u043b\u043e \u043c\u0430\u0442\u0435\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u043e\u0431\u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u044f. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0434\u043e\u0431\u0430\u0432\u044c\u0442\u0435 \u044d\u0442\u043e \u0432 \u0441\u0432\u043e\u0439 `configuration.yml` \u0434\u043b\u044f `bayesian/{entity}`. \u042d\u0442\u0438 \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0443\u0442 \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043a\u0430 \u0412\u044b \u043d\u0435 \u0441\u0434\u0435\u043b\u0430\u0435\u0442\u0435 \u044d\u0442\u043e.", + "title": "\u0414\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Bayesian \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e YAML" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/he.json b/homeassistant/components/bthome/translations/he.json index 47308062d0d..b90a366130a 100644 --- a/homeassistant/components/bthome/translations/he.json +++ b/homeassistant/components/bthome/translations/he.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { diff --git a/homeassistant/components/dsmr_reader/translations/he.json b/homeassistant/components/dsmr_reader/translations/he.json new file mode 100644 index 00000000000..d0c3523da94 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/he.json b/homeassistant/components/ecowitt/translations/he.json new file mode 100644 index 00000000000..822dcf2be14 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/he.json b/homeassistant/components/fully_kiosk/translations/he.json new file mode 100644 index 00000000000..2a59e340e54 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/he.json b/homeassistant/components/google_sheets/translations/he.json index 412a09eb52a..c2e721784b3 100644 --- a/homeassistant/components/google_sheets/translations/he.json +++ b/homeassistant/components/google_sheets/translations/he.json @@ -14,6 +14,9 @@ "step": { "pick_implementation": { "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + }, + "reauth_confirm": { + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" } } } diff --git a/homeassistant/components/here_travel_time/translations/he.json b/homeassistant/components/here_travel_time/translations/he.json index dc5eb786f67..5ddb6737e2a 100644 --- a/homeassistant/components/here_travel_time/translations/he.json +++ b/homeassistant/components/here_travel_time/translations/he.json @@ -10,7 +10,8 @@ "step": { "user": { "data": { - "api_key": "\u05de\u05e4\u05ea\u05d7 API" + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "name": "\u05e9\u05dd" } } } diff --git a/homeassistant/components/icloud/translations/he.json b/homeassistant/components/icloud/translations/he.json index 73f09385a36..eae7fa97a83 100644 --- a/homeassistant/components/icloud/translations/he.json +++ b/homeassistant/components/icloud/translations/he.json @@ -15,6 +15,12 @@ "description": "\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d4\u05d6\u05e0\u05ea \u05d1\u05e2\u05d1\u05e8 \u05e2\u05d1\u05d5\u05e8 {username} \u05d0\u05d9\u05e0\u05d4 \u05e4\u05d5\u05e2\u05dc\u05ea \u05e2\u05d5\u05d3. \u05e2\u05d3\u05db\u05df \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05d4\u05de\u05e9\u05d9\u05da \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e9\u05d9\u05dc\u05d5\u05d1 \u05d6\u05d4.", "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" }, + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, "trusted_device": { "data": { "trusted_device": "\u05de\u05db\u05e9\u05d9\u05e8 \u05de\u05d4\u05d9\u05de\u05df" diff --git a/homeassistant/components/justnimbus/translations/he.json b/homeassistant/components/justnimbus/translations/he.json new file mode 100644 index 00000000000..ec7547d5405 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "client_id": "\u05de\u05d6\u05d4\u05d4 \u05dc\u05e7\u05d5\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/he.json b/homeassistant/components/lametric/translations/he.json index 53f74430ae1..46de952d566 100644 --- a/homeassistant/components/lametric/translations/he.json +++ b/homeassistant/components/lametric/translations/he.json @@ -1,6 +1,13 @@ { "config": { "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", + "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" } } diff --git a/homeassistant/components/led_ble/translations/he.json b/homeassistant/components/led_ble/translations/he.json new file mode 100644 index 00000000000..8b78650e6fe --- /dev/null +++ b/homeassistant/components/led_ble/translations/he.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/he.json b/homeassistant/components/litterrobot/translations/he.json index 454b7e1ae51..d6636c6f865 100644 --- a/homeassistant/components/litterrobot/translations/he.json +++ b/homeassistant/components/litterrobot/translations/he.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", @@ -9,6 +10,12 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/pushover/translations/he.json b/homeassistant/components/pushover/translations/he.json new file mode 100644 index 00000000000..9cdb8c5afcd --- /dev/null +++ b/homeassistant/components/pushover/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/he.json b/homeassistant/components/qingping/translations/he.json index 47308062d0d..de780eb221a 100644 --- a/homeassistant/components/qingping/translations/he.json +++ b/homeassistant/components/qingping/translations/he.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { diff --git a/homeassistant/components/risco/translations/he.json b/homeassistant/components/risco/translations/he.json index 08c5ec7d4c0..926afdf8abf 100644 --- a/homeassistant/components/risco/translations/he.json +++ b/homeassistant/components/risco/translations/he.json @@ -9,6 +9,20 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { + "cloud": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "pin": "\u05e7\u05d5\u05d3 PIN", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "local": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "pin": "\u05e7\u05d5\u05d3 PIN", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/schedule/translations/he.json b/homeassistant/components/schedule/translations/he.json new file mode 100644 index 00000000000..1a4191f20fc --- /dev/null +++ b/homeassistant/components/schedule/translations/he.json @@ -0,0 +1,8 @@ +{ + "state": { + "_": { + "on": "\u05de\u05d5\u05e4\u05e2\u05dc" + } + }, + "title": "\u05dc\u05d5\u05d7 \u05d6\u05de\u05e0\u05d9\u05dd" +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/he.json b/homeassistant/components/sensor/translations/he.json index fc0ba9b48c4..6adc9a6da87 100644 --- a/homeassistant/components/sensor/translations/he.json +++ b/homeassistant/components/sensor/translations/he.json @@ -4,23 +4,45 @@ "is_apparent_power": "\u05d4\u05e2\u05d5\u05e6\u05de\u05d4 \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name} \u05de\u05e1\u05ea\u05de\u05e0\u05ea", "is_battery_level": "\u05e8\u05de\u05ea \u05d4\u05e1\u05d5\u05dc\u05dc\u05d4 \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea \u05e9\u05dc {entity_name}", "is_current": "\u05db\u05e2\u05ea {entity_name}", + "is_distance": "\u05de\u05e8\u05d7\u05e7 \u05e0\u05d5\u05db\u05d7\u05d9 {entity_name}", "is_energy": "\u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name}", + "is_frequency": "\u05ea\u05d3\u05e8 \u05e0\u05d5\u05db\u05d7\u05d9 {entity_name}", "is_gas": "\u05db\u05e2\u05ea {entity_name} \u05d2\u05d6", + "is_humidity": "\u05dc\u05d7\u05d5\u05ea \u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name}", "is_illuminance": "\u05e2\u05d5\u05e6\u05de\u05ea \u05d4\u05d0\u05e8\u05d4 {entity_name} \u05e0\u05d5\u05db\u05d7\u05d9\u05ea", + "is_moisture": "\u05d4\u05dc\u05d7\u05d5\u05ea \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name}", + "is_nitrogen_dioxide": "\u05e8\u05de\u05ea \u05e8\u05d9\u05db\u05d5\u05d6 \u05d4\u05d7\u05e0\u05e7\u05df \u05d4\u05d3\u05d5-\u05d7\u05de\u05e6\u05e0\u05d9 \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name}", + "is_nitrogen_monoxide": "\u05e8\u05de\u05ea \u05e8\u05d9\u05db\u05d5\u05d6 \u05d4\u05d7\u05e0\u05e7\u05df \u05d7\u05d3 \u05d7\u05de\u05e6\u05e0\u05d9 {entity_name} \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea", + "is_nitrous_oxide": "\u05e8\u05de\u05ea \u05e8\u05d9\u05db\u05d5\u05d6 \u05ea\u05d7\u05de\u05d5\u05e6\u05ea \u05d4\u05d7\u05e0\u05e7\u05df \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name}", + "is_ozone": "\u05e8\u05de\u05ea \u05e8\u05d9\u05db\u05d5\u05d6 \u05d4\u05d0\u05d5\u05d6\u05d5\u05df \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name}", "is_pm1": "\u05e8\u05de\u05ea \u05d4\u05e8\u05d9\u05db\u05d5\u05d6 \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name} PM1", + "is_pm10": "\u05e8\u05de\u05ea \u05e8\u05d9\u05db\u05d5\u05d6 \u05d6\u05e8\u05dd {entity_name} PM10", + "is_pm25": "\u05e8\u05de\u05ea \u05e8\u05d9\u05db\u05d5\u05d6 \u05d6\u05e8\u05dd {entity_name} PM2.5", + "is_power": "\u05db\u05d5\u05d7 \u05e0\u05d5\u05db\u05d7\u05d9 {entity_name}", + "is_pressure": "\u05dc\u05d7\u05e5 \u05e0\u05d5\u05db\u05d7\u05d9 {entity_name}", "is_reactive_power": "\u05d4\u05e1\u05e4\u05e7 \u05ea\u05d2\u05d5\u05d1\u05ea\u05d9 \u05e0\u05d5\u05db\u05d7\u05d9 {entity_name}", - "is_temperature": "\u05db\u05e2\u05ea {entity_name} \u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4" + "is_signal_strength": "\u05e2\u05d5\u05e6\u05de\u05ea \u05d4\u05d0\u05d5\u05ea \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name}", + "is_speed": "\u05de\u05d4\u05d9\u05e8\u05d5\u05ea \u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name}", + "is_sulphur_dioxide": "\u05e8\u05de\u05ea \u05e8\u05d9\u05db\u05d5\u05d6 \u05d4\u05d2\u05d5\u05e4\u05e8\u05d9\u05ea \u05d4\u05d3\u05d5-\u05d7\u05de\u05e6\u05e0\u05d9\u05ea \u05e9\u05dc \u05d6\u05e8\u05dd {entity_name}", + "is_temperature": "\u05db\u05e2\u05ea {entity_name} \u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4", + "is_value": "\u05e2\u05e8\u05da \u05e0\u05d5\u05db\u05d7\u05d9 {entity_name}", + "is_volatile_organic_compounds": "\u05e8\u05de\u05ea \u05e8\u05d9\u05db\u05d5\u05d6 \u05d4\u05ea\u05e8\u05db\u05d5\u05d1\u05d5\u05ea \u05d4\u05d0\u05d5\u05e8\u05d2\u05e0\u05d9\u05d5\u05ea \u05d4\u05e0\u05d3\u05d9\u05e4\u05d5\u05ea \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05d5\u05ea {entity_name}", + "is_volume": "\u05e0\u05e4\u05d7 \u05e0\u05d5\u05db\u05d7\u05d9 \u05e9\u05dc {entity_name}", + "is_weight": "\u05de\u05e9\u05e7\u05dc \u05e0\u05d5\u05db\u05d7\u05d9 {entity_name}" }, "trigger_type": { "apparent_power": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d4\u05e1\u05e4\u05e7 \u05dc\u05db\u05d0\u05d5\u05e8\u05d4", "battery_level": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05de\u05ea \u05d4\u05e1\u05d5\u05dc\u05dc\u05d4", "current": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05e0\u05d5\u05db\u05d7\u05d9\u05d9\u05dd", + "distance": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05de\u05e8\u05d7\u05e7", "energy": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d0\u05e0\u05e8\u05d2\u05d9\u05d4", "frequency": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05ea\u05d3\u05e8\u05d9\u05dd", "gas": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d2\u05d6", "humidity": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05dc\u05d7\u05d5\u05ea", "illuminance": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05e2\u05d5\u05e6\u05de\u05ea \u05d4\u05d0\u05e8\u05d4", "nitrogen_dioxide": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 \u05d4\u05d7\u05e0\u05e7\u05df \u05d4\u05d3\u05d5-\u05d7\u05de\u05e6\u05e0\u05d9", + "nitrogen_monoxide": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 \u05d7\u05d3 \u05ea\u05d7\u05de\u05d5\u05e6\u05ea \u05d4\u05d7\u05e0\u05e7\u05df", + "nitrous_oxide": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 \u05ea\u05d7\u05de\u05d5\u05e6\u05ea \u05d4\u05d7\u05e0\u05e7\u05df", "ozone": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 \u05d4\u05d0\u05d5\u05d6\u05d5\u05df", "pm1": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 PM1", "pm10": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 PM10", @@ -29,9 +51,14 @@ "power_factor": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05d2\u05d5\u05e8\u05dd \u05d4\u05d4\u05e1\u05e4\u05e7", "pressure": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05dc\u05d7\u05e5", "reactive_power": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d4\u05e1\u05e4\u05e7 \u05ea\u05d2\u05d5\u05d1\u05ea\u05d9", + "signal_strength": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e2\u05d5\u05e6\u05de\u05ea \u05d4\u05d0\u05d5\u05ea", + "speed": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05de\u05d4\u05d9\u05e8\u05d5\u05ea", + "sulphur_dioxide": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 \u05d3\u05d5 \u05ea\u05d7\u05de\u05d5\u05e6\u05ea \u05d4\u05d2\u05d5\u05e4\u05e8\u05d9\u05ea", "temperature": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4", "value": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05e2\u05e8\u05da", - "voltage": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05de\u05ea\u05d7" + "voltage": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05de\u05ea\u05d7", + "volume": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05e0\u05e4\u05d7", + "weight": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05de\u05e9\u05e7\u05dc" } }, "state": { diff --git a/homeassistant/components/sensor/translations/ru.json b/homeassistant/components/sensor/translations/ru.json index af4d66b631f..9b7a1f7dbe8 100644 --- a/homeassistant/components/sensor/translations/ru.json +++ b/homeassistant/components/sensor/translations/ru.json @@ -25,11 +25,14 @@ "is_pressure": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_reactive_power": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_signal_strength": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_speed": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_sulphur_dioxide": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0434\u0438\u043e\u043a\u0441\u0438\u0434\u0430 \u0441\u0435\u0440\u044b", "is_temperature": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_value": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_volatile_organic_compounds": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u043b\u0435\u0442\u0443\u0447\u0438\u0445 \u043e\u0440\u0433\u0430\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u0432\u0435\u0449\u0435\u0441\u0442\u0432", - "is_voltage": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0430\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f" + "is_voltage": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0430\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f", + "is_volume": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_weight": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435" }, "trigger_type": { "apparent_power": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043f\u043e\u043b\u043d\u043e\u0439 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", @@ -56,11 +59,14 @@ "pressure": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "reactive_power": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0440\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0439 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", "signal_strength": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "speed": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c", "sulphur_dioxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0434\u0438\u043e\u043a\u0441\u0438\u0434\u0430 \u0441\u0435\u0440\u044b", "temperature": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "value": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "volatile_organic_compounds": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u043b\u0435\u0442\u0443\u0447\u0438\u0445 \u043e\u0440\u0433\u0430\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u0432\u0435\u0449\u0435\u0441\u0442\u0432", - "voltage": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0430\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f" + "voltage": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0430\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f", + "volume": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u043e\u0431\u044a\u0451\u043c", + "weight": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0432\u0435\u0441" } }, "state": { diff --git a/homeassistant/components/sensorpro/translations/he.json b/homeassistant/components/sensorpro/translations/he.json index 47308062d0d..b182a698234 100644 --- a/homeassistant/components/sensorpro/translations/he.json +++ b/homeassistant/components/sensorpro/translations/he.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "not_supported": "\u05d4\u05ea\u05e7\u05df \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { diff --git a/homeassistant/components/skybell/translations/he.json b/homeassistant/components/skybell/translations/he.json index 21b9822e248..0e3ced77bc3 100644 --- a/homeassistant/components/skybell/translations/he.json +++ b/homeassistant/components/skybell/translations/he.json @@ -10,6 +10,11 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + }, "user": { "data": { "email": "\u05d3\u05d5\u05d0\"\u05dc", diff --git a/homeassistant/components/switchbot/translations/he.json b/homeassistant/components/switchbot/translations/he.json index 7f7974024b1..b4cb968ff23 100644 --- a/homeassistant/components/switchbot/translations/he.json +++ b/homeassistant/components/switchbot/translations/he.json @@ -7,6 +7,11 @@ }, "flow_title": "{name} ({address})", "step": { + "password": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + }, "user": { "data": { "name": "\u05e9\u05dd", diff --git a/homeassistant/components/tautulli/translations/he.json b/homeassistant/components/tautulli/translations/he.json index 80d0bba902b..7091be81520 100644 --- a/homeassistant/components/tautulli/translations/he.json +++ b/homeassistant/components/tautulli/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, diff --git a/homeassistant/components/thermobeacon/translations/he.json b/homeassistant/components/thermobeacon/translations/he.json index 47308062d0d..b182a698234 100644 --- a/homeassistant/components/thermobeacon/translations/he.json +++ b/homeassistant/components/thermobeacon/translations/he.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "not_supported": "\u05d4\u05ea\u05e7\u05df \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { diff --git a/homeassistant/components/volvooncall/translations/he.json b/homeassistant/components/volvooncall/translations/he.json new file mode 100644 index 00000000000..6f2cdbf82e1 --- /dev/null +++ b/homeassistant/components/volvooncall/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/he.json b/homeassistant/components/yalexs_ble/translations/he.json new file mode 100644 index 00000000000..a447b36c3ec --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/he.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "invalid_key_format": "\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05dc\u05d0 \u05de\u05e7\u05d5\u05d5\u05df \u05d7\u05d9\u05d9\u05d1 \u05dc\u05d4\u05d9\u05d5\u05ea \u05de\u05d7\u05e8\u05d5\u05d6\u05ea \u05d4\u05e7\u05e1\u05d3\u05e6\u05d9\u05de\u05dc\u05d9\u05ea \u05e9\u05dc 32 \u05d1\u05ea\u05d9\u05dd.", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/he.json b/homeassistant/components/zha/translations/he.json index fa40de672e2..16f25bf00d7 100644 --- a/homeassistant/components/zha/translations/he.json +++ b/homeassistant/components/zha/translations/he.json @@ -11,6 +11,12 @@ "port_config": { "title": "\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea" }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "\u05d4\u05e2\u05dc\u05d0\u05ea \u05e7\u05d5\u05d1\u05e5" + }, + "title": "\u05d4\u05e2\u05dc\u05d0\u05ea \u05d2\u05d9\u05d1\u05d5\u05d9 \u05d9\u05d3\u05e0\u05d9" + }, "user": { "title": "ZHA" } @@ -46,5 +52,21 @@ "device_dropped": "\u05d4\u05d4\u05ea\u05e7\u05df \u05d4\u05d5\u05e9\u05de\u05d8", "device_offline": "\u05d4\u05ea\u05e7\u05df \u05dc\u05d0 \u05de\u05e7\u05d5\u05d5\u05df" } + }, + "options": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "init": { + "title": "\u05d4\u05d2\u05d3\u05e8\u05d4 \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc ZHA" + }, + "upload_manual_backup": { + "data": { + "uploaded_backup_file": "\u05d4\u05e2\u05dc\u05d0\u05ea \u05e7\u05d5\u05d1\u05e5" + }, + "title": "\u05d4\u05e2\u05dc\u05d0\u05ea \u05d2\u05d9\u05d1\u05d5\u05d9 \u05d9\u05d3\u05e0\u05d9" + } + } } } \ No newline at end of file From d03553bbf0ca921ba1bc4c1c4867c5f39320508e Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 1 Oct 2022 17:42:11 -0700 Subject: [PATCH 087/985] Address Google Sheets PR feedback (#78889) --- .../components/google_sheets/__init__.py | 12 +-- .../components/google_sheets/config_flow.py | 35 +++---- .../components/google_sheets/strings.json | 4 + .../google_sheets/test_config_flow.py | 63 ++++++++++++ tests/components/google_sheets/test_init.py | 96 ++++++++++++++++--- 5 files changed, 172 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/google_sheets/__init__.py b/homeassistant/components/google_sheets/__init__.py index ea96288371c..e211693bf21 100644 --- a/homeassistant/components/google_sheets/__init__.py +++ b/homeassistant/components/google_sheets/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import datetime -from typing import cast import aiohttp from google.auth.exceptions import RefreshError @@ -105,12 +104,13 @@ async def async_setup_service(hass: HomeAssistant) -> None: async def append_to_sheet(call: ServiceCall) -> None: """Append new line of data to a Google Sheets document.""" - - entry = cast( - ConfigEntry, - hass.config_entries.async_get_entry(call.data[DATA_CONFIG_ENTRY]), + entry: ConfigEntry | None = hass.config_entries.async_get_entry( + call.data[DATA_CONFIG_ENTRY] ) - session: OAuth2Session = hass.data[DOMAIN][entry.entry_id] + if not entry: + raise ValueError(f"Invalid config entry: {call.data[DATA_CONFIG_ENTRY]}") + if not (session := hass.data[DOMAIN].get(entry.entry_id)): + raise ValueError(f"Config entry not loaded: {call.data[DATA_CONFIG_ENTRY]}") await session.async_ensure_token_valid() await hass.async_add_executor_job(_append_to_sheet, call, entry) diff --git a/homeassistant/components/google_sheets/config_flow.py b/homeassistant/components/google_sheets/config_flow.py index d19a5b5c3fa..3805ee9d38b 100644 --- a/homeassistant/components/google_sheets/config_flow.py +++ b/homeassistant/components/google_sheets/config_flow.py @@ -8,7 +8,7 @@ from typing import Any from google.oauth2.credentials import Credentials from gspread import Client, GSpreadException -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow @@ -25,6 +25,8 @@ class OAuth2FlowHandler( DOMAIN = DOMAIN + reauth_entry: ConfigEntry | None = None + @property def logger(self) -> logging.Logger: """Return logger.""" @@ -42,6 +44,9 @@ class OAuth2FlowHandler( async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( @@ -52,40 +57,27 @@ class OAuth2FlowHandler( return self.async_show_form(step_id="reauth_confirm") return await self.async_step_user() - def _async_reauth_entry(self) -> ConfigEntry | None: - """Return existing entry for reauth.""" - if self.source != SOURCE_REAUTH or not ( - entry_id := self.context.get("entry_id") - ): - return None - return next( - ( - entry - for entry in self._async_current_entries() - if entry.entry_id == entry_id - ), - None, - ) - async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Create an entry for the flow, or update existing entry.""" service = Client(Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN])) - if entry := self._async_reauth_entry(): + if self.reauth_entry: _LOGGER.debug("service.open_by_key") try: await self.hass.async_add_executor_job( service.open_by_key, - entry.unique_id, + self.reauth_entry.unique_id, ) except GSpreadException as err: _LOGGER.error( - "Could not find spreadsheet '%s': %s", entry.unique_id, str(err) + "Could not find spreadsheet '%s': %s", + self.reauth_entry.unique_id, + str(err), ) return self.async_abort(reason="open_spreadsheet_failure") - self.hass.config_entries.async_update_entry(entry, data=data) - await self.hass.config_entries.async_reload(entry.entry_id) + self.hass.config_entries.async_update_entry(self.reauth_entry, data=data) + await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) return self.async_abort(reason="reauth_successful") try: @@ -97,6 +89,7 @@ class OAuth2FlowHandler( return self.async_abort(reason="create_spreadsheet_failure") await self.async_set_unique_id(doc.id) + self._abort_if_unique_id_configured() return self.async_create_entry( title=DEFAULT_NAME, data=data, description_placeholders={"url": doc.url} ) diff --git a/homeassistant/components/google_sheets/strings.json b/homeassistant/components/google_sheets/strings.json index 2170f6e4c1d..33230038cdf 100644 --- a/homeassistant/components/google_sheets/strings.json +++ b/homeassistant/components/google_sheets/strings.json @@ -10,6 +10,10 @@ }, "auth": { "title": "Link Google Account" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Google Sheets integration needs to re-authenticate your account" } }, "abort": { diff --git a/tests/components/google_sheets/test_config_flow.py b/tests/components/google_sheets/test_config_flow.py index 3fcd2f99ed0..e74602dc8a1 100644 --- a/tests/components/google_sheets/test_config_flow.py +++ b/tests/components/google_sheets/test_config_flow.py @@ -312,3 +312,66 @@ async def test_reauth_abort( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result.get("type") == "abort" assert result.get("reason") == "open_spreadsheet_failure" + + +async def test_already_configured( + hass: HomeAssistant, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, + setup_credentials, + mock_client, +) -> None: + """Test case where config flow discovers unique id was already configured.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=SHEET_ID, + data={ + "token": { + "access_token": "mock-access-token", + }, + }, + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + "google_sheets", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" + "&access_type=offline&prompt=consent" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + # Prepare fake client library response when creating the sheet + mock_create = Mock() + mock_create.return_value.id = SHEET_ID + mock_client.return_value.create = mock_create + + aioclient_mock.post( + oauth2client.GOOGLE_TOKEN_URI, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result.get("type") == "abort" + assert result.get("reason") == "already_configured" diff --git a/tests/components/google_sheets/test_init.py b/tests/components/google_sheets/test_init.py index c32eb345534..f77edcbb491 100644 --- a/tests/components/google_sheets/test_init.py +++ b/tests/components/google_sheets/test_init.py @@ -14,6 +14,7 @@ from homeassistant.components.application_credentials import ( from homeassistant.components.google_sheets import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceNotFound from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -75,16 +76,6 @@ async def mock_setup_integration( yield func - # Verify clean unload - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - await hass.config_entries.async_unload(entries[0].entry_id) - await hass.async_block_till_done() - assert not len(hass.services.async_services().get(DOMAIN, {})) - - assert not hass.data.get(DOMAIN) - assert entries[0].state is ConfigEntryState.NOT_LOADED - async def test_setup_success( hass: HomeAssistant, setup_integration: ComponentSetup @@ -96,6 +87,13 @@ async def test_setup_success( assert len(entries) == 1 assert entries[0].state is ConfigEntryState.LOADED + await hass.config_entries.async_unload(entries[0].entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert entries[0].state is ConfigEntryState.NOT_LOADED + assert not len(hass.services.async_services().get(DOMAIN, {})) + @pytest.mark.parametrize( "scopes", @@ -194,7 +192,7 @@ async def test_append_sheet( setup_integration: ComponentSetup, config_entry: MockConfigEntry, ) -> None: - """Test successful setup and unload.""" + """Test service call appending to a sheet.""" await setup_integration() entries = hass.config_entries.async_entries(DOMAIN) @@ -213,3 +211,79 @@ async def test_append_sheet( blocking=True, ) assert len(mock_client.mock_calls) == 8 + + +async def test_append_sheet_invalid_config_entry( + hass: HomeAssistant, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, + expires_at: int, + scopes: list[str], +) -> None: + """Test service call with invalid config entries.""" + config_entry2 = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_SHEET_ID + "2", + data={ + "auth_implementation": DOMAIN, + "token": { + "access_token": "mock-access-token", + "refresh_token": "mock-refresh-token", + "expires_at": expires_at, + "scope": " ".join(scopes), + }, + }, + ) + config_entry2.add_to_hass(hass) + + await setup_integration() + + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry2.state is ConfigEntryState.LOADED + + # Exercise service call on a config entry that does not exist + with pytest.raises(ValueError, match="Invalid config entry"): + await hass.services.async_call( + DOMAIN, + "append_sheet", + { + "config_entry": config_entry.entry_id + "XXX", + "worksheet": "Sheet1", + "data": {"foo": "bar"}, + }, + blocking=True, + ) + + # Unload the config entry invoke the service on the unloaded entry id + await hass.config_entries.async_unload(config_entry2.entry_id) + await hass.async_block_till_done() + assert config_entry2.state is ConfigEntryState.NOT_LOADED + + with pytest.raises(ValueError, match="Config entry not loaded"): + await hass.services.async_call( + DOMAIN, + "append_sheet", + { + "config_entry": config_entry2.entry_id, + "worksheet": "Sheet1", + "data": {"foo": "bar"}, + }, + blocking=True, + ) + + # Unloading the other config entry will de-register the service + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.NOT_LOADED + + with pytest.raises(ServiceNotFound): + await hass.services.async_call( + DOMAIN, + "append_sheet", + { + "config_entry": config_entry.entry_id, + "worksheet": "Sheet1", + "data": {"foo": "bar"}, + }, + blocking=True, + ) From dac60990ee037e0624d11e9795db4d458019de5a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Oct 2022 14:42:54 -1000 Subject: [PATCH 088/985] Ensure bluetooth disconnect callback fires if esphome config entry is reloaded (#79389) --- .../components/esphome/bluetooth/client.py | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 8e8d7cf6427..14924756074 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -15,10 +15,9 @@ from bleak.backends.device import BLEDevice from bleak.backends.service import BleakGATTServiceCollection from bleak.exc import BleakError -from homeassistant.core import CALLBACK_TYPE, async_get_hass, callback as hass_callback +from homeassistant.core import CALLBACK_TYPE, async_get_hass from ..domain_data import DomainData -from ..entry_data import RuntimeEntryData from .characteristic import BleakGATTCharacteristicESPHome from .descriptor import BleakGATTDescriptorESPHome from .service import BleakGATTServiceESPHome @@ -85,7 +84,9 @@ class ESPHomeClient(BaseBleakClient): assert self._ble_device.details is not None self._source = self._ble_device.details["source"] self.domain_data = DomainData.get(async_get_hass()) - self._client = self._async_get_entry_data().client + config_entry = self.domain_data.get_by_unique_id(self._source) + self.entry_data = self.domain_data.get_entry_data(config_entry) + self._client = self.entry_data.client self._is_connected = False self._mtu: int | None = None self._cancel_connection_state: CALLBACK_TYPE | None = None @@ -108,12 +109,6 @@ class ESPHomeClient(BaseBleakClient): ) self._cancel_connection_state = None - @hass_callback - def _async_get_entry_data(self) -> RuntimeEntryData: - """Get the entry data.""" - config_entry = self.domain_data.get_by_unique_id(self._source) - return self.domain_data.get_entry_data(config_entry) - def _async_ble_device_disconnected(self) -> None: """Handle the BLE device disconnecting from the ESP.""" _LOGGER.debug("%s: BLE device disconnected", self._source) @@ -125,8 +120,7 @@ class ESPHomeClient(BaseBleakClient): def _async_esp_disconnected(self) -> None: """Handle the esp32 client disconnecting from hass.""" _LOGGER.debug("%s: ESP device disconnected", self._source) - entry_data = self._async_get_entry_data() - entry_data.disconnect_callbacks.remove(self._async_esp_disconnected) + self.entry_data.disconnect_callbacks.remove(self._async_esp_disconnected) self._async_ble_device_disconnected() def _async_call_bleak_disconnected_callback(self) -> None: @@ -179,8 +173,7 @@ class ESPHomeClient(BaseBleakClient): connected_future.set_exception(BleakError("Disconnected")) return - entry_data = self._async_get_entry_data() - entry_data.disconnect_callbacks.append(self._async_esp_disconnected) + self.entry_data.disconnect_callbacks.append(self._async_esp_disconnected) connected_future.set_result(connected) timeout = kwargs.get("timeout", self._timeout) @@ -203,14 +196,13 @@ class ESPHomeClient(BaseBleakClient): async def _wait_for_free_connection_slot(self, timeout: float) -> None: """Wait for a free connection slot.""" - entry_data = self._async_get_entry_data() - if entry_data.ble_connections_free: + if self.entry_data.ble_connections_free: return _LOGGER.debug( "%s: Out of connection slots, waiting for a free one", self._source ) async with async_timeout.timeout(timeout): - await entry_data.wait_for_ble_connections_free() + await self.entry_data.wait_for_ble_connections_free() @property def is_connected(self) -> bool: From a3cd03b70b6b28b986b3a8c4b3347f9b7aaa2715 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 2 Oct 2022 02:43:15 +0200 Subject: [PATCH 089/985] Fix checking of upgrade API availability during setup of Synology DSM integration (#79435) --- homeassistant/components/synology_dsm/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 019108c3230..f57262e2a57 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -110,7 +110,7 @@ class SynoApi: # check if upgrade is available try: self.dsm.upgrade.update() - except SynologyDSMAPIErrorException as ex: + except SYNOLOGY_CONNECTION_EXCEPTIONS as ex: self._with_upgrade = False self.dsm.reset(SynoCoreUpgrade.API_KEY) LOGGER.debug("Disabled fetching upgrade data during setup: %s", ex) From d9191cf2f2db3198548cf8ddd52f6d45a6131365 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Oct 2022 14:45:01 -1000 Subject: [PATCH 090/985] Bump dbus-fast to 1.18.0 (#79440) Changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.17.0...v1.18.0 --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 9a2f1f0e901..e4563038569 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.5.2", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.17.0" + "dbus-fast==1.18.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 38b6ee1e52b..4dfd94862a2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.17.0 +dbus-fast==1.18.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index a8ea997b280..3eb0b8fb519 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -543,7 +543,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.17.0 +dbus-fast==1.18.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6d66a4f6c1..15ac32d3c2f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.17.0 +dbus-fast==1.18.0 # homeassistant.components.debugpy debugpy==1.6.3 From ebc2a751d25bc88a436ec196c1e44f0b97e3d5d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Oct 2022 14:48:09 -1000 Subject: [PATCH 091/985] Bump ibeacon-ble to 0.7.3 (#79443) --- homeassistant/components/ibeacon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ibeacon/manifest.json b/homeassistant/components/ibeacon/manifest.json index 7b4110a7fe4..a2b55a69403 100644 --- a/homeassistant/components/ibeacon/manifest.json +++ b/homeassistant/components/ibeacon/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ibeacon", "dependencies": ["bluetooth"], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [2, 21] }], - "requirements": ["ibeacon_ble==0.7.2"], + "requirements": ["ibeacon_ble==0.7.3"], "codeowners": ["@bdraco"], "iot_class": "local_push", "loggers": ["bleak"], diff --git a/requirements_all.txt b/requirements_all.txt index 3eb0b8fb519..3f7eaa0d909 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -901,7 +901,7 @@ iammeter==0.1.7 iaqualink==0.4.1 # homeassistant.components.ibeacon -ibeacon_ble==0.7.2 +ibeacon_ble==0.7.3 # homeassistant.components.watson_tts ibm-watson==5.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 15ac32d3c2f..ed1da50091b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -672,7 +672,7 @@ hyperion-py==0.7.5 iaqualink==0.4.1 # homeassistant.components.ibeacon -ibeacon_ble==0.7.2 +ibeacon_ble==0.7.3 # homeassistant.components.ping icmplib==3.0 From 3e411935bbe07ebe0e7a9f5323734448486d75d7 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 2 Oct 2022 03:11:54 +0200 Subject: [PATCH 092/985] Fix Netatmo scope issue with HA cloud (#79437) Co-authored-by: Paulus Schoutsen --- homeassistant/components/netatmo/__init__.py | 14 ++++++++-- .../components/netatmo/config_flow.py | 9 ++++++- tests/components/netatmo/test_camera.py | 26 +++++++++---------- tests/components/netatmo/test_climate.py | 24 ++++++++--------- tests/components/netatmo/test_cover.py | 2 +- tests/components/netatmo/test_init.py | 2 +- tests/components/netatmo/test_light.py | 6 ++--- tests/components/netatmo/test_select.py | 2 +- tests/components/netatmo/test_sensor.py | 8 +++--- tests/components/netatmo/test_switch.py | 2 +- 10 files changed, 56 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 65b321d25aa..eb0e93c4b38 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -137,9 +137,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryAuthFailed("Token not valid, trigger renewal") from ex raise ConfigEntryNotReady from ex - if sorted(session.token["scope"]) != sorted(NETATMO_SCOPES): + if entry.data["auth_implementation"] == cloud.DOMAIN: + required_scopes = { + scope + for scope in NETATMO_SCOPES + if scope not in ("access_doorbell", "read_doorbell") + } + else: + required_scopes = set(NETATMO_SCOPES) + + if not (set(session.token["scope"]) & required_scopes): _LOGGER.debug( - "Scope is invalid: %s != %s", session.token["scope"], NETATMO_SCOPES + "Session is missing scopes: %s", + required_scopes - set(session.token["scope"]), ) raise ConfigEntryAuthFailed("Token scope not valid, trigger renewal") diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index 99fa195b118..acd8965d013 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -54,7 +54,14 @@ class NetatmoFlowHandler( @property def extra_authorize_data(self) -> dict: """Extra data that needs to be appended to the authorize url.""" - return {"scope": " ".join(ALL_SCOPES)} + exclude = [] + if self.flow_impl.name == "Home Assistant Cloud": + exclude = ["access_doorbell", "read_doorbell"] + + scopes = [scope for scope in ALL_SCOPES if scope not in exclude] + scopes.sort() + + return {"scope": " ".join(scopes)} async def async_step_user(self, user_input: dict | None = None) -> FlowResult: """Handle a flow start.""" diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index ea39497ce58..027b0907d50 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -25,7 +25,7 @@ from tests.common import async_capture_events, async_fire_time_changed async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): """Test setup with webhook.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -132,7 +132,7 @@ IMAGE_BYTES_FROM_STREAM = b"test stream image bytes" async def test_camera_image_local(hass, config_entry, requests_mock, netatmo_auth): """Test retrieval or local camera image.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -158,7 +158,7 @@ async def test_camera_image_local(hass, config_entry, requests_mock, netatmo_aut async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth): """Test retrieval of remote camera image.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -182,7 +182,7 @@ async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth) async def test_service_set_person_away(hass, config_entry, netatmo_auth): """Test service to set person as away.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -219,7 +219,7 @@ async def test_service_set_person_away(hass, config_entry, netatmo_auth): async def test_service_set_person_away_invalid_person(hass, config_entry, netatmo_auth): """Test service to set invalid person as away.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -247,7 +247,7 @@ async def test_service_set_persons_home_invalid_person( ): """Test service to set invalid persons as home.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -273,7 +273,7 @@ async def test_service_set_persons_home_invalid_person( async def test_service_set_persons_home(hass, config_entry, netatmo_auth): """Test service to set persons as home.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -297,7 +297,7 @@ async def test_service_set_persons_home(hass, config_entry, netatmo_auth): async def test_service_set_camera_light(hass, config_entry, netatmo_auth): """Test service to set the outdoor camera light mode.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -327,7 +327,7 @@ async def test_service_set_camera_light(hass, config_entry, netatmo_auth): async def test_service_set_camera_light_invalid_type(hass, config_entry, netatmo_auth): """Test service to set the indoor camera light mode.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -377,7 +377,7 @@ async def test_camera_reconnect_webhook(hass, config_entry): mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() mock_webhook.return_value = "https://example.com" - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -412,7 +412,7 @@ async def test_camera_reconnect_webhook(hass, config_entry): async def test_webhook_person_event(hass, config_entry, netatmo_auth): """Test that person events are handled.""" with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -469,7 +469,7 @@ async def test_setup_component_no_devices(hass, config_entry): mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert fake_post_hits == 9 @@ -508,7 +508,7 @@ async def test_camera_image_raises_exception(hass, config_entry, requests_mock): mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() camera_entity_indoor = "camera.hall" diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index cc23dc887bd..d37bab929e1 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -27,7 +27,7 @@ from .common import selected_platforms, simulate_webhook async def test_webhook_event_handling_thermostats(hass, config_entry, netatmo_auth): """Test service and webhook event handling with thermostats.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -204,7 +204,7 @@ async def test_service_preset_mode_frost_guard_thermostat( ): """Test service with frost guard preset for thermostats.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -277,7 +277,7 @@ async def test_service_preset_mode_frost_guard_thermostat( async def test_service_preset_modes_thermostat(hass, config_entry, netatmo_auth): """Test service with preset modes for thermostats.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -356,7 +356,7 @@ async def test_service_preset_modes_thermostat(hass, config_entry, netatmo_auth) async def test_webhook_event_handling_no_data(hass, config_entry, netatmo_auth): """Test service and webhook event handling with erroneous data.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -405,7 +405,7 @@ async def test_webhook_event_handling_no_data(hass, config_entry, netatmo_auth): async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_auth): """Test service for selecting Netatmo schedule with thermostats.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -458,7 +458,7 @@ async def test_service_preset_mode_already_boost_valves( ): """Test service with boost preset for valves when already in boost mode.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -536,7 +536,7 @@ async def test_service_preset_mode_already_boost_valves( async def test_service_preset_mode_boost_valves(hass, config_entry, netatmo_auth): """Test service with boost preset for valves.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -586,7 +586,7 @@ async def test_service_preset_mode_boost_valves(hass, config_entry, netatmo_auth async def test_service_preset_mode_invalid(hass, config_entry, caplog, netatmo_auth): """Test service with invalid preset.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -604,7 +604,7 @@ async def test_service_preset_mode_invalid(hass, config_entry, caplog, netatmo_a async def test_valves_service_turn_off(hass, config_entry, netatmo_auth): """Test service turn off for valves.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -654,7 +654,7 @@ async def test_valves_service_turn_off(hass, config_entry, netatmo_auth): async def test_valves_service_turn_on(hass, config_entry, netatmo_auth): """Test service turn on for valves.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -699,7 +699,7 @@ async def test_valves_service_turn_on(hass, config_entry, netatmo_auth): async def test_webhook_home_id_mismatch(hass, config_entry, netatmo_auth): """Test service turn on for valves.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -737,7 +737,7 @@ async def test_webhook_home_id_mismatch(hass, config_entry, netatmo_auth): async def test_webhook_set_point(hass, config_entry, netatmo_auth): """Test service turn on for valves.""" with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/netatmo/test_cover.py b/tests/components/netatmo/test_cover.py index c0f34f25b24..6a5709ebf8f 100644 --- a/tests/components/netatmo/test_cover.py +++ b/tests/components/netatmo/test_cover.py @@ -17,7 +17,7 @@ from .common import selected_platforms async def test_cover_setup_and_services(hass, config_entry, netatmo_auth): """Test setup and services.""" with selected_platforms(["cover"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index 373d7e19765..187a89afeb6 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -121,7 +121,7 @@ async def test_setup_component_with_config(hass, config_entry): async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): """Test setup and teardown of the netatmo component with webhook registration.""" with selected_platforms(["camera", "climate", "light", "sensor"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/netatmo/test_light.py b/tests/components/netatmo/test_light.py index ced24c738e3..b1a5270745c 100644 --- a/tests/components/netatmo/test_light.py +++ b/tests/components/netatmo/test_light.py @@ -17,7 +17,7 @@ from tests.test_util.aiohttp import AiohttpClientMockResponse async def test_camera_light_setup_and_services(hass, config_entry, netatmo_auth): """Test camera ligiht setup and services.""" with selected_platforms(["light"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -108,7 +108,7 @@ async def test_setup_component_no_devices(hass, config_entry): mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() # Fake webhook activation @@ -126,7 +126,7 @@ async def test_setup_component_no_devices(hass, config_entry): async def test_light_setup_and_services(hass, config_entry, netatmo_auth): """Test setup and services.""" with selected_platforms(["light"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/netatmo/test_select.py b/tests/components/netatmo/test_select.py index ea8e88ce8de..8053b8bdac7 100644 --- a/tests/components/netatmo/test_select.py +++ b/tests/components/netatmo/test_select.py @@ -14,7 +14,7 @@ from .common import selected_platforms, simulate_webhook async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_auth): """Test service for selecting Netatmo schedule with thermostats.""" with selected_platforms(["climate", "select"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index 99e76389c13..d3ea8fb8167 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -12,7 +12,7 @@ from .common import TEST_TIME, selected_platforms async def test_weather_sensor(hass, config_entry, netatmo_auth): """Test weather sensor setup.""" with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -27,7 +27,7 @@ async def test_weather_sensor(hass, config_entry, netatmo_auth): async def test_public_weather_sensor(hass, config_entry, netatmo_auth): """Test public weather sensor setup.""" with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -182,7 +182,7 @@ async def test_weather_sensor_enabling( suggested_object_id=name, disabled_by=None, ) - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -195,7 +195,7 @@ async def test_climate_battery_sensor(hass, config_entry, netatmo_auth): with patch("time.time", return_value=TEST_TIME), selected_platforms( ["sensor", "climate"] ): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/netatmo/test_switch.py b/tests/components/netatmo/test_switch.py index dc11ac22746..c6f9c9c514e 100644 --- a/tests/components/netatmo/test_switch.py +++ b/tests/components/netatmo/test_switch.py @@ -14,7 +14,7 @@ from .common import selected_platforms async def test_switch_setup_and_services(hass, config_entry, netatmo_auth): """Test setup and services.""" with selected_platforms(["switch"]): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From 8a73795f5082f173159f1d7b78d5a70470c23c10 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 1 Oct 2022 15:27:44 -1000 Subject: [PATCH 093/985] Bump bluetooth-adapters to 0.5.3 (#79442) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index e4563038569..9c989bfe9fa 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -8,7 +8,7 @@ "requirements": [ "bleak==0.18.1", "bleak-retry-connector==2.1.3", - "bluetooth-adapters==0.5.2", + "bluetooth-adapters==0.5.3", "bluetooth-auto-recovery==0.3.3", "dbus-fast==1.18.0" ], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4dfd94862a2..258776dde30 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ awesomeversion==22.9.0 bcrypt==3.1.7 bleak-retry-connector==2.1.3 bleak==0.18.1 -bluetooth-adapters==0.5.2 +bluetooth-adapters==0.5.3 bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3f7eaa0d909..8074756a367 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -438,7 +438,7 @@ bluemaestro-ble==0.2.0 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.5.2 +bluetooth-adapters==0.5.3 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ed1da50091b..4097f10c78b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -352,7 +352,7 @@ blinkpy==0.19.2 bluemaestro-ble==0.2.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.5.2 +bluetooth-adapters==0.5.3 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.3 From 7b8b73f82682e5d276d464d915b76130e4a2f573 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 1 Oct 2022 18:59:10 -0700 Subject: [PATCH 094/985] Update nest climate to avoid duplicate set mode commands (#79445) --- homeassistant/components/nest/climate_sdm.py | 2 ++ tests/components/nest/test_climate_sdm.py | 23 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 3113cb2dd40..bed44045c11 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -320,6 +320,8 @@ class ThermostatEntity(ClimateEntity): """Set new target preset mode.""" if preset_mode not in self.preset_modes: raise ValueError(f"Unsupported preset_mode '{preset_mode}'") + if self.preset_mode == preset_mode: # API doesn't like duplicate preset modes + return trait = self._device.traits[ThermostatEcoTrait.NAME] try: await trait.set_mode(PRESET_INV_MODE_MAP[preset_mode]) diff --git a/tests/components/nest/test_climate_sdm.py b/tests/components/nest/test_climate_sdm.py index 4ac58171fcd..ffe957a3e28 100644 --- a/tests/components/nest/test_climate_sdm.py +++ b/tests/components/nest/test_climate_sdm.py @@ -602,6 +602,29 @@ async def test_thermostat_set_eco_preset( "params": {"mode": "OFF"}, } + # Simulate the mode changing + await create_event( + { + "sdm.devices.traits.ThermostatEco": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "OFF", + }, + } + ) + + auth.method = None + auth.url = None + auth.json = None + + # Attempting to set the preset mode when already in that mode will + # not send any messages to the API (it would otherwise fail) + await common.async_set_preset_mode(hass, PRESET_NONE) + await hass.async_block_till_done() + + assert auth.method is None + assert auth.url is None + assert auth.json is None + async def test_thermostat_set_cool( hass: HomeAssistant, From 38a680c3eb8bdd7332c776765247fc48922e9598 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Sun, 2 Oct 2022 06:37:24 +0300 Subject: [PATCH 095/985] Add reauthenticaion to `mikrotik` (#74454) --- homeassistant/components/mikrotik/__init__.py | 6 +- .../components/mikrotik/config_flow.py | 45 +++++++++ homeassistant/components/mikrotik/hub.py | 5 +- .../components/mikrotik/strings.json | 10 +- .../components/mikrotik/translations/en.json | 10 +- tests/components/mikrotik/test_config_flow.py | 96 +++++++++++++++++++ 6 files changed, 166 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index 6a158c60fcf..6c98c389984 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -2,7 +2,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from .const import ATTR_MANUFACTURER, DOMAIN @@ -20,8 +20,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b api = await hass.async_add_executor_job(get_api, dict(config_entry.data)) except CannotConnect as api_error: raise ConfigEntryNotReady from api_error - except LoginError: - return False + except LoginError as err: + raise ConfigEntryAuthFailed from err coordinator = MikrotikDataUpdateCoordinator(hass, config_entry, api) await hass.async_add_executor_job(coordinator.api.get_hub_details) diff --git a/homeassistant/components/mikrotik/config_flow.py b/homeassistant/components/mikrotik/config_flow.py index ed62734578f..84b334c5f8f 100644 --- a/homeassistant/components/mikrotik/config_flow.py +++ b/homeassistant/components/mikrotik/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Mikrotik.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any import voluptuous as vol @@ -33,6 +34,7 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Mikrotik config flow.""" VERSION = 1 + _reauth_entry: config_entries.ConfigEntry | None @staticmethod @callback @@ -76,6 +78,49 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Confirm reauth dialog.""" + errors = {} + assert self._reauth_entry + if user_input is not None: + user_input = {**self._reauth_entry.data, **user_input} + try: + await self.hass.async_add_executor_job(get_api, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except LoginError: + errors[CONF_PASSWORD] = "invalid_auth" + + if not errors: + self.hass.config_entries.async_update_entry( + self._reauth_entry, + data=user_input, + ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + description_placeholders={ + CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME] + }, + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) + class MikrotikOptionsFlowHandler(config_entries.OptionsFlow): """Handle Mikrotik options.""" diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 08320c603f9..26a58948620 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -13,6 +13,7 @@ from librouteros.login import plain as login_plain, token as login_token from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( @@ -132,8 +133,10 @@ class MikrotikData: # get new hub firmware version if updated self.firmware = self.get_info(ATTR_FIRMWARE) - except (CannotConnect, LoginError) as err: + except CannotConnect as err: raise UpdateFailed from err + except LoginError as err: + raise ConfigEntryAuthFailed from err if not device_list: return diff --git a/homeassistant/components/mikrotik/strings.json b/homeassistant/components/mikrotik/strings.json index 6d421cb1838..ec47d98b7a9 100644 --- a/homeassistant/components/mikrotik/strings.json +++ b/homeassistant/components/mikrotik/strings.json @@ -11,6 +11,13 @@ "port": "[%key:common::config_flow::data::port%]", "verify_ssl": "Use ssl" } + }, + "reauth_confirm": { + "description": "The password for {username} is invalid.", + "title": "[%key:common::config_flow::title::reauth%]", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -19,7 +26,8 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "options": { diff --git a/homeassistant/components/mikrotik/translations/en.json b/homeassistant/components/mikrotik/translations/en.json index d60a7064e3a..9874ed21ff1 100644 --- a/homeassistant/components/mikrotik/translations/en.json +++ b/homeassistant/components/mikrotik/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", @@ -9,6 +10,13 @@ "name_exists": "Name exists" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "The password for {username} is invalid.", + "title": "Reauthenticate Integration" + }, "user": { "data": { "host": "Host", diff --git a/tests/components/mikrotik/test_config_flow.py b/tests/components/mikrotik/test_config_flow.py index 6a71806cea9..6a2945c406b 100644 --- a/tests/components/mikrotik/test_config_flow.py +++ b/tests/components/mikrotik/test_config_flow.py @@ -162,3 +162,99 @@ async def test_wrong_credentials(hass, auth_error): CONF_USERNAME: "invalid_auth", CONF_PASSWORD: "invalid_auth", } + + +async def test_reauth_success(hass, api): + """Test we can reauth.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=DEMO_USER_INPUT, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=DEMO_USER_INPUT, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + assert result["description_placeholders"] == {CONF_USERNAME: "username"} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + + +async def test_reauth_failed(hass, auth_error): + """Test reauth fails due to wrong password.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=DEMO_USER_INPUT, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=DEMO_USER_INPUT, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-wrong-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == { + CONF_PASSWORD: "invalid_auth", + } + + +async def test_reauth_failed_conn_error(hass, conn_error): + """Test reauth failed due to connection error.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=DEMO_USER_INPUT, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=DEMO_USER_INPUT, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-wrong-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} From f95b8ccc204bf8eac21c27f5b76760f8f60c4ca1 Mon Sep 17 00:00:00 2001 From: Jevgeni Kiski Date: Sun, 2 Oct 2022 07:13:15 +0300 Subject: [PATCH 096/985] Improve vallox tests and code quality (#75787) code quality improvements --- .../components/vallox/binary_sensor.py | 8 ++--- homeassistant/components/vallox/fan.py | 4 +-- tests/components/vallox/test_binary_sensor.py | 34 +++++++++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 tests/components/vallox/test_binary_sensor.py diff --git a/homeassistant/components/vallox/binary_sensor.py b/homeassistant/components/vallox/binary_sensor.py index 9f1b3018186..5e14b795dae 100644 --- a/homeassistant/components/vallox/binary_sensor.py +++ b/homeassistant/components/vallox/binary_sensor.py @@ -16,7 +16,7 @@ from . import ValloxDataUpdateCoordinator, ValloxEntity from .const import DOMAIN -class ValloxBinarySensor(ValloxEntity, BinarySensorEntity): +class ValloxBinarySensorEntity(ValloxEntity, BinarySensorEntity): """Representation of a Vallox binary sensor.""" entity_description: ValloxBinarySensorEntityDescription @@ -56,7 +56,7 @@ class ValloxBinarySensorEntityDescription( """Describes Vallox binary sensor entity.""" -SENSORS: tuple[ValloxBinarySensorEntityDescription, ...] = ( +BINARY_SENSOR_ENTITIES: tuple[ValloxBinarySensorEntityDescription, ...] = ( ValloxBinarySensorEntityDescription( key="post_heater", name="Post heater", @@ -77,7 +77,7 @@ async def async_setup_entry( async_add_entities( [ - ValloxBinarySensor(data["name"], data["coordinator"], description) - for description in SENSORS + ValloxBinarySensorEntity(data["name"], data["coordinator"], description) + for description in BINARY_SENSOR_ENTITIES ] ) diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index be496bbf899..be713e34e25 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -70,7 +70,7 @@ async def async_setup_entry( client = data["client"] client.set_settable_address(METRIC_KEY_MODE, int) - device = ValloxFan( + device = ValloxFanEntity( data["name"], client, data["coordinator"], @@ -79,7 +79,7 @@ async def async_setup_entry( async_add_entities([device]) -class ValloxFan(ValloxEntity, FanEntity): +class ValloxFanEntity(ValloxEntity, FanEntity): """Representation of the fan.""" _attr_supported_features = FanEntityFeature.PRESET_MODE diff --git a/tests/components/vallox/test_binary_sensor.py b/tests/components/vallox/test_binary_sensor.py new file mode 100644 index 00000000000..a1bd02cf950 --- /dev/null +++ b/tests/components/vallox/test_binary_sensor.py @@ -0,0 +1,34 @@ +"""Tests for Vallox binary sensor platform.""" +from typing import Any + +import pytest + +from homeassistant.core import HomeAssistant + +from .conftest import patch_metrics + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "metrics,expected_state", + [ + ({"A_CYC_IO_HEATER": 1}, "on"), + ({"A_CYC_IO_HEATER": 0}, "off"), + ], +) +async def test_binary_sensor_entitity( + metrics: dict[str, Any], + expected_state: str, + mock_entry: MockConfigEntry, + hass: HomeAssistant, +): + """Test binary sensor with metrics.""" + # Act + with patch_metrics(metrics=metrics): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Assert + sensor = hass.states.get("binary_sensor.vallox_post_heater") + assert sensor.state == expected_state From 205ce2bac51d3e7e701b513708b5b4d2a3e5abe2 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Sun, 2 Oct 2022 15:21:48 +1100 Subject: [PATCH 097/985] Refactor LIFX multizone devices to use extended messages (#79444) --- homeassistant/components/lifx/coordinator.py | 53 ++++- homeassistant/components/lifx/light.py | 55 ++++- tests/components/lifx/__init__.py | 3 +- tests/components/lifx/test_light.py | 237 +++++++++++++++++++ 4 files changed, 333 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index a6d61d91d28..e3a66261fb2 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -12,6 +12,7 @@ from aiolifx.connection import LIFXConnection from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -148,10 +149,14 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): if self.device.mac_addr == TARGET_ANY: self.device.mac_addr = response.target_addr - # Update model-specific configuration - if lifx_features(self.device)["multizone"]: - await self.async_update_color_zones() - await self.async_update_multizone_effect() + # Update extended multizone devices + if lifx_features(self.device)["extended_multizone"]: + await self.async_get_extended_color_zones() + await self.async_get_multizone_effect() + # use legacy methods for older devices + elif lifx_features(self.device)["multizone"]: + await self.async_get_color_zones() + await self.async_get_multizone_effect() if lifx_features(self.device)["hev"]: await self.async_get_hev_cycle() @@ -159,7 +164,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): if lifx_features(self.device)["infrared"]: response = await async_execute_lifx(self.device.get_infrared) - async def async_update_color_zones(self) -> None: + async def async_get_color_zones(self) -> None: """Get updated color information for each zone.""" zone = 0 top = 1 @@ -175,6 +180,15 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): if zone == top - 1: zone -= 1 + async def async_get_extended_color_zones(self) -> None: + """Get updated color information for all zones.""" + try: + await async_execute_lifx(self.device.get_extended_color_zones) + except asyncio.TimeoutError as ex: + raise HomeAssistantError( + f"Timeout getting color zones from {self.name}" + ) from ex + def async_get_hev_cycle_state(self) -> bool | None: """Return the current HEV cycle state.""" if self.device.hev_cycle is None: @@ -232,7 +246,34 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): ) ) - async def async_update_multizone_effect(self) -> None: + async def async_set_extended_color_zones( + self, + colors: list[tuple[int | float, int | float, int | float, int | float]], + colors_count: int | None = None, + duration: int = 0, + apply: int = 1, + ) -> None: + """Send a single set extended color zones message to the device.""" + + if colors_count is None: + colors_count = len(colors) + + # pad the color list with blanks if necessary + if len(colors) < 82: + for _ in range(82 - len(colors)): + colors.append((0, 0, 0, 0)) + + await async_execute_lifx( + partial( + self.device.set_extended_color_zones, + colors=colors, + colors_count=colors_count, + duration=duration, + apply=apply, + ) + ) + + async def async_get_multizone_effect(self) -> None: """Update the device firmware effect running state.""" await async_execute_lifx(self.device.get_multizone_effect) self.active_effect = FirmwareEffect[self.device.effect.get("effect", "OFF")] diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index aa02e42a9bf..50e4593077a 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -95,8 +95,10 @@ async def async_setup_entry( LIFX_SET_HEV_CYCLE_STATE_SCHEMA, "set_hev_cycle_state", ) - if lifx_features(device)["multizone"]: - entity: LIFXLight = LIFXStrip(coordinator, manager, entry) + if lifx_features(device)["extended_multizone"]: + entity: LIFXLight = LIFXExtendedMultiZone(coordinator, manager, entry) + elif lifx_features(device)["multizone"]: + entity = LIFXMultiZone(coordinator, manager, entry) elif lifx_features(device)["color"]: entity = LIFXColor(coordinator, manager, entry) else: @@ -362,8 +364,8 @@ class LIFXColor(LIFXLight): return (hue, sat) if sat else None -class LIFXStrip(LIFXColor): - """Representation of a LIFX light strip with multiple zones.""" +class LIFXMultiZone(LIFXColor): + """Representation of a legacy LIFX multizone device.""" _attr_effect_list = [ SERVICE_EFFECT_COLORLOOP, @@ -426,16 +428,53 @@ class LIFXStrip(LIFXColor): ) from ex # set_color_zones does not update the - # state of the bulb, so we need to do that + # state of the device, so we need to do that await self.get_color() async def update_color_zones( self, ) -> None: - """Send a get color zones message to the bulb.""" + """Send a get color zones message to the device.""" try: - await self.coordinator.async_update_color_zones() + await self.coordinator.async_get_color_zones() except asyncio.TimeoutError as ex: raise HomeAssistantError( - f"Timeout setting updating color zones for {self.name}" + f"Timeout getting color zones from {self.name}" ) from ex + + +class LIFXExtendedMultiZone(LIFXMultiZone): + """Representation of a LIFX device that supports extended multizone messages.""" + + async def set_color( + self, hsbk: list[float | int | None], kwargs: dict[str, Any], duration: int = 0 + ) -> None: + """Set colors on all zones of the device.""" + + # trigger an update of all zone values before merging new values + await self.coordinator.async_get_extended_color_zones() + + color_zones = self.bulb.color_zones + if (zones := kwargs.get(ATTR_ZONES)) is None: + # merge the incoming hsbk across all zones + for index, zone in enumerate(color_zones): + color_zones[index] = merge_hsbk(zone, hsbk) + else: + # merge the incoming HSBK with only the specified zones + for index, zone in enumerate(color_zones): + if index in zones: + color_zones[index] = merge_hsbk(zone, hsbk) + + # send the updated color zones list to the device + try: + await self.coordinator.async_set_extended_color_zones( + color_zones, duration=duration + ) + except asyncio.TimeoutError as ex: + raise HomeAssistantError( + f"Timeout setting color zones on {self.name}" + ) from ex + + # set_extended_color_zones does not update the + # state of the device, so we need to do that + await self.get_color() diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py index 72a355877e1..acfe8f69b02 100644 --- a/tests/components/lifx/__init__.py +++ b/tests/components/lifx/__init__.py @@ -151,7 +151,8 @@ def _mocked_light_strip() -> Light: bulb.set_color_zones = MockLifxCommand(bulb) bulb.get_multizone_effect = MockLifxCommand(bulb) bulb.set_multizone_effect = MockLifxCommand(bulb) - + bulb.get_extended_color_zones = MockLifxCommand(bulb) + bulb.set_extended_color_zones = MockLifxCommand(bulb) return bulb diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index e7c18989767..c2f846b0a76 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -412,6 +412,243 @@ async def test_light_strip(hass: HomeAssistant) -> None: ) +async def test_extended_multizone_messages(hass: HomeAssistant) -> None: + """Test a light strip that supports extended multizone.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + config_entry.add_to_hass(hass) + bulb = _mocked_light_strip() + bulb.product = 38 # LIFX Beam + bulb.power_level = 65535 + bulb.color = [65535, 65535, 65535, 3500] + bulb.color_zones = [(65535, 65535, 65535, 3500)] * 8 + bulb.zones_count = 8 + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 255 + assert attributes[ATTR_COLOR_MODE] == ColorMode.HS + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] + assert attributes[ATTR_HS_COLOR] == (360.0, 100.0) + assert attributes[ATTR_RGB_COLOR] == (255, 0, 0) + assert attributes[ATTR_XY_COLOR] == (0.701, 0.299) + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is True + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + assert len(bulb.set_color_zones.calls) == 0 + assert len(bulb.set_extended_color_zones.calls) == 1 + + bulb.set_color_zones.reset_mock() + bulb.set_extended_color_zones.reset_mock() + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + assert len(bulb.set_color.calls) == 0 + assert len(bulb.set_color_zones.calls) == 0 + assert len(bulb.set_extended_color_zones.calls) == 1 + bulb.set_color.reset_mock() + bulb.set_color_zones.reset_mock() + bulb.set_extended_color_zones.reset_mock() + + bulb.color_zones = [ + (0, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ] + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + + assert len(bulb.set_color.calls) == 0 + assert len(bulb.set_color_zones.calls) == 0 + assert len(bulb.set_extended_color_zones.calls) == 1 + bulb.set_color.reset_mock() + bulb.set_color_zones.reset_mock() + bulb.set_extended_color_zones.reset_mock() + + bulb.color_zones = [ + (0, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ] + + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (255, 10, 30)}, + blocking=True, + ) + # always use a set_extended_color_zones + assert len(bulb.set_color.calls) == 0 + assert len(bulb.set_color_zones.calls) == 0 + assert len(bulb.set_extended_color_zones.calls) == 1 + bulb.set_color.reset_mock() + bulb.set_color_zones.reset_mock() + bulb.set_extended_color_zones.reset_mock() + + bulb.color_zones = [ + (0, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ] + + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_XY_COLOR: (0.3, 0.7)}, + blocking=True, + ) + # Single color uses the fast path + assert len(bulb.set_color.calls) == 0 + assert len(bulb.set_color_zones.calls) == 0 + assert len(bulb.set_extended_color_zones.calls) == 1 + bulb.set_color.reset_mock() + bulb.set_color_zones.reset_mock() + bulb.set_extended_color_zones.reset_mock() + + bulb.color_zones = [ + (0, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ] + + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + + # always use set_extended_color_zones + assert len(bulb.set_color.calls) == 0 + assert len(bulb.set_color_zones.calls) == 0 + assert len(bulb.set_extended_color_zones.calls) == 1 + bulb.set_color.reset_mock() + bulb.set_color_zones.reset_mock() + bulb.set_extended_color_zones.reset_mock() + + await hass.services.async_call( + DOMAIN, + "set_state", + { + ATTR_ENTITY_ID: entity_id, + ATTR_RGB_COLOR: (255, 255, 255), + ATTR_ZONES: [0, 2], + }, + blocking=True, + ) + # set a two zones + assert len(bulb.set_color.calls) == 0 + assert len(bulb.set_color_zones.calls) == 0 + assert len(bulb.set_extended_color_zones.calls) == 1 + bulb.set_color.reset_mock() + bulb.set_color_zones.reset_mock() + bulb.set_extended_color_zones.reset_mock() + + bulb.power_level = 0 + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (255, 255, 255), ATTR_ZONES: [3]}, + blocking=True, + ) + # set a one zone + assert len(bulb.set_power.calls) == 2 + assert len(bulb.get_color_zones.calls) == 0 + assert len(bulb.set_color.calls) == 0 + assert len(bulb.set_color_zones.calls) == 0 + + bulb.get_color_zones.reset_mock() + bulb.set_power.reset_mock() + bulb.set_color_zones.reset_mock() + + bulb.set_extended_color_zones = MockFailingLifxCommand(bulb) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + "set_state", + { + ATTR_ENTITY_ID: entity_id, + ATTR_RGB_COLOR: (255, 255, 255), + ATTR_ZONES: [3], + }, + blocking=True, + ) + + bulb.set_extended_color_zones = MockLifxCommand(bulb) + bulb.get_extended_color_zones = MockFailingLifxCommand(bulb) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + "set_state", + { + ATTR_ENTITY_ID: entity_id, + ATTR_RGB_COLOR: (255, 255, 255), + ATTR_ZONES: [3], + }, + blocking=True, + ) + + async def test_lightstrip_move_effect(hass: HomeAssistant) -> None: """Test the firmware move effect on a light strip.""" config_entry = MockConfigEntry( From 7ae942a62b38bf854b1c0decb7fb795bc8d1010c Mon Sep 17 00:00:00 2001 From: Maximilian <43999966+DeerMaximum@users.noreply.github.com> Date: Sun, 2 Oct 2022 06:22:18 +0200 Subject: [PATCH 098/985] Fix nina warning state (#76354) * Fix warning state * Improve data handling * Remove duplicate code --- homeassistant/components/nina/__init__.py | 57 ++++++++++--------- .../components/nina/binary_sensor.py | 31 +++++----- .../nina/fixtures/sample_warning_details.json | 47 +++++++++++++++ .../nina/fixtures/sample_warnings.json | 24 ++++++++ 4 files changed, 118 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/nina/__init__.py b/homeassistant/components/nina/__init__.py index 17e0280ca50..75ec2cdfe94 100644 --- a/homeassistant/components/nina/__init__.py +++ b/homeassistant/components/nina/__init__.py @@ -1,6 +1,7 @@ """The Nina integration.""" from __future__ import annotations +from dataclasses import dataclass from typing import Any from async_timeout import timeout @@ -12,21 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ( - _LOGGER, - ATTR_DESCRIPTION, - ATTR_EXPIRES, - ATTR_HEADLINE, - ATTR_ID, - ATTR_SENDER, - ATTR_SENT, - ATTR_SEVERITY, - ATTR_START, - CONF_FILTER_CORONA, - CONF_REGIONS, - DOMAIN, - SCAN_INTERVAL, -) +from .const import _LOGGER, CONF_FILTER_CORONA, CONF_REGIONS, DOMAIN, SCAN_INTERVAL PLATFORMS: list[str] = [Platform.BINARY_SENSOR] @@ -61,6 +48,21 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> Non await hass.config_entries.async_reload(entry.entry_id) +@dataclass +class NinaWarningData: + """Class to hold the warning data.""" + + id: str + headline: str + description: str + sender: str + severity: str + sent: str + start: str + expires: str + is_valid: bool + + class NINADataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching NINA data API.""" @@ -93,23 +95,24 @@ class NINADataUpdateCoordinator(DataUpdateCoordinator): return_data: dict[str, Any] = {} for region_id, raw_warnings in self._nina.warnings.items(): - warnings_for_regions: list[Any] = [] + warnings_for_regions: list[NinaWarningData] = [] for raw_warn in raw_warnings: if "corona" in raw_warn.headline.lower() and self.corona_filter: continue - warn_obj: dict[str, Any] = { - ATTR_ID: raw_warn.id, - ATTR_HEADLINE: raw_warn.headline, - ATTR_DESCRIPTION: raw_warn.description, - ATTR_SENDER: raw_warn.sender, - ATTR_SEVERITY: raw_warn.severity, - ATTR_SENT: raw_warn.sent or "", - ATTR_START: raw_warn.start or "", - ATTR_EXPIRES: raw_warn.expires or "", - } - warnings_for_regions.append(warn_obj) + warning_data: NinaWarningData = NinaWarningData( + raw_warn.id, + raw_warn.headline, + raw_warn.description, + raw_warn.sender, + raw_warn.severity, + raw_warn.sent or "", + raw_warn.start or "", + raw_warn.expires or "", + raw_warn.isValid(), + ) + warnings_for_regions.append(warning_data) return_data[region_id] = warnings_for_regions diff --git a/homeassistant/components/nina/binary_sensor.py b/homeassistant/components/nina/binary_sensor.py index 29f985df618..4cbb3b6887a 100644 --- a/homeassistant/components/nina/binary_sensor.py +++ b/homeassistant/components/nina/binary_sensor.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import NINADataUpdateCoordinator +from . import NINADataUpdateCoordinator, NinaWarningData from .const import ( ATTR_DESCRIPTION, ATTR_EXPIRES, @@ -72,25 +72,28 @@ class NINAMessage(CoordinatorEntity[NINADataUpdateCoordinator], BinarySensorEnti @property def is_on(self) -> bool: """Return the state of the sensor.""" - return len(self.coordinator.data[self._region]) > self._warning_index + if not len(self.coordinator.data[self._region]) > self._warning_index: + return False + + data: NinaWarningData = self.coordinator.data[self._region][self._warning_index] + + return data.is_valid @property def extra_state_attributes(self) -> dict[str, Any]: """Return extra attributes of the sensor.""" - if ( - not len(self.coordinator.data[self._region]) > self._warning_index - ) or not self.is_on: + if not self.is_on: return {} - data: dict[str, Any] = self.coordinator.data[self._region][self._warning_index] + data: NinaWarningData = self.coordinator.data[self._region][self._warning_index] return { - ATTR_HEADLINE: data[ATTR_HEADLINE], - ATTR_DESCRIPTION: data[ATTR_DESCRIPTION], - ATTR_SENDER: data[ATTR_SENDER], - ATTR_SEVERITY: data[ATTR_SEVERITY], - ATTR_ID: data[ATTR_ID], - ATTR_SENT: data[ATTR_SENT], - ATTR_START: data[ATTR_START], - ATTR_EXPIRES: data[ATTR_EXPIRES], + ATTR_HEADLINE: data.headline, + ATTR_DESCRIPTION: data.description, + ATTR_SENDER: data.sender, + ATTR_SEVERITY: data.severity, + ATTR_ID: data.id, + ATTR_SENT: data.sent, + ATTR_START: data.start, + ATTR_EXPIRES: data.expires, } diff --git a/tests/components/nina/fixtures/sample_warning_details.json b/tests/components/nina/fixtures/sample_warning_details.json index f9da183c553..aa176b2199e 100644 --- a/tests/components/nina/fixtures/sample_warning_details.json +++ b/tests/components/nina/fixtures/sample_warning_details.json @@ -157,5 +157,52 @@ ] } ] + }, + "biw.BIWAPP-69634": { + "identifier": "biw.BIWAPP-69634", + "sender": "CAP@biwapp.de", + "sent": "1999-08-07T10:59:00+02:00", + "status": "Actual", + "msgType": "Alert", + "scope": "Public", + "code": ["DVN:2", "BIWAPP"], + "info": [ + { + "language": "DE", + "category": ["Other"], + "event": "4", + "urgency": "Unknown", + "severity": "Minor", + "certainty": "Unknown", + "expires": "2002-08-07T10:59:00+02:00", + "headline": "Geflügelpest im Landkreis Cuxhaven - Teile des Landkreises Osterholz zur Überwachungszone erklärt", + "description": "In Beverstedt im Landkreis Cuxhaven ist am 20. Juli 2022 in einer Geflügelhaltung der Ausbruch der Geflügelpest (Vogelgrippe, Aviäre Influenza) amtlich festgestellt worden. Durch die geografische Nähe des Ausbruchsbetriebes zum Gebiet des Landkreises Osterholz musste das Veterinäramt des Landkreises zum Schutz vor einer Ausbreitung der Geflügelpest auch für sein Gebiet ein Restriktionsgebiet festlegen. Rund um den Ausbruchsort wurde eine Überwachungszone ausgewiesen. Eine entsprechende Tierseuchenbehördliche Allgemeinverfügung wurde vom Landkreis Osterholz erlassen und tritt am 23.07.2022 in Kraft.
 
Die Überwachungszone mit einem Radius von mindestens zehn Kilometern um den Ausbruchsbetrieb erstreckt sich im Landkreis Osterholz innerhalb der Samtgemeinde Hambergen auf die Mitgliedsgemeinden Axstedt, Holste und Lübberstedt. Die vorgenannten Gemeinden sind vollständig zur Überwachungszone erklärt worden. Der genaue Grenzverlauf des Gebietes kann auch der interaktiven Karte im Internet entnommen werden.
 
In der Überwachungszone liegen im Landkreis Osterholz rund 70 Geflügelhaltungen mit einem Gesamtbestand von rund 1.800 Tieren. Sie alle unterliegen mit der Allgemeinverfügung der sogenannten amtlichen Beobachtung. Für die Betriebe sind die Biosicherheitsmaßnahmen einzuhalten. Dazu zählen insbesondere Hygienemaßnahmen im laufenden Betrieb und eine ordnungsgemäße Schadnagerbekämpfung.
 
Das Verbringen von Vögeln, Fleisch von Geflügel, Eiern und sonstige Nebenprodukte von Geflügel in und aus Betrieben in der Überwachungszone ist verboten. Auch Geflügeltransporte sind in der Überwachungszone verboten. Jeder Verdacht der Erkrankung auf Geflügelpest ist zudem dem Veterinäramt des Landkreises Osterholz unter der E-Mail-Adresse veterinaeramt@landkreis-osterholz.de sofort zu melden. Alle Hinweise, die innerhalb der Überwachungszone zu beachten sind, sind unter www.landkreis-osterholz.de/gefluegelpest zusammengefasst dargestellt.
 
Die Veterinärbehörde weist zudem darauf hin, dass sämtliche Geflügelhaltungen – Hühner, Enten, Gänse, Fasane, Perlhühner, Rebhühner, Truthühner, Wachteln oder Laufvögel – der zuständigen Behörde angezeigt werden müssen. Wer dies bisher noch nicht gemacht hat und über keine Registriernummer für seinen Geflügelbestand verfügt, sollte die Meldung über das Veterinäramt umgehend nachholen.
 
Das Beobachtungsgebiet kann frühestens 30 Tage nach der Grobreinigung des Ausbruchsbetriebes wieder aufgehoben werden. Hierüber wird der Landkreis Osterholz informieren.
 
Die Allgemeinverfügung, eine Übersicht zur Überwachungszone und weitere Hinweise sind auf der Internetseite unter www.landkreis-osterholz.de/gefluegelpest zu finden.", + "parameter": [ + { + "valueName": "sender_langname", + "value": "Landkreis Osterholz" + }, + { + "valueName": "PHGEM", + "value": "740+10,770,792,817,100001" + }, + { + "valueName": "GRID", + "value": "101346,101954+7,102566+9,103177+13,103774,103790+13,104387+1,104403+13,105000+1,105016+15,105612+2,105630+15,106225+2,106241+18,106838+2,106853+18,107451+1,107464+22,108064+9,108075+23,108677+34,109290+34,109903+35,110516+35,111129+35,111742+35,112355,112357+34,112971+33,113587+30,114200+30,114814,114818+26,115432,115436+22,116050+21,116669+15,117283+5,117290+7,117897+3,117904+6,500001" + } + ], + "area": [ + { + "areaDesc": "Axstedt, Gnarrenburg, Grasberg, Hagen im Bremischen, Hambergen, Hepstedt, Holste, Lilienthal, Lübberstedt, Osterholz-Scharmbeck, Ritterhude, Schwanewede, Vollersode, Worpswede", + "geocode": [ + { + "valueName": "AreaId", + "value": "0" + } + ] + } + ] + } + ] } } diff --git a/tests/components/nina/fixtures/sample_warnings.json b/tests/components/nina/fixtures/sample_warnings.json index 0a41611b7ee..12d78b03cce 100644 --- a/tests/components/nina/fixtures/sample_warnings.json +++ b/tests/components/nina/fixtures/sample_warnings.json @@ -40,5 +40,29 @@ "onset": "2021-11-01T05:20:00+01:00", "sent": "2021-10-11T05:20:00+01:00", "expires": "3021-11-22T05:19:00+01:00" + }, + { + "id": "biw.BIWAPP-69634", + "payload": { + "version": 2, + "type": "ALERT", + "id": "biw.BIWAPP-69634", + "hash": "fdbafb6b164f549ff60b9adfa5b1c707069cdd178bf55f025066f319451660ad", + "data": { + "headline": "Geflügelpest im Landkreis Cuxhaven - Teile des Landkreises Osterholz zur Überwachungszone erklärt", + "provider": "BIWAPP", + "severity": "Minor", + "msgType": "Alert", + "area": { + "type": "GRID", + "data": "101346,101954+7,102566+9,103177+13,103774,103790+13,104387+1,104403+13,105000+1,105016+15,105612+2,105630+15,106225+2,106241+18,106838+2,106853+18,107451+1,107464+22,108064+9,108075+23,108677+34,109290+34,109903+35,110516+35,111129+35,111742+35,112355,112357+34,112971+33,113587+30,114200+30,114814,114818+26,115432,115436+22,116050+21,116669+15,117283+5,117290+7,117897+3,117904+6,500001" + } + } + }, + "i18nTitle": { + "de": "Geflügelpest im Landkreis Cuxhaven - Teile des Landkreises Osterholz zur Überwachungszone erklärt" + }, + "sent": "1999-08-07T10:59:00+02:00", + "expires": "2002-08-07T10:59:00+02:00" } ] From 89d0b434bcf0f0bd42eb055b575da77449de38bf Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 2 Oct 2022 06:24:25 +0200 Subject: [PATCH 099/985] Use explicit return value in azure_event_hub (#79315) * Use explicit return value in azure_event_hub * Use abstractmethod --- homeassistant/components/azure_event_hub/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/azure_event_hub/client.py b/homeassistant/components/azure_event_hub/client.py index 27a4eabf535..90880a92b64 100644 --- a/homeassistant/components/azure_event_hub/client.py +++ b/homeassistant/components/azure_event_hub/client.py @@ -1,6 +1,7 @@ """File for Azure Event Hub models.""" from __future__ import annotations +from abc import ABC, abstractmethod from dataclasses import dataclass import logging @@ -12,12 +13,13 @@ _LOGGER = logging.getLogger(__name__) @dataclass -class AzureEventHubClient: +class AzureEventHubClient(ABC): """Class for the Azure Event Hub client. Use from_input to initialize.""" event_hub_instance_name: str @property + @abstractmethod def client(self) -> EventHubProducerClient: """Return the client.""" From 229e387a1d1984276dfeeff24ecceff9015dc3c0 Mon Sep 17 00:00:00 2001 From: Ryan Fleming Date: Sun, 2 Oct 2022 01:05:53 -0400 Subject: [PATCH 100/985] Bump pyoctoprintapi to version 1.9 (#79449) Bump to version 1.9 --- homeassistant/components/octoprint/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/octoprint/manifest.json b/homeassistant/components/octoprint/manifest.json index 4086f9fbe20..9bc2f0011c0 100644 --- a/homeassistant/components/octoprint/manifest.json +++ b/homeassistant/components/octoprint/manifest.json @@ -3,7 +3,7 @@ "name": "OctoPrint", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/octoprint", - "requirements": ["pyoctoprintapi==0.1.8"], + "requirements": ["pyoctoprintapi==0.1.9"], "codeowners": ["@rfleming71"], "zeroconf": ["_octoprint._tcp.local."], "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8074756a367..27d2020e900 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1763,7 +1763,7 @@ pynzbgetapi==0.2.0 pyobihai==1.3.2 # homeassistant.components.octoprint -pyoctoprintapi==0.1.8 +pyoctoprintapi==0.1.9 # homeassistant.components.ombi pyombi==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4097f10c78b..ad4687562dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1246,7 +1246,7 @@ pynx584==0.5 pynzbgetapi==0.2.0 # homeassistant.components.octoprint -pyoctoprintapi==0.1.8 +pyoctoprintapi==0.1.9 # homeassistant.components.openuv pyopenuv==2022.04.0 From 28809fc7fd04ee318c528ee7921a79877b3f58ea Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 1 Oct 2022 23:07:27 -0700 Subject: [PATCH 101/985] Remove dead code code in calendar (#79450) --- homeassistant/components/calendar/__init__.py | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index cfbe038c251..213376b2081 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -133,32 +133,6 @@ def _get_api_date(dt_or_d: datetime.datetime | datetime.date) -> dict[str, str]: return {"date": dt_or_d.isoformat()} -def normalize_event(event: dict[str, Any]) -> dict[str, Any]: - """Normalize a calendar event.""" - normalized_event: dict[str, Any] = {} - - start = event.get("start") - end = event.get("end") - start = get_date(start) if start is not None else None - end = get_date(end) if end is not None else None - normalized_event["dt_start"] = start - normalized_event["dt_end"] = end - - start = start.strftime(DATE_STR_FORMAT) if start is not None else None - end = end.strftime(DATE_STR_FORMAT) if end is not None else None - normalized_event["start"] = start - normalized_event["end"] = end - - # cleanup the string so we don't have a bunch of double+ spaces - summary = event.get("summary", "") - normalized_event["message"] = re.sub(" +", "", summary).strip() - normalized_event["location"] = event.get("location", "") - normalized_event["description"] = event.get("description", "") - normalized_event["all_day"] = "date" in event["start"] - - return normalized_event - - def extract_offset(summary: str, offset_prefix: str) -> tuple[str, datetime.timedelta]: """Extract the offset from the event summary. From 2ea97324195c0294ec4a8bc3239321fab267587d Mon Sep 17 00:00:00 2001 From: Ryan Fleming Date: Sun, 2 Oct 2022 02:08:45 -0400 Subject: [PATCH 102/985] Support reauth for octoprint (#77213) * Add reauth flow to octoprint * Add unit tests around octoprint reauth * Add missing strings * Fix unit test mocks --- .../components/octoprint/__init__.py | 6 +++ .../components/octoprint/config_flow.py | 54 ++++++++++++++++++- .../components/octoprint/strings.json | 8 ++- .../components/octoprint/translations/en.json | 6 +++ .../components/octoprint/test_config_flow.py | 52 ++++++++++++++++++ 5 files changed, 124 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 1d1c1958420..9db4e834571 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -6,6 +6,7 @@ import logging from typing import cast from pyoctoprintapi import ApiError, OctoprintClient, PrinterOffline +from pyoctoprintapi.exceptions import UnauthorizedException import voluptuous as vol from yarl import URL @@ -24,6 +25,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo @@ -226,6 +228,8 @@ class OctoprintDataUpdateCoordinator(DataUpdateCoordinator): printer = None try: job = await self._octoprint.get_job_info() + except UnauthorizedException as err: + raise ConfigEntryAuthFailed from err except ApiError as err: raise UpdateFailed(err) from err @@ -238,6 +242,8 @@ class OctoprintDataUpdateCoordinator(DataUpdateCoordinator): if not self._printer_offline: _LOGGER.debug("Unable to retrieve printer information: Printer offline") self._printer_offline = True + except UnauthorizedException as err: + raise ConfigEntryAuthFailed from err except ApiError as err: raise UpdateFailed(err) from err else: diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index 1bd54e2214e..c1bdc623291 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -1,5 +1,9 @@ """Config flow for OctoPrint integration.""" +from __future__ import annotations + +from collections.abc import Mapping import logging +from typing import Any from pyoctoprintapi import ApiError, OctoprintClient, OctoprintException import voluptuous as vol @@ -16,6 +20,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -46,6 +51,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 api_key_task = None + _reauth_data = None def __init__(self) -> None: """Handle a config flow for OctoPrint.""" @@ -114,8 +120,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._user_input = user_input return self.async_show_progress_done(next_step_id="user") - async def _finish_config(self, user_input): + async def _finish_config(self, user_input: dict): """Finish the configuration setup.""" + existing_entry = await self.async_set_unique_id(self.unique_id) + if existing_entry is not None: + self.hass.config_entries.async_update_entry(existing_entry, data=user_input) + # Reload the config entry otherwise devices will remain unavailable + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) + + return self.async_abort(reason="reauth_successful") + octoprint = self._get_octoprint_client(user_input) octoprint.set_api_key(user_input[CONF_API_KEY]) @@ -127,6 +143,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(discovery.upnp_uuid, raise_on_progress=False) self._abort_if_unique_id_configured() + return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) async def async_step_auth_failed(self, user_input): @@ -188,6 +205,41 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() + async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: + """Handle reauthorization request from Octoprint.""" + self._reauth_data = dict(config) + + self.context.update( + { + "title_placeholders": {CONF_HOST: config[CONF_HOST]}, + } + ) + + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle reauthorization flow.""" + assert self._reauth_data is not None + + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required( + CONF_USERNAME, default=self._reauth_data[CONF_USERNAME] + ): str, + } + ), + ) + + self.api_key_task = None + self._reauth_data[CONF_USERNAME] = user_input[CONF_USERNAME] + + return await self.async_step_get_api_key(self._reauth_data) + async def _async_get_auth_key(self, user_input: dict): """Get application api key.""" octoprint = self._get_octoprint_client(user_input) diff --git a/homeassistant/components/octoprint/strings.json b/homeassistant/components/octoprint/strings.json index 89e44a6a3a6..23cdf6ce56e 100644 --- a/homeassistant/components/octoprint/strings.json +++ b/homeassistant/components/octoprint/strings.json @@ -11,6 +11,11 @@ "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]", "username": "[%key:common::config_flow::data::username%]" } + }, + "reauth_confirm": { + "data": { + "username": "[%key:common::config_flow::data::username%]" + } } }, "error": { @@ -21,7 +26,8 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "unknown": "[%key:common::config_flow::error::unknown%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "auth_failed": "Failed to retrieve application api key" + "auth_failed": "Failed to retrieve application api key", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "progress": { "get_api_key": "Open the OctoPrint UI and click 'Allow' on the Access Request for 'Home Assistant'." diff --git a/homeassistant/components/octoprint/translations/en.json b/homeassistant/components/octoprint/translations/en.json index e0729b27856..035274d6e9c 100644 --- a/homeassistant/components/octoprint/translations/en.json +++ b/homeassistant/components/octoprint/translations/en.json @@ -4,6 +4,7 @@ "already_configured": "Device is already configured", "auth_failed": "Failed to retrieve application api key", "cannot_connect": "Failed to connect", + "reauth_successful": "Re-authentication was successful", "unknown": "Unexpected error" }, "error": { @@ -24,6 +25,11 @@ "username": "Username", "verify_ssl": "Verify SSL certificate" } + }, + "reauth_confirm": { + "data": { + "username": "Username" + } } } } diff --git a/tests/components/octoprint/test_config_flow.py b/tests/components/octoprint/test_config_flow.py index e9de98206d1..b4e6c5b0666 100644 --- a/tests/components/octoprint/test_config_flow.py +++ b/tests/components/octoprint/test_config_flow.py @@ -533,3 +533,55 @@ async def test_duplicate_ssdp_ignored(hass: HomeAssistant) -> None: ) assert result["type"] == "abort" assert result["reason"] == "already_configured" + + +async def test_reauth_form(hass): + """Test we get the form.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "username": "testuser", + "host": "1.1.1.1", + "name": "Printer", + "port": 81, + "ssl": True, + "path": "/", + }, + unique_id="1234", + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "entry_id": entry.entry_id, + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + }, + data=entry.data, + ) + assert result["type"] == "form" + assert not result["errors"] + + with patch( + "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "testuser", + }, + ) + await hass.async_block_till_done() + assert result["type"] == "progress" + + with patch( + "homeassistant.components.octoprint.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" From 547a63e3145bd54d392ed58aafc6bd60b265b49a Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 2 Oct 2022 11:07:19 -0400 Subject: [PATCH 103/985] Remove unnecessary config entity from ZHA (#79472) --- .../zha/core/channels/manufacturerspecific.py | 30 ------------------- homeassistant/components/zha/number.py | 13 -------- 2 files changed, 43 deletions(-) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 49f5d1df249..724a794007d 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -181,43 +181,13 @@ class InovelliConfigEntityChannel(ZigbeeChannel): "power_type": False, "switch_type": False, "button_delay": False, - "device_bind_number": False, "smart_bulb_mode": False, "double_tap_up_for_full_brightness": False, - "default_led1_strip_color_when_on": False, - "default_led1_strip_color_when_off": False, - "default_led1_strip_intensity_when_on": False, - "default_led1_strip_intensity_when_off": False, - "default_led2_strip_color_when_on": False, - "default_led2_strip_color_when_off": False, - "default_led2_strip_intensity_when_on": False, - "default_led2_strip_intensity_when_off": False, - "default_led3_strip_color_when_on": False, - "default_led3_strip_color_when_off": False, - "default_led3_strip_intensity_when_on": False, - "default_led3_strip_intensity_when_off": False, - "default_led4_strip_color_when_on": False, - "default_led4_strip_color_when_off": False, - "default_led4_strip_intensity_when_on": False, - "default_led4_strip_intensity_when_off": False, - "default_led5_strip_color_when_on": False, - "default_led5_strip_color_when_off": False, - "default_led5_strip_intensity_when_on": False, - "default_led5_strip_intensity_when_off": False, - "default_led6_strip_color_when_on": False, - "default_led6_strip_color_when_off": False, - "default_led6_strip_intensity_when_on": False, - "default_led6_strip_intensity_when_off": False, - "default_led7_strip_color_when_on": False, - "default_led7_strip_color_when_off": False, - "default_led7_strip_intensity_when_on": False, - "default_led7_strip_intensity_when_off": False, "led_color_when_on": False, "led_color_when_off": False, "led_intensity_when_on": False, "led_intensity_when_off": False, "local_protection": False, - "remote_protection": False, "output_mode": False, "on_off_led_mode": False, "firmware_progress_led": False, diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 6fe411abfb3..3bace412744 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -576,19 +576,6 @@ class InovelliButtonDelay(ZHANumberConfigurationEntity, id_suffix="button_delay" _attr_name: str = "Button delay" -@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI) -class InovelliDeviceBindNumber( - ZHANumberConfigurationEntity, id_suffix="device_bind_number" -): - """Inovelli device bind number configuration entity.""" - - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0 - _attr_native_max_value: float = 255 - _zcl_attribute: str = "device_bind_number" - _attr_name: str = "Device bind number" - - @CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI) class InovelliLocalDimmingUpSpeed( ZHANumberConfigurationEntity, id_suffix="dimming_speed_up_local" From 7aacdec8e1c6cc245be890bef26dcdf52ab5a7ba Mon Sep 17 00:00:00 2001 From: Maximilian <43999966+DeerMaximum@users.noreply.github.com> Date: Sun, 2 Oct 2022 17:57:32 +0200 Subject: [PATCH 104/985] Address late review of nina (#79467) * Address review * Remove unused attribute --- homeassistant/components/nina/__init__.py | 12 ++++++------ homeassistant/components/nina/binary_sensor.py | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/nina/__init__.py b/homeassistant/components/nina/__init__.py index 75ec2cdfe94..f03a2c765cc 100644 --- a/homeassistant/components/nina/__init__.py +++ b/homeassistant/components/nina/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any from async_timeout import timeout from pynina import ApiError, Nina @@ -63,7 +62,9 @@ class NinaWarningData: is_valid: bool -class NINADataUpdateCoordinator(DataUpdateCoordinator): +class NINADataUpdateCoordinator( + DataUpdateCoordinator[dict[str, list[NinaWarningData]]] +): """Class to manage fetching NINA data API.""" def __init__( @@ -72,7 +73,6 @@ class NINADataUpdateCoordinator(DataUpdateCoordinator): """Initialize.""" self._regions: dict[str, str] = regions self._nina: Nina = Nina(async_get_clientsession(hass)) - self.warnings: dict[str, Any] = {} self.corona_filter: bool = corona_filter for region in regions: @@ -80,7 +80,7 @@ class NINADataUpdateCoordinator(DataUpdateCoordinator): super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) - async def _async_update_data(self) -> dict[str, Any]: + async def _async_update_data(self) -> dict[str, list[NinaWarningData]]: """Update data.""" async with timeout(10): try: @@ -89,10 +89,10 @@ class NINADataUpdateCoordinator(DataUpdateCoordinator): raise UpdateFailed(err) from err return self._parse_data() - def _parse_data(self) -> dict[str, Any]: + def _parse_data(self) -> dict[str, list[NinaWarningData]]: """Parse warning data.""" - return_data: dict[str, Any] = {} + return_data: dict[str, list[NinaWarningData]] = {} for region_id, raw_warnings in self._nina.warnings.items(): warnings_for_regions: list[NinaWarningData] = [] diff --git a/homeassistant/components/nina/binary_sensor.py b/homeassistant/components/nina/binary_sensor.py index 4cbb3b6887a..76280ab159e 100644 --- a/homeassistant/components/nina/binary_sensor.py +++ b/homeassistant/components/nina/binary_sensor.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import NINADataUpdateCoordinator, NinaWarningData +from . import NINADataUpdateCoordinator from .const import ( ATTR_DESCRIPTION, ATTR_EXPIRES, @@ -75,7 +75,7 @@ class NINAMessage(CoordinatorEntity[NINADataUpdateCoordinator], BinarySensorEnti if not len(self.coordinator.data[self._region]) > self._warning_index: return False - data: NinaWarningData = self.coordinator.data[self._region][self._warning_index] + data = self.coordinator.data[self._region][self._warning_index] return data.is_valid @@ -85,7 +85,7 @@ class NINAMessage(CoordinatorEntity[NINADataUpdateCoordinator], BinarySensorEnti if not self.is_on: return {} - data: NinaWarningData = self.coordinator.data[self._region][self._warning_index] + data = self.coordinator.data[self._region][self._warning_index] return { ATTR_HEADLINE: data.headline, From 653620345c34db1f0ba31caffa8f5e096f3de637 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Oct 2022 06:46:01 -1000 Subject: [PATCH 105/985] Bump dbus-fast to 1.20.0 (#79465) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 9c989bfe9fa..8aef8bb42c0 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.5.3", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.18.0" + "dbus-fast==1.20.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 258776dde30..b68772c9fa1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.18.0 +dbus-fast==1.20.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 27d2020e900..a013fcfb220 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -543,7 +543,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.18.0 +dbus-fast==1.20.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ad4687562dc..ef63872fc84 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.18.0 +dbus-fast==1.20.0 # homeassistant.components.debugpy debugpy==1.6.3 From 069818940e5a16c412544e1e14e69b5d0964f157 Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Sun, 2 Oct 2022 09:47:07 -0700 Subject: [PATCH 106/985] Skip parsing Flume sensors without location (#79456) --- homeassistant/components/flume/sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index b9b5f819520..6d68058732d 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -40,7 +40,10 @@ async def async_setup_entry( flume_entity_list = [] for device in flume_devices.device_list: - if device[KEY_DEVICE_TYPE] != FLUME_TYPE_SENSOR: + if ( + device[KEY_DEVICE_TYPE] != FLUME_TYPE_SENSOR + or KEY_DEVICE_LOCATION not in device + ): continue device_id = device[KEY_DEVICE_ID] From d58e16b9903247030bd683725e3dfb21e2205015 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Sun, 2 Oct 2022 20:34:15 +0200 Subject: [PATCH 107/985] Fix empty default ZHA configuration (#79475) * Also add 0 as a default for transition in const.py This is the same default transition (none) that is used in ZHA's light.py * Send default values for unconfigured options in ZHA's configuration API * Remove options that match defaults values before saving --- homeassistant/components/zha/api.py | 22 ++++++++++++++++++++++ homeassistant/components/zha/core/const.py | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 1095bae5ac8..6cbcdf50983 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -1028,6 +1028,12 @@ async def websocket_get_configuration( data["data"][section] = zha_gateway.config_entry.options.get( CUSTOM_CONFIGURATION, {} ).get(section, {}) + + # send default values for unconfigured options + for entry in data["schemas"][section]: + if data["data"][section].get(entry["name"]) is None: + data["data"][section][entry["name"]] = entry["default"] + connection.send_result(msg[ID], data) @@ -1047,6 +1053,22 @@ async def websocket_update_zha_configuration( options = zha_gateway.config_entry.options data_to_save = {**options, **{CUSTOM_CONFIGURATION: msg["data"]}} + for section, schema in ZHA_CONFIG_SCHEMAS.items(): + for entry in schema.schema: + # remove options that match defaults + if ( + data_to_save[CUSTOM_CONFIGURATION].get(section, {}).get(entry) + == entry.default() + ): + data_to_save[CUSTOM_CONFIGURATION][section].pop(entry) + # remove entire section block if empty + if not data_to_save[CUSTOM_CONFIGURATION][section]: + data_to_save[CUSTOM_CONFIGURATION].pop(section) + + # remove entire custom_configuration block if empty + if not data_to_save[CUSTOM_CONFIGURATION]: + data_to_save.pop(CUSTOM_CONFIGURATION) + _LOGGER.info( "Updating ZHA custom configuration options from %s to %s", options, diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 0204fb50bed..b9871a1f2ab 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -146,7 +146,7 @@ CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY = 60 * 60 * 6 # 6 hours CONF_ZHA_OPTIONS_SCHEMA = vol.Schema( { - vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION): cv.positive_int, + vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION, default=0): cv.positive_int, vol.Required(CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, default=False): cv.boolean, vol.Required(CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, default=True): cv.boolean, vol.Required(CONF_ALWAYS_PREFER_XY_COLOR_MODE, default=True): cv.boolean, From 81b940ec171779ed678580a81ca35ac8e29552dd Mon Sep 17 00:00:00 2001 From: zbeky <32236798+zbeky@users.noreply.github.com> Date: Sun, 2 Oct 2022 20:34:53 +0200 Subject: [PATCH 108/985] Add EVOLVEO Heat M30v2 TRV (#79462) --- homeassistant/components/zha/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index ca3110dad60..a4e1be78c08 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -585,6 +585,7 @@ class CentralitePearl(ZenWithinThermostat): "_TZE200_4eeyebrt", "_TZE200_cpmgn2cf", "_TZE200_9sfg7gm0", + "_TZE200_8whxpsiw", "_TYST11_ckud7u2l", "_TYST11_ywdxldoj", "_TYST11_cwnjrr72", From 14f60dc87162c1a005ee68f05db4a877f1305024 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 2 Oct 2022 20:50:01 +0200 Subject: [PATCH 109/985] Fix missing string message in UniFi (#79487) --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 9e8a1ef28f3..0ff781418d4 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==35"], + "requirements": ["aiounifi==36"], "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index a013fcfb220..20f2e7d827d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,7 +276,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==35 +aiounifi==36 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef63872fc84..903c01c4ec6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -251,7 +251,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==35 +aiounifi==36 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From f28e3fb46caf8689a1dd70e22d3197da9d24e954 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sun, 2 Oct 2022 21:30:54 +0200 Subject: [PATCH 110/985] Update frontend to 20221002.0 (#79491) --- 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 bcc574a4cad..3dbf73bdeaf 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==20220929.0"], + "requirements": ["home-assistant-frontend==20221002.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b68772c9fa1..a6c3fcceedd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.20.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220929.0 +home-assistant-frontend==20221002.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 20f2e7d827d..586a146fd32 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,7 +868,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20220929.0 +home-assistant-frontend==20221002.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 903c01c4ec6..648fbed4b50 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,7 +648,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20220929.0 +home-assistant-frontend==20221002.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 3b794038b1e439627b08a8a404313600e578195f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 2 Oct 2022 20:07:57 +0000 Subject: [PATCH 111/985] Add reauth flow to BraviaTV integration (#79405) * Raise ConfigEntryAuthFailed * Add reauth flow * Add tests * Patch pair() method to avoid IO * Remove unused errors dict --- .../components/braviatv/config_flow.py | 52 +++++++ .../components/braviatv/coordinator.py | 4 + .../components/braviatv/strings.json | 11 +- tests/components/braviatv/test_config_flow.py | 128 +++++++++++++++++- 4 files changed, 187 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index e6bf5a44019..dbd08809703 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure the Bravia TV integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any from urllib.parse import urlparse @@ -40,6 +41,7 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Initialize config flow.""" self.client: BraviaTV | None = None self.device_config: dict[str, Any] = {} + self.entry: ConfigEntry | None = None @staticmethod @callback @@ -177,6 +179,56 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="confirm") + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle configuration by re-auth.""" + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + self.device_config = {**entry_data} + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Dialog that informs the user that reauth is required.""" + self.create_client() + + assert self.client is not None + assert self.entry is not None + + if user_input is not None: + pin = user_input[CONF_PIN] + use_psk = user_input[CONF_USE_PSK] + try: + if use_psk: + await self.client.connect(psk=pin) + else: + await self.client.connect( + pin=pin, clientid=CLIENTID_PREFIX, nickname=NICKNAME + ) + await self.client.set_wol_mode(True) + except BraviaTVError: + return self.async_abort(reason="reauth_unsuccessful") + else: + self.hass.config_entries.async_update_entry( + self.entry, data={**self.device_config, **user_input} + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") + + try: + await self.client.pair(CLIENTID_PREFIX, NICKNAME) + except BraviaTVError: + return self.async_abort(reason="reauth_unsuccessful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_PIN, default=""): str, + vol.Required(CONF_USE_PSK, default=False): bool, + } + ), + ) + class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow): """Config flow options for Bravia TV.""" diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index f8128483852..46dd39bf470 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -9,6 +9,7 @@ from typing import Any, Final, TypeVar from pybravia import ( BraviaTV, + BraviaTVAuthError, BraviaTVConnectionError, BraviaTVConnectionTimeout, BraviaTVError, @@ -19,6 +20,7 @@ from typing_extensions import Concatenate, ParamSpec from homeassistant.components.media_player import MediaType from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -139,6 +141,8 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): _LOGGER.debug("Update skipped, Bravia API service is reloading") return raise UpdateFailed("Error communicating with device") from err + except BraviaTVAuthError as err: + raise ConfigEntryAuthFailed from err except (BraviaTVConnectionError, BraviaTVConnectionTimeout, BraviaTVTurnedOff): self.is_on = False self.connected = False diff --git a/homeassistant/components/braviatv/strings.json b/homeassistant/components/braviatv/strings.json index f6c35f2b8ca..4dd08135896 100644 --- a/homeassistant/components/braviatv/strings.json +++ b/homeassistant/components/braviatv/strings.json @@ -17,6 +17,13 @@ }, "confirm": { "description": "[%key:common::config_flow::description::confirm_setup%]" + }, + "reauth_confirm": { + "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check «Use PSK authentication» box and enter your PSK instead of PIN.", + "data": { + "pin": "[%key:common::config_flow::data::pin%]", + "use_psk": "Use PSK authentication" + } } }, "error": { @@ -28,7 +35,9 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_ip_control": "IP Control is disabled on your TV or the TV is not supported.", - "not_bravia_device": "The device is not a Bravia TV." + "not_bravia_device": "The device is not a Bravia TV.", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again." } }, "options": { diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index 64986e9d973..cc70d685b78 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -1,7 +1,13 @@ """Define tests for the Bravia TV config flow.""" from unittest.mock import patch -from pybravia import BraviaTVAuthError, BraviaTVConnectionError, BraviaTVNotSupported +from pybravia import ( + BraviaTVAuthError, + BraviaTVConnectionError, + BraviaTVError, + BraviaTVNotSupported, +) +import pytest from homeassistant import data_entry_flow from homeassistant.components import ssdp @@ -10,7 +16,7 @@ from homeassistant.components.braviatv.const import ( CONF_USE_PSK, DOMAIN, ) -from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN from tests.common import MockConfigEntry @@ -222,12 +228,13 @@ async def test_authorize_model_unsupported(hass): async def test_authorize_no_ip_control(hass): """Test that errors are shown when IP Control is disabled on the TV.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} - ) + with patch("pybravia.BraviaTV.pair", side_effect=BraviaTVError): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} + ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "no_ip_control" + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "no_ip_control" async def test_duplicate_error(hass): @@ -398,3 +405,110 @@ async def test_options_flow(hass): assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_IGNORED_SOURCES: ["HDMI 1", "HDMI 2"]} + + +@pytest.mark.parametrize( + "user_input", + [{CONF_PIN: "mypsk", CONF_USE_PSK: True}, {CONF_PIN: "1234", CONF_USE_PSK: False}], +) +async def test_reauth_successful(hass, user_input): + """Test starting a reauthentication flow.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="very_unique_string", + data={ + CONF_HOST: "bravia-host", + CONF_PIN: "1234", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + }, + title="TV-Model", + ) + config_entry.add_to_hass(hass) + + with patch("pybravia.BraviaTV.connect"), patch( + "pybravia.BraviaTV.get_power_status", + return_value="active", + ), patch( + "pybravia.BraviaTV.get_external_status", + return_value=BRAVIA_SOURCES, + ), patch( + "pybravia.BraviaTV.send_rest_req", + return_value={}, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": config_entry.entry_id}, + data=config_entry.data, + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=user_input, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + +async def test_reauth_unsuccessful(hass): + """Test reauthentication flow failed.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="very_unique_string", + data={ + CONF_HOST: "bravia-host", + CONF_PIN: "1234", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + }, + title="TV-Model", + ) + config_entry.add_to_hass(hass) + + with patch( + "pybravia.BraviaTV.connect", + side_effect=BraviaTVAuthError, + ), patch("pybravia.BraviaTV.pair"): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": config_entry.entry_id}, + data=config_entry.data, + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PIN: "mypsk", CONF_USE_PSK: True}, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "reauth_unsuccessful" + + +async def test_reauth_unsuccessful_during_pairing(hass): + """Test reauthentication flow failed because of pairing error.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="very_unique_string", + data={ + CONF_HOST: "bravia-host", + CONF_PIN: "1234", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + }, + title="TV-Model", + ) + config_entry.add_to_hass(hass) + + with patch("pybravia.BraviaTV.pair", side_effect=BraviaTVError): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": config_entry.entry_id}, + data=config_entry.data, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "reauth_unsuccessful" From 3a8282d0c5f7c2f72b677e84395543797418ba1a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 2 Oct 2022 17:24:06 -0400 Subject: [PATCH 112/985] Improve zwave_js service error (#79504) --- homeassistant/components/zwave_js/services.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 63a9071ffb6..2dfeaaa4a8d 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -88,13 +88,14 @@ def raise_exceptions_from_results( if errors := [ tup for tup in zip(zwave_objects, results) if isinstance(tup[1], Exception) ]: - lines = ( - f"{len(errors)} error(s):", + lines = [ *( f"{zwave_object} - {error.__class__.__name__}: {error.args[0]}" for zwave_object, error in errors - ), - ) + ) + ] + if len(lines) > 1: + lines.insert(0, f"{len(errors)} error(s):") raise HomeAssistantError("\n".join(lines)) From 12358f24464418698f72c888515b19d366576263 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 3 Oct 2022 00:31:05 +0000 Subject: [PATCH 113/985] [ci skip] Translation update --- .../airthings_ble/translations/it.json | 23 ++++++++++++++++ .../airthings_ble/translations/pl.json | 22 ++++++++++++++++ .../components/apcupsd/translations/it.json | 26 +++++++++++++++++++ .../components/awair/translations/pl.json | 2 +- .../components/bayesian/translations/it.json | 12 +++++++++ .../components/braviatv/translations/ca.json | 10 ++++++- .../components/braviatv/translations/en.json | 11 +++++++- .../components/braviatv/translations/fr.json | 10 ++++++- .../braviatv/translations/pt-BR.json | 11 +++++++- .../dsmr_reader/translations/it.json | 18 +++++++++++++ .../components/ezviz/translations/it.json | 10 +++---- .../components/ezviz/translations/ru.json | 10 +++---- .../forked_daapd/translations/it.json | 14 +++++----- .../forked_daapd/translations/ru.json | 4 +-- .../google_sheets/translations/it.json | 4 +++ .../translations/sensor.pl.json | 10 +++++++ .../litterrobot/translations/it.json | 6 +++++ .../components/mikrotik/translations/ca.json | 10 ++++++- .../components/mikrotik/translations/de.json | 10 ++++++- .../components/mikrotik/translations/el.json | 10 ++++++- .../components/mikrotik/translations/es.json | 10 ++++++- .../components/mikrotik/translations/et.json | 10 ++++++- .../components/mikrotik/translations/fr.json | 10 ++++++- .../components/mikrotik/translations/hu.json | 10 ++++++- .../components/mikrotik/translations/id.json | 10 ++++++- .../components/mikrotik/translations/it.json | 10 ++++++- .../components/mikrotik/translations/nl.json | 9 ++++++- .../components/mikrotik/translations/pl.json | 10 ++++++- .../mikrotik/translations/pt-BR.json | 10 ++++++- .../components/mikrotik/translations/ru.json | 10 ++++++- .../mikrotik/translations/zh-Hant.json | 10 ++++++- .../components/moon/translations/it.json | 6 +++++ .../components/octoprint/translations/ca.json | 6 +++++ .../components/octoprint/translations/de.json | 6 +++++ .../components/octoprint/translations/el.json | 6 +++++ .../components/octoprint/translations/en.json | 10 +++---- .../components/octoprint/translations/es.json | 6 +++++ .../components/octoprint/translations/et.json | 6 +++++ .../components/octoprint/translations/fr.json | 6 +++++ .../components/octoprint/translations/hu.json | 6 +++++ .../components/octoprint/translations/id.json | 6 +++++ .../components/octoprint/translations/it.json | 6 +++++ .../components/octoprint/translations/nl.json | 6 +++++ .../components/octoprint/translations/pl.json | 6 +++++ .../octoprint/translations/pt-BR.json | 6 +++++ .../components/octoprint/translations/ru.json | 6 +++++ .../octoprint/translations/zh-Hant.json | 6 +++++ .../components/roomba/translations/it.json | 4 +-- .../components/roomba/translations/ru.json | 2 +- .../components/season/translations/it.json | 6 +++++ .../components/sensor/translations/id.json | 2 +- .../components/sensor/translations/it.json | 12 +++++++-- .../components/tautulli/translations/it.json | 1 + .../transmission/translations/ru.json | 2 +- .../components/uptime/translations/it.json | 6 +++++ .../yalexs_ble/translations/pl.json | 2 +- .../components/zha/translations/it.json | 2 ++ 57 files changed, 430 insertions(+), 51 deletions(-) create mode 100644 homeassistant/components/airthings_ble/translations/it.json create mode 100644 homeassistant/components/airthings_ble/translations/pl.json create mode 100644 homeassistant/components/apcupsd/translations/it.json create mode 100644 homeassistant/components/bayesian/translations/it.json create mode 100644 homeassistant/components/dsmr_reader/translations/it.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.pl.json diff --git a/homeassistant/components/airthings_ble/translations/it.json b/homeassistant/components/airthings_ble/translations/it.json new file mode 100644 index 00000000000..90e1ecdeee8 --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "cannot_connect": "Impossibile connettersi", + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "unknown": "Errore imprevisto" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/pl.json b/homeassistant/components/airthings_ble/translations/pl.json new file mode 100644 index 00000000000..2efd17b5aaf --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "B\u0142\u0105d po\u0142\u0105czenia", + "no_devices_found": "Nie znaleziono \u017cadnych urz\u0105dze\u0144", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz instalowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/it.json b/homeassistant/components/apcupsd/translations/it.json new file mode 100644 index 00000000000..ad68159e956 --- /dev/null +++ b/homeassistant/components/apcupsd/translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "no_status": "Nessuno stato viene segnalato da Host" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta" + }, + "description": "Immettere l'host e la porta su cui viene servito il NIS apcupsd." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione del demone APC UPS tramite YAML \u00e8 stata rimossa. \n\nLa tua configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente. \n\nRimuovere la configurazione YAML di APC UPS Daemon dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di APC UPS Daemon \u00e8 stata rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/pl.json b/homeassistant/components/awair/translations/pl.json index 7bd01486f88..7c9dba59e06 100644 --- a/homeassistant/components/awair/translations/pl.json +++ b/homeassistant/components/awair/translations/pl.json @@ -29,7 +29,7 @@ "data": { "host": "Adres IP" }, - "description": "Lokalny interfejs API Awair musi by\u0107 w\u0142\u0105czony, wykonuj\u0105c nast\u0119puj\u0105ce czynno\u015bci: {url}" + "description": "Post\u0119puj zgodnie z [tymi instrukcjami]( {url} ), aby dowiedzie\u0107 si\u0119, jak w\u0142\u0105czy\u0107 lokalny interfejs API Awair. \n\n Po zako\u0144czeniu kliknij Prze\u015blij." }, "local_pick": { "data": { diff --git a/homeassistant/components/bayesian/translations/it.json b/homeassistant/components/bayesian/translations/it.json new file mode 100644 index 00000000000..0b162649e4c --- /dev/null +++ b/homeassistant/components/bayesian/translations/it.json @@ -0,0 +1,12 @@ +{ + "issues": { + "manual_migration": { + "description": "L'integrazione bayesiana ora aggiorna anche la probabilit\u00e0 se l'osservato `to_state`, `above`, `below` o `value_template` restituisce `False` piuttosto che solo `True`. Quindi non \u00e8 pi\u00f9 necessario avere voci duplicate e complementari per ogni stato binario. Rimuovere la voce duplicata per `{entity}`.", + "title": "Correzione YAML manuale richiesta per Bayesian" + }, + "no_prob_given_false": { + "description": "Nell'integrazione bayesiana `prob_given_false` \u00e8 ora una variabile di configurazione richiesta in quanto non vi era alcuna logica matematica per il precedente valore predefinito. Aggiungilo al tuo `configuration.yml` per `bayesian/{entity}`. Queste osservazioni saranno ignorate fino a quando non lo farai.", + "title": "Aggiunta manuale YAML richiesta per Bayesian" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/ca.json b/homeassistant/components/braviatv/translations/ca.json index 6c8c9736750..08599a29a06 100644 --- a/homeassistant/components/braviatv/translations/ca.json +++ b/homeassistant/components/braviatv/translations/ca.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", "no_ip_control": "El control IP del teu televisor est\u00e0 desactivat o aquest no \u00e9s compatible.", - "not_bravia_device": "El dispositiu no \u00e9s un televisor Bravia." + "not_bravia_device": "El dispositiu no \u00e9s un televisor Bravia.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "reauth_unsuccessful": "La re-autenticaci\u00f3 no ha tingut \u00e8xit, elimina la integraci\u00f3 i torna-la a configurar." }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -23,6 +25,12 @@ "confirm": { "description": "Vols comen\u00e7ar la configuraci\u00f3?" }, + "reauth_confirm": { + "data": { + "pin": "Codi PIN", + "use_psk": "Utilitza autenticaci\u00f3 PSK" + } + }, "user": { "data": { "host": "Amfitri\u00f3" diff --git a/homeassistant/components/braviatv/translations/en.json b/homeassistant/components/braviatv/translations/en.json index 7401fda7324..c3341d33112 100644 --- a/homeassistant/components/braviatv/translations/en.json +++ b/homeassistant/components/braviatv/translations/en.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Device is already configured", "no_ip_control": "IP Control is disabled on your TV or the TV is not supported.", - "not_bravia_device": "The device is not a Bravia TV." + "not_bravia_device": "The device is not a Bravia TV.", + "reauth_successful": "Re-authentication was successful", + "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again." }, "error": { "cannot_connect": "Failed to connect", @@ -23,6 +25,13 @@ "confirm": { "description": "Do you want to start set up?" }, + "reauth_confirm": { + "data": { + "pin": "PIN Code", + "use_psk": "Use PSK authentication" + }, + "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check \u00abUse PSK authentication\u00bb box and enter your PSK instead of PIN." + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/fr.json b/homeassistant/components/braviatv/translations/fr.json index 73a32d0a06a..40445ec8062 100644 --- a/homeassistant/components/braviatv/translations/fr.json +++ b/homeassistant/components/braviatv/translations/fr.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "no_ip_control": "Le contr\u00f4le IP est d\u00e9sactiv\u00e9 sur votre t\u00e9l\u00e9viseur ou le t\u00e9l\u00e9viseur n'est pas pris en charge.", - "not_bravia_device": "L'appareil n'est pas un t\u00e9l\u00e9viseur Bravia." + "not_bravia_device": "L'appareil n'est pas un t\u00e9l\u00e9viseur Bravia.", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "reauth_unsuccessful": "La r\u00e9authentification a \u00e9chou\u00e9, veuillez supprimer l'int\u00e9gration puis la configurer \u00e0 nouveau." }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -23,6 +25,12 @@ "confirm": { "description": "Voulez-vous commencer la configuration\u00a0?" }, + "reauth_confirm": { + "data": { + "pin": "Code PIN", + "use_psk": "Utiliser l'authentification PSK" + } + }, "user": { "data": { "host": "H\u00f4te" diff --git a/homeassistant/components/braviatv/translations/pt-BR.json b/homeassistant/components/braviatv/translations/pt-BR.json index a6aef38d17f..7c5af6e2694 100644 --- a/homeassistant/components/braviatv/translations/pt-BR.json +++ b/homeassistant/components/braviatv/translations/pt-BR.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "no_ip_control": "O Controle de IP est\u00e1 desativado em sua TV ou a TV n\u00e3o \u00e9 compat\u00edvel.", - "not_bravia_device": "O dispositivo n\u00e3o \u00e9 uma TV Bravia." + "not_bravia_device": "O dispositivo n\u00e3o \u00e9 uma TV Bravia.", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "reauth_unsuccessful": "A reautentica\u00e7\u00e3o falhou. Remova a integra\u00e7\u00e3o e configure-a novamente." }, "error": { "cannot_connect": "Falha ao conectar", @@ -23,6 +25,13 @@ "confirm": { "description": "Deseja iniciar a configura\u00e7\u00e3o?" }, + "reauth_confirm": { + "data": { + "pin": "C\u00f3digo PIN", + "use_psk": "Usar autentica\u00e7\u00e3o PSK" + }, + "description": "Digite o c\u00f3digo PIN mostrado na TV Sony Bravia. \n\n Se o c\u00f3digo PIN n\u00e3o for exibido, voc\u00ea deve cancelar o registro do Home Assistant na sua TV, v\u00e1 para: Configura\u00e7\u00f5es - > Rede - > Configura\u00e7\u00f5es do dispositivo remoto - > Cancelar o registro do dispositivo remoto. \n\n Voc\u00ea pode usar PSK (Pre-Shared-Key) em vez de PIN. PSK \u00e9 uma chave secreta definida pelo usu\u00e1rio usada para controle de acesso. Este m\u00e9todo de autentica\u00e7\u00e3o \u00e9 recomendado como mais est\u00e1vel. Para ativar o PSK em sua TV, v\u00e1 para: Configura\u00e7\u00f5es - > Rede - > Configura\u00e7\u00e3o de rede dom\u00e9stica - > Controle de IP. Em seguida, marque a caixa \u00abUsar autentica\u00e7\u00e3o PSK\u00bb e digite seu PSK em vez do PIN." + }, "user": { "data": { "host": "Nome do host" diff --git a/homeassistant/components/dsmr_reader/translations/it.json b/homeassistant/components/dsmr_reader/translations/it.json new file mode 100644 index 00000000000..55e13fc78bb --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "step": { + "confirm": { + "description": "Assicurarsi di configurare le origini dati \"split topic\" in DSMR Reader." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di DSMR Reader tramite YAML \u00e8 stata rimossa. \n\nLa tua configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente. \n\nRimuovere la configurazione YAML di DSMR Reader dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione del lettore DSMR \u00e8 stata rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/it.json b/homeassistant/components/ezviz/translations/it.json index febba0cad51..9ef6bb4b6c2 100644 --- a/homeassistant/components/ezviz/translations/it.json +++ b/homeassistant/components/ezviz/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "L'account \u00e8 gi\u00e0 configurato", - "ezviz_cloud_account_missing": "Ezviz cloud account mancante. Riconfigura l'account Ezviz cloud", + "ezviz_cloud_account_missing": "Account EZVIZ cloud mancante. Si prega di riconfigurare l'account EZVIZ cloud", "unknown": "Errore imprevisto" }, "error": { @@ -17,8 +17,8 @@ "password": "Password", "username": "Nome utente" }, - "description": "Inserisci le credenziali RTSP per la videocamera Ezviz {serial} con IP {ip_address}", - "title": "Rilevata videocamera Ezviz" + "description": "Inserisci le credenziali RTSP per la videocamera EZVIZ {serial} con IP {ip_address}", + "title": "Rilevata videocamera EZVIZ" }, "user": { "data": { @@ -26,7 +26,7 @@ "url": "URL", "username": "Nome utente" }, - "title": "Connettiti a Ezviz Cloud" + "title": "Connettiti a EZVIZ Cloud" }, "user_custom_url": { "data": { @@ -35,7 +35,7 @@ "username": "Nome utente" }, "description": "Specifica manualmente l'URL dell'area geografica", - "title": "Connettiti all'URL personalizzato di Ezviz" + "title": "Connettiti all'URL EZVIZ personalizzato" } } }, diff --git a/homeassistant/components/ezviz/translations/ru.json b/homeassistant/components/ezviz/translations/ru.json index c03bbe22dae..13bdf601817 100644 --- a/homeassistant/components/ezviz/translations/ru.json +++ b/homeassistant/components/ezviz/translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", - "ezviz_cloud_account_missing": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0443\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Ezviz Cloud. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0435\u0451.", + "ezviz_cloud_account_missing": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0443\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c EZVIZ Cloud. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0435\u0451.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { @@ -17,8 +17,8 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 RTSP \u0434\u043b\u044f \u043a\u0430\u043c\u0435\u0440\u044b Ezviz {serial} \u0441 IP-\u0430\u0434\u0440\u0435\u0441\u043e\u043c {ip_address}", - "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u0430\u044f \u043a\u0430\u043c\u0435\u0440\u0430 Ezviz" + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 RTSP \u0434\u043b\u044f \u043a\u0430\u043c\u0435\u0440\u044b EZVIZ {serial} \u0441 IP-\u0430\u0434\u0440\u0435\u0441\u043e\u043c {ip_address}", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u0430\u044f \u043a\u0430\u043c\u0435\u0440\u0430 EZVIZ" }, "user": { "data": { @@ -26,7 +26,7 @@ "url": "URL-\u0430\u0434\u0440\u0435\u0441", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Ezviz Cloud" + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a EZVIZ Cloud" }, "user_custom_url": { "data": { @@ -35,7 +35,7 @@ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 URL-\u0430\u0434\u0440\u0435\u0441 \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u0440\u0435\u0433\u0438\u043e\u043d\u0430 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.", - "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u043c\u0443 URL-\u0430\u0434\u0440\u0435\u0441\u0443 Ezviz" + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u043c\u0443 URL-\u0430\u0434\u0440\u0435\u0441\u0443 EZVIZ" } } }, diff --git a/homeassistant/components/forked_daapd/translations/it.json b/homeassistant/components/forked_daapd/translations/it.json index a5a1d092281..107d6cb8eef 100644 --- a/homeassistant/components/forked_daapd/translations/it.json +++ b/homeassistant/components/forked_daapd/translations/it.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "not_forked_daapd": "Il dispositivo non \u00e8 un server forked-daapd." + "not_forked_daapd": "Il dispositivo non \u00e8 un server Owntone." }, "error": { - "forbidden": "Impossibile connettersi. Controlla i permessi di rete forked-daapd.", + "forbidden": "Impossibile connetersi. Controlla le autorizzazioni di rete di Owntone.", "unknown_error": "Errore imprevisto", - "websocket_not_enabled": "websocket del server forked-daapd non abilitato.", + "websocket_not_enabled": "Websocket del server Owntone non abilitato.", "wrong_host_or_port": "Impossibile connettersi. Controlla host e porta.", "wrong_password": "Password errata", - "wrong_server_type": "L'integrazione forked-daapd richiede un server forked-daapd con versione >= 27.0." + "wrong_server_type": "L'integrazione Owntone richiede un server Owntone con versione >= 27.0." }, "flow_title": "{name} ({host})", "step": { @@ -21,7 +21,7 @@ "password": "Password API (lascia vuota se non c'\u00e8 password)", "port": "Porta API" }, - "title": "Configura il dispositivo forked-daapd" + "title": "Configura dispositivo Owntone" } } }, @@ -34,8 +34,8 @@ "tts_pause_time": "Secondi di pausa prima e dopo il TTS", "tts_volume": "Volume TTS (variabile nell'intervallo [0,1])" }, - "description": "Imposta le varie opzioni per l'integrazione forked-daapd.", - "title": "Configura le opzioni forked-daapd" + "description": "Imposta varie opzioni per l'integrazione Owntone.", + "title": "Configurare le opzioni Owntone" } } } diff --git a/homeassistant/components/forked_daapd/translations/ru.json b/homeassistant/components/forked_daapd/translations/ru.json index 3850c895353..ba8946ed112 100644 --- a/homeassistant/components/forked_daapd/translations/ru.json +++ b/homeassistant/components/forked_daapd/translations/ru.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", - "not_forked_daapd": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 forked-daapd." + "not_forked_daapd": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 Owntone." }, "error": { "forbidden": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u044f forked-daapd.", "unknown_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", - "websocket_not_enabled": "\u0412\u0435\u0431-\u0441\u043e\u043a\u0435\u0442 forked-daapd \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d.", + "websocket_not_enabled": "\u0412\u0435\u0431-\u0441\u043e\u043a\u0435\u0442 Owntone \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d.", "wrong_host_or_port": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430.", "wrong_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.", "wrong_server_type": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0432\u0435\u0440 forked-daapd \u0432\u0435\u0440\u0441\u0438\u0438 27.0 \u0438\u043b\u0438 \u0432\u044b\u0448\u0435." diff --git a/homeassistant/components/google_sheets/translations/it.json b/homeassistant/components/google_sheets/translations/it.json index 6d8f315a84a..296899ad741 100644 --- a/homeassistant/components/google_sheets/translations/it.json +++ b/homeassistant/components/google_sheets/translations/it.json @@ -25,6 +25,10 @@ }, "pick_implementation": { "title": "Scegli il metodo di autenticazione" + }, + "reauth_confirm": { + "description": "L'integrazione di Fogli Google deve riautenticare il tuo account", + "title": "Autentica nuovamente l'integrazione" } } } diff --git a/homeassistant/components/homekit_controller/translations/sensor.pl.json b/homeassistant/components/homekit_controller/translations/sensor.pl.json new file mode 100644 index 00000000000..a3a251cbc6f --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.pl.json @@ -0,0 +1,10 @@ +{ + "state": { + "homekit_controller__thread_status": { + "child": "Dziecko", + "detached": "Od\u0142\u0105czony", + "joining": "Do\u0142\u0105czanie", + "router": "Router" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/it.json b/homeassistant/components/litterrobot/translations/it.json index 8b8ea9c03bb..7e09efc0610 100644 --- a/homeassistant/components/litterrobot/translations/it.json +++ b/homeassistant/components/litterrobot/translations/it.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "Gli attributi dell'entit\u00e0 aspirapolvere sono ora disponibili come sensori diagnostici. \n\nModifica eventuali automazioni o script che potresti avere che utilizzano questi attributi.", + "title": "Gli attributi Litter-Robot sono ora sensori propri" + } } } \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/ca.json b/homeassistant/components/mikrotik/translations/ca.json index f7adef2f885..41bafc4d32f 100644 --- a/homeassistant/components/mikrotik/translations/ca.json +++ b/homeassistant/components/mikrotik/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", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -9,6 +10,13 @@ "name_exists": "El nom existeix" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "La contrasenya de {username} \u00e9s inv\u00e0lida.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "host": "Amfitri\u00f3", diff --git a/homeassistant/components/mikrotik/translations/de.json b/homeassistant/components/mikrotik/translations/de.json index 1a9c3b5d352..6ecdf66989f 100644 --- a/homeassistant/components/mikrotik/translations/de.json +++ b/homeassistant/components/mikrotik/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -9,6 +10,13 @@ "name_exists": "Name vorhanden" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Das Passwort f\u00fcr {username} ist ung\u00fcltig.", + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/mikrotik/translations/el.json b/homeassistant/components/mikrotik/translations/el.json index 4faca6d756e..4dfffe98932 100644 --- a/homeassistant/components/mikrotik/translations/el.json +++ b/homeassistant/components/mikrotik/translations/el.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", @@ -9,6 +10,13 @@ "name_exists": "\u03a4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {username} \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2.", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, "user": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", diff --git a/homeassistant/components/mikrotik/translations/es.json b/homeassistant/components/mikrotik/translations/es.json index 13bea3f0be0..66b5c626a5c 100644 --- a/homeassistant/components/mikrotik/translations/es.json +++ b/homeassistant/components/mikrotik/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", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", @@ -9,6 +10,13 @@ "name_exists": "El nombre existe" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "La contrase\u00f1a para {username} no es v\u00e1lida.", + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/mikrotik/translations/et.json b/homeassistant/components/mikrotik/translations/et.json index bdd6c393b0c..fc7c8dfb4e4 100644 --- a/homeassistant/components/mikrotik/translations/et.json +++ b/homeassistant/components/mikrotik/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", @@ -9,6 +10,13 @@ "name_exists": "Nimi on juba olemas" }, "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Kasutaja {username} salas\u00f5na on kehtetu", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "host": "", diff --git a/homeassistant/components/mikrotik/translations/fr.json b/homeassistant/components/mikrotik/translations/fr.json index 5d0f2786eff..a627d3c15e3 100644 --- a/homeassistant/components/mikrotik/translations/fr.json +++ b/homeassistant/components/mikrotik/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", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -9,6 +10,13 @@ "name_exists": "Le nom existe" }, "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "description": "Le mot de passe pour {username} n'est pas valide.", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, "user": { "data": { "host": "H\u00f4te", diff --git a/homeassistant/components/mikrotik/translations/hu.json b/homeassistant/components/mikrotik/translations/hu.json index c15ba2f07aa..b224f163b64 100644 --- a/homeassistant/components/mikrotik/translations/hu.json +++ b/homeassistant/components/mikrotik/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -9,6 +10,13 @@ "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik" }, "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "{username} jelszava \u00e9rv\u00e9nytelen.", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "host": "C\u00edm", diff --git a/homeassistant/components/mikrotik/translations/id.json b/homeassistant/components/mikrotik/translations/id.json index 3ef0dacb763..e6166ef9ed2 100644 --- a/homeassistant/components/mikrotik/translations/id.json +++ b/homeassistant/components/mikrotik/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", @@ -9,6 +10,13 @@ "name_exists": "Nama sudah ada" }, "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Kata sandi untuk {username} tidak valid.", + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/mikrotik/translations/it.json b/homeassistant/components/mikrotik/translations/it.json index 203b13ff2d3..297d9728b70 100644 --- a/homeassistant/components/mikrotik/translations/it.json +++ b/homeassistant/components/mikrotik/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", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", @@ -9,6 +10,13 @@ "name_exists": "Il Nome esiste gi\u00e0" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "La password per {username} non \u00e8 valida.", + "title": "Autentica nuovamente l'integrazione" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/mikrotik/translations/nl.json b/homeassistant/components/mikrotik/translations/nl.json index 78e143ddadb..daa70c9e3a1 100644 --- a/homeassistant/components/mikrotik/translations/nl.json +++ b/homeassistant/components/mikrotik/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -9,6 +10,12 @@ "name_exists": "Naam bestaat al" }, "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "title": "Integratie herauthenticeren" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/mikrotik/translations/pl.json b/homeassistant/components/mikrotik/translations/pl.json index 8f056e29de6..a8bf06df15f 100644 --- a/homeassistant/components/mikrotik/translations/pl.json +++ b/homeassistant/components/mikrotik/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowna autoryzacja przebieg\u0142a pomy\u015blnie" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -9,6 +10,13 @@ "name_exists": "Nazwa ju\u017c istnieje" }, "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Has\u0142o u\u017cytkownika {username} jest nieprawid\u0142owe.", + "title": "Ponownie autoryzuj integracje" + }, "user": { "data": { "host": "Nazwa hosta lub adres IP", diff --git a/homeassistant/components/mikrotik/translations/pt-BR.json b/homeassistant/components/mikrotik/translations/pt-BR.json index 0fb66a063bd..923ebc26806 100644 --- a/homeassistant/components/mikrotik/translations/pt-BR.json +++ b/homeassistant/components/mikrotik/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "cannot_connect": "Falha ao conectar", @@ -9,6 +10,13 @@ "name_exists": "O nome j\u00e1 existe" }, "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "A senha para {username} \u00e9 inv\u00e1lida.", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, "user": { "data": { "host": "Nome do host", diff --git a/homeassistant/components/mikrotik/translations/ru.json b/homeassistant/components/mikrotik/translations/ru.json index 015d2061c76..2c72c3f4aa1 100644 --- a/homeassistant/components/mikrotik/translations/ru.json +++ b/homeassistant/components/mikrotik/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.", + "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.", @@ -9,6 +10,13 @@ "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f {username}.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/mikrotik/translations/zh-Hant.json b/homeassistant/components/mikrotik/translations/zh-Hant.json index 3872814e417..d77f3c51dff 100644 --- a/homeassistant/components/mikrotik/translations/zh-Hant.json +++ b/homeassistant/components/mikrotik/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -9,6 +10,13 @@ "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "{username} \u5bc6\u78bc\u7121\u6548\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", diff --git a/homeassistant/components/moon/translations/it.json b/homeassistant/components/moon/translations/it.json index af891532233..f510b6b5837 100644 --- a/homeassistant/components/moon/translations/it.json +++ b/homeassistant/components/moon/translations/it.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "La configurazione di Moon tramite YAML \u00e8 stata rimossa. \n\nLa tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Moon \u00e8 stata rimossa" + } + }, "title": "Luna" } \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/ca.json b/homeassistant/components/octoprint/translations/ca.json index 2e7665ea7ec..a8b4512da3c 100644 --- a/homeassistant/components/octoprint/translations/ca.json +++ b/homeassistant/components/octoprint/translations/ca.json @@ -4,6 +4,7 @@ "already_configured": "El dispositiu ja est\u00e0 configurat", "auth_failed": "No s'ha pogut obtenir la clau API de l'aplicaci\u00f3", "cannot_connect": "Ha fallat la connexi\u00f3", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "unknown": "Error inesperat" }, "error": { @@ -15,6 +16,11 @@ "get_api_key": "Obre la interf\u00edcie d'usuari d'OctoPrint i clica a 'Permet' a la sol\u00b7licitud d'acc\u00e9s de 'Home Assistant'." }, "step": { + "reauth_confirm": { + "data": { + "username": "Nom d'usuari" + } + }, "user": { "data": { "host": "Amfitri\u00f3", diff --git a/homeassistant/components/octoprint/translations/de.json b/homeassistant/components/octoprint/translations/de.json index 8cbe6846950..782920e2959 100644 --- a/homeassistant/components/octoprint/translations/de.json +++ b/homeassistant/components/octoprint/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "auth_failed": "Fehler beim Abrufen des Anwendungs-API-Schl\u00fcssels", "cannot_connect": "Verbindung fehlgeschlagen", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "unknown": "Unerwarteter Fehler" }, "error": { @@ -15,6 +16,11 @@ "get_api_key": "\u00d6ffne die OctoPrint-Benutzeroberfl\u00e4che und dr\u00fccke bei der Zugriffsanfrage f\u00fcr \"Home Assistant\" auf \"Zulassen\"." }, "step": { + "reauth_confirm": { + "data": { + "username": "Benutzername" + } + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/octoprint/translations/el.json b/homeassistant/components/octoprint/translations/el.json index 60dc47229e2..f253e2e28d0 100644 --- a/homeassistant/components/octoprint/translations/el.json +++ b/homeassistant/components/octoprint/translations/el.json @@ -4,6 +4,7 @@ "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "auth_failed": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd api \u03c4\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { @@ -15,6 +16,11 @@ "get_api_key": "\u0391\u03bd\u03bf\u03af\u03be\u03c4\u03b5 \u03c4\u03bf OctoPrint UI \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf 'Allow' \u03c3\u03c4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf 'Home Assistant'." }, "step": { + "reauth_confirm": { + "data": { + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + }, "user": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", diff --git a/homeassistant/components/octoprint/translations/en.json b/homeassistant/components/octoprint/translations/en.json index 035274d6e9c..eb05cd7fae4 100644 --- a/homeassistant/components/octoprint/translations/en.json +++ b/homeassistant/components/octoprint/translations/en.json @@ -16,6 +16,11 @@ "get_api_key": "Open the OctoPrint UI and click 'Allow' on the Access Request for 'Home Assistant'." }, "step": { + "reauth_confirm": { + "data": { + "username": "Username" + } + }, "user": { "data": { "host": "Host", @@ -25,11 +30,6 @@ "username": "Username", "verify_ssl": "Verify SSL certificate" } - }, - "reauth_confirm": { - "data": { - "username": "Username" - } } } } diff --git a/homeassistant/components/octoprint/translations/es.json b/homeassistant/components/octoprint/translations/es.json index 827e29e0fde..427b0151166 100644 --- a/homeassistant/components/octoprint/translations/es.json +++ b/homeassistant/components/octoprint/translations/es.json @@ -4,6 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "auth_failed": "No se pudo recuperar la clave API de la aplicaci\u00f3n", "cannot_connect": "No se pudo conectar", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "unknown": "Error inesperado" }, "error": { @@ -15,6 +16,11 @@ "get_api_key": "Abre la interfaz de usuario de OctoPrint y haz clic en 'Permitir' en la solicitud de acceso para 'Home Assistant'." }, "step": { + "reauth_confirm": { + "data": { + "username": "Nombre de usuario" + } + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/octoprint/translations/et.json b/homeassistant/components/octoprint/translations/et.json index f27dd9d77aa..20a6832255d 100644 --- a/homeassistant/components/octoprint/translations/et.json +++ b/homeassistant/components/octoprint/translations/et.json @@ -4,6 +4,7 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "auth_failed": "Rakenduse API v\u00f5tme toomine nurjus", "cannot_connect": "\u00dchendamine nurjus", + "reauth_successful": "Taastuvastamine \u00f5nnestus", "unknown": "Ootamatu t\u00f5rge" }, "error": { @@ -15,6 +16,11 @@ "get_api_key": "Ava OctoPrinti kasutajaliides ja kl\u00f5psa Home Assistanti juurdep\u00e4\u00e4sutaotluses nuppu Luba." }, "step": { + "reauth_confirm": { + "data": { + "username": "Kasutajanimi" + } + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/octoprint/translations/fr.json b/homeassistant/components/octoprint/translations/fr.json index be78ad8b877..ecb057d3831 100644 --- a/homeassistant/components/octoprint/translations/fr.json +++ b/homeassistant/components/octoprint/translations/fr.json @@ -4,6 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "auth_failed": "\u00c9chec de la r\u00e9cup\u00e9ration de la cl\u00e9 API de l'application", "cannot_connect": "\u00c9chec de connexion", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", "unknown": "Erreur inattendue" }, "error": { @@ -15,6 +16,11 @@ "get_api_key": "Ouvrez l'interface utilisateur d'OctoPrint et cliquez sur \u00abAutoriser\u00bb sur la demande d'acc\u00e8s pour \u00abHome Assistant\u00bb." }, "step": { + "reauth_confirm": { + "data": { + "username": "Nom d'utilisateur" + } + }, "user": { "data": { "host": "H\u00f4te", diff --git a/homeassistant/components/octoprint/translations/hu.json b/homeassistant/components/octoprint/translations/hu.json index 3f283b51189..b1d293c2c3f 100644 --- a/homeassistant/components/octoprint/translations/hu.json +++ b/homeassistant/components/octoprint/translations/hu.json @@ -4,6 +4,7 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "auth_failed": "Nem siker\u00fclt lek\u00e9rni az alkalmaz\u00e1s api kulcs\u00e1t", "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { @@ -15,6 +16,11 @@ "get_api_key": "Nyissa meg az OctoPrint kezel\u0151 fel\u00fclet\u00e9t, \u00e9s kattintson az 'Allow' gombra a 'Home Assistant' hozz\u00e1f\u00e9r\u00e9si k\u00e9relemn\u00e9l." }, "step": { + "reauth_confirm": { + "data": { + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + }, "user": { "data": { "host": "C\u00edm", diff --git a/homeassistant/components/octoprint/translations/id.json b/homeassistant/components/octoprint/translations/id.json index 34675967d4f..f697193c399 100644 --- a/homeassistant/components/octoprint/translations/id.json +++ b/homeassistant/components/octoprint/translations/id.json @@ -4,6 +4,7 @@ "already_configured": "Perangkat sudah dikonfigurasi", "auth_failed": "Gagal mengambil kunci API aplikasi", "cannot_connect": "Gagal terhubung", + "reauth_successful": "Autentikasi ulang berhasil", "unknown": "Kesalahan yang tidak diharapkan" }, "error": { @@ -15,6 +16,11 @@ "get_api_key": "Buka antarmuka OctoPrint dan klik 'Izinkan' pada Permintaan Akses untuk 'Home Assistant'." }, "step": { + "reauth_confirm": { + "data": { + "username": "Nama Pengguna" + } + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/octoprint/translations/it.json b/homeassistant/components/octoprint/translations/it.json index 639b304417d..0c7d1f1159b 100644 --- a/homeassistant/components/octoprint/translations/it.json +++ b/homeassistant/components/octoprint/translations/it.json @@ -4,6 +4,7 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "auth_failed": "Impossibile recuperare la chiave API dell'applicazione", "cannot_connect": "Impossibile connettersi", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "unknown": "Errore imprevisto" }, "error": { @@ -15,6 +16,11 @@ "get_api_key": "Apri l'interfaccia utente di OctoPrint e fai clic su \"Consenti\" nella richiesta di accesso per \"Home Assistant\"." }, "step": { + "reauth_confirm": { + "data": { + "username": "Nome utente" + } + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/octoprint/translations/nl.json b/homeassistant/components/octoprint/translations/nl.json index fa8f5edc01a..59453531503 100644 --- a/homeassistant/components/octoprint/translations/nl.json +++ b/homeassistant/components/octoprint/translations/nl.json @@ -4,6 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd", "auth_failed": "Kan applicatie API-sleutel niet ophalen", "cannot_connect": "Kan geen verbinding maken", + "reauth_successful": "Herauthenticatie geslaagd", "unknown": "Onverwachte fout" }, "error": { @@ -15,6 +16,11 @@ "get_api_key": "Open de OctoPrint UI en klik op 'Toestaan' op het toegangsverzoek voor 'Home Assistant'." }, "step": { + "reauth_confirm": { + "data": { + "username": "Gebruikersnaam" + } + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/octoprint/translations/pl.json b/homeassistant/components/octoprint/translations/pl.json index e6a0c3ad680..fcb8c5fbbb0 100644 --- a/homeassistant/components/octoprint/translations/pl.json +++ b/homeassistant/components/octoprint/translations/pl.json @@ -4,6 +4,7 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "auth_failed": "Nie uda\u0142o si\u0119 pobra\u0107 klucza API aplikacji", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "reauth_successful": "Ponowna autoryzacja przebieg\u0142a pomy\u015blnie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { @@ -15,6 +16,11 @@ "get_api_key": "Otw\u00f3rz interfejs OctoPrint i kliknij \u201eZezw\u00f3l\u201d przy \u017c\u0105daniu dost\u0119pu do Home Assistanta." }, "step": { + "reauth_confirm": { + "data": { + "username": "Nazwa u\u017cytkownika" + } + }, "user": { "data": { "host": "Nazwa hosta lub adres IP", diff --git a/homeassistant/components/octoprint/translations/pt-BR.json b/homeassistant/components/octoprint/translations/pt-BR.json index f8af97f7526..39cc45bb8a6 100644 --- a/homeassistant/components/octoprint/translations/pt-BR.json +++ b/homeassistant/components/octoprint/translations/pt-BR.json @@ -4,6 +4,7 @@ "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "auth_failed": "Falha ao recuperar a chave de API do aplicativo", "cannot_connect": "Falha ao conectar", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", "unknown": "Erro inesperado" }, "error": { @@ -15,6 +16,11 @@ "get_api_key": "Abra a interface do usu\u00e1rio do OctoPrint e clique em 'Permitir' na solicita\u00e7\u00e3o de acesso para 'Assistente dom\u00e9stico'." }, "step": { + "reauth_confirm": { + "data": { + "username": "Nome de usu\u00e1rio" + } + }, "user": { "data": { "host": "Nome do host", diff --git a/homeassistant/components/octoprint/translations/ru.json b/homeassistant/components/octoprint/translations/ru.json index 48d99ebe673..c51f0e4a0dd 100644 --- a/homeassistant/components/octoprint/translations/ru.json +++ b/homeassistant/components/octoprint/translations/ru.json @@ -4,6 +4,7 @@ "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.", "auth_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "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.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { @@ -15,6 +16,11 @@ "get_api_key": "\u041e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u0432\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 OctoPrint \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 '\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c' \u0432 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0434\u043b\u044f 'Home Assistant'." }, "step": { + "reauth_confirm": { + "data": { + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/octoprint/translations/zh-Hant.json b/homeassistant/components/octoprint/translations/zh-Hant.json index f26a39d6a02..840e3665c57 100644 --- a/homeassistant/components/octoprint/translations/zh-Hant.json +++ b/homeassistant/components/octoprint/translations/zh-Hant.json @@ -4,6 +4,7 @@ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "auth_failed": "\u63a5\u6536\u61c9\u7528\u7a0b\u5f0f API \u91d1\u9470\u5931\u6557", "cannot_connect": "\u9023\u7dda\u5931\u6557", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { @@ -15,6 +16,11 @@ "get_api_key": "\u958b\u555f OctoPrint UI \u4e26\u65bc 'Home Assistant' \u5b58\u53d6\u8acb\u6c42\u4e0a\u9ede\u9078 '\u5141\u8a31'\u3002" }, "step": { + "reauth_confirm": { + "data": { + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", diff --git a/homeassistant/components/roomba/translations/it.json b/homeassistant/components/roomba/translations/it.json index b87a7cb8ef6..5882c8544c6 100644 --- a/homeassistant/components/roomba/translations/it.json +++ b/homeassistant/components/roomba/translations/it.json @@ -12,14 +12,14 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "Tieni premuto il pulsante Home su {name} fino a quando il dispositivo non genera un suono (circa due secondi), quindi invialo entro 30 secondi.", + "description": "Assicurati che l'app iRobot non sia in esecuzione su nessun dispositivo. Tieni premuto il pulsante Home su {name} finch\u00e9 il dispositivo non genera un suono (circa due secondi), quindi invia entro 30 secondi.", "title": "Recupera password" }, "link_manual": { "data": { "password": "Password" }, - "description": "La password non pu\u00f2 essere recuperata automaticamente dal dispositivo. Segui le istruzioni indicate sulla documentazione a: {auth_help_url}", + "description": "Non \u00e8 stato possibile recuperare automaticamente la password dal dispositivo. Assicurati che l'app iRobot non sia aperta su nessun dispositivo durante il tentativo di recuperare la password. Segui i passaggi descritti nella documentazione all'indirizzo: {auth_help_url}", "title": "Inserisci la password" }, "manual": { diff --git a/homeassistant/components/roomba/translations/ru.json b/homeassistant/components/roomba/translations/ru.json index 643da09744c..efba186e20c 100644 --- a/homeassistant/components/roomba/translations/ru.json +++ b/homeassistant/components/roomba/translations/ru.json @@ -19,7 +19,7 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, - "description": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \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: {auth_help_url}.", + "description": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \u041f\u0440\u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u043f\u0430\u0440\u043e\u043b\u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 iRobot \u043d\u0435 \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u043e\u0442\u043a\u0440\u044b\u0442\u043e \u043d\u0438 \u043d\u0430 \u043e\u0434\u043d\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435. \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: {auth_help_url}.", "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c" }, "manual": { diff --git a/homeassistant/components/season/translations/it.json b/homeassistant/components/season/translations/it.json index f77a7410705..8771365d3c5 100644 --- a/homeassistant/components/season/translations/it.json +++ b/homeassistant/components/season/translations/it.json @@ -10,5 +10,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "La configurazione di Season tramite YAML \u00e8 stata rimossa. \n\nLa tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Season \u00e8 stata rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/id.json b/homeassistant/components/sensor/translations/id.json index b8f85805ebc..e9e2340fed0 100644 --- a/homeassistant/components/sensor/translations/id.json +++ b/homeassistant/components/sensor/translations/id.json @@ -59,7 +59,7 @@ "pressure": "Perubahan tekanan {entity_name}", "reactive_power": "Perubahan daya reaktif {entity_name}", "signal_strength": "Perubahan kekuatan sinyal {entity_name}", - "speed": "Perubahan kecepatan {nama_entitas}", + "speed": "Perubahan kecepatan {entity_name}", "sulphur_dioxide": "Perubahan konsentrasi sulfur dioksida {entity_name}", "temperature": "Perubahan suhu {entity_name}", "value": "Perubahan nilai {entity_name}", diff --git a/homeassistant/components/sensor/translations/it.json b/homeassistant/components/sensor/translations/it.json index caaddb8c858..ca319a3437a 100644 --- a/homeassistant/components/sensor/translations/it.json +++ b/homeassistant/components/sensor/translations/it.json @@ -6,6 +6,7 @@ "is_carbon_dioxide": "Livello di concentrazione di anidride carbonica attuale in {entity_name}", "is_carbon_monoxide": "Livello attuale di concentrazione di monossido di carbonio in {entity_name}", "is_current": "Corrente attuale di {entity_name}", + "is_distance": "Distanza attuale di {entity_name}", "is_energy": "Energia attuale di {entity_name}", "is_frequency": "Frequenza attuale di {entity_name}", "is_gas": "Attuale gas di {entity_name}", @@ -24,11 +25,14 @@ "is_pressure": "Pressione attuale di {entity_name}", "is_reactive_power": "Potenza reattiva attuale di {entity_name}", "is_signal_strength": "Potenza del segnale attuale di {entity_name}", + "is_speed": "Velocit\u00e0 corrente di {entity_name}", "is_sulphur_dioxide": "Attuale livello di concentrazione di anidride solforosa di {entity_name}", "is_temperature": "Temperatura attuale di {entity_name}", "is_value": "Valore attuale di {entity_name}", "is_volatile_organic_compounds": "Attuale livello di concentrazione di composti organici volatili di {entity_name}", - "is_voltage": "Tensione attuale di {entity_name}" + "is_voltage": "Tensione attuale di {entity_name}", + "is_volume": "Volume attuale di {entity_name}", + "is_weight": "Peso attuale di {entity_name}" }, "trigger_type": { "apparent_power": "Variazioni di potenza apparente di {entity_name}", @@ -36,6 +40,7 @@ "carbon_dioxide": "Variazioni della concentrazione di anidride carbonica di {entity_name}", "carbon_monoxide": "Variazioni nella concentrazione di monossido di carbonio di {entity_name}", "current": "Variazioni di corrente di {entity_name}", + "distance": "Variazioni di distanza di {entity_name}", "energy": "Variazioni di energia di {entity_name}", "frequency": "{entity_name} cambiamenti di frequenza", "gas": "Variazioni di gas di {entity_name}", @@ -54,11 +59,14 @@ "pressure": "Variazioni della pressione di {entity_name}", "reactive_power": "Variazioni di potenza reattiva di {entity_name}", "signal_strength": "Variazioni della potenza del segnale di {entity_name}", + "speed": "Variazioni di velocit\u00e0 di {entity_name}", "sulphur_dioxide": "Variazioni della concentrazione di anidride solforosa di {entity_name}", "temperature": "Variazioni di temperatura di {entity_name}", "value": "Cambi di valore di {entity_name}", "volatile_organic_compounds": "Variazioni della concentrazione di composti organici volatili di {entity_name}", - "voltage": "variazioni di tensione di {entity_name}" + "voltage": "variazioni di tensione di {entity_name}", + "volume": "Variazioni di volume di {entity_name}", + "weight": "Variazioni di peso di {entity_name}" } }, "state": { diff --git a/homeassistant/components/tautulli/translations/it.json b/homeassistant/components/tautulli/translations/it.json index 7dcfb1dd057..fcc456a8763 100644 --- a/homeassistant/components/tautulli/translations/it.json +++ b/homeassistant/components/tautulli/translations/it.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, diff --git a/homeassistant/components/transmission/translations/ru.json b/homeassistant/components/transmission/translations/ru.json index a01c71898f8..ba6787eed7d 100644 --- a/homeassistant/components/transmission/translations/ru.json +++ b/homeassistant/components/transmission/translations/ru.json @@ -14,7 +14,7 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, - "description": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}.", + "description": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f {username}.", "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { diff --git a/homeassistant/components/uptime/translations/it.json b/homeassistant/components/uptime/translations/it.json index 9913180a309..f842f6d6aa9 100644 --- a/homeassistant/components/uptime/translations/it.json +++ b/homeassistant/components/uptime/translations/it.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "La configurazione di Uptime tramite YAML \u00e8 stata rimossa. \n\nLa tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Uptime \u00e8 stata rimossa" + } + }, "title": "Tempo di funzionamento" } \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/pl.json b/homeassistant/components/yalexs_ble/translations/pl.json index 2d32834337c..6017fb86ffb 100644 --- a/homeassistant/components/yalexs_ble/translations/pl.json +++ b/homeassistant/components/yalexs_ble/translations/pl.json @@ -24,7 +24,7 @@ "key": "Klucz offline (32-bajtowy ci\u0105g szesnastkowy)", "slot": "Slot klucza offline (liczba ca\u0142kowita od 0 do 255)" }, - "description": "Sprawd\u017a dokumentacj\u0119 na {docs_url}, aby dowiedzie\u0107 si\u0119, jak znale\u017a\u0107 klucz offline." + "description": "Sprawd\u017a dokumentacj\u0119, aby dowiedzie\u0107 si\u0119, jak znale\u017a\u0107 klucz offline." } } } diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 7ff1fc354ba..edda54ca6bf 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -116,6 +116,8 @@ }, "device_automation": { "action_type": { + "issue_all_led_effect": "Effetto di emissione per tutti i LED", + "issue_individual_led_effect": "Effetto di emissione per i singoli LED", "squawk": "Strillare", "warn": "Avvertire" }, From 867601220454e2defd5ca21cc9cf8c1b2e638678 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 3 Oct 2022 14:11:18 +1300 Subject: [PATCH 114/985] Bump aioesphomeapi to 11.1.0 (#79515) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index c6a475b6eea..066050d796d 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==11.0.0"], + "requirements": ["aioesphomeapi==11.1.0"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index 586a146fd32..4851099397c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -156,7 +156,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.0.0 +aioesphomeapi==11.1.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 648fbed4b50..9f2a1f6ffb4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -143,7 +143,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.0.0 +aioesphomeapi==11.1.0 # homeassistant.components.flo aioflo==2021.11.0 From 790eb9e72d28e5d096e7870464b5c2fbe4fc7537 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 3 Oct 2022 03:11:45 +0200 Subject: [PATCH 115/985] Remove deprecated update binary sensor from Synology DSM (#79509) --- .../components/synology_dsm/binary_sensor.py | 49 +------------------ 1 file changed, 1 insertion(+), 48 deletions(-) diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index b5f5effbb8e..ac930467442 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -1,12 +1,10 @@ """Support for Synology DSM binary sensors.""" from __future__ import annotations -from collections.abc import Mapping from dataclasses import dataclass from typing import Any from synology_dsm.api.core.security import SynoCoreSecurity -from synology_dsm.api.core.upgrade import SynoCoreUpgrade from synology_dsm.api.storage.storage import SynoStorage from homeassistant.components.binary_sensor import ( @@ -38,18 +36,6 @@ class SynologyDSMBinarySensorEntityDescription( """Describes Synology DSM binary sensor entity.""" -UPGRADE_BINARY_SENSORS: tuple[SynologyDSMBinarySensorEntityDescription, ...] = ( - SynologyDSMBinarySensorEntityDescription( - # Deprecated, scheduled to be removed in 2022.6 (#68664) - api_key=SynoCoreUpgrade.API_KEY, - key="update_available", - name="Update Available", - entity_registry_enabled_default=False, - device_class=BinarySensorDeviceClass.UPDATE, - entity_category=EntityCategory.DIAGNOSTIC, - ), -) - SECURITY_BINARY_SENSORS: tuple[SynologyDSMBinarySensorEntityDescription, ...] = ( SynologyDSMBinarySensorEntityDescription( api_key=SynoCoreSecurity.API_KEY, @@ -85,22 +71,11 @@ async def async_setup_entry( api = data.api coordinator = data.coordinator_central - entities: list[ - SynoDSMSecurityBinarySensor - | SynoDSMUpgradeBinarySensor - | SynoDSMStorageBinarySensor - ] = [ + entities: list[SynoDSMSecurityBinarySensor | SynoDSMStorageBinarySensor] = [ SynoDSMSecurityBinarySensor(api, coordinator, description) for description in SECURITY_BINARY_SENSORS ] - entities.extend( - [ - SynoDSMUpgradeBinarySensor(api, coordinator, description) - for description in UPGRADE_BINARY_SENSORS - ] - ) - # Handle all disks if api.storage.disks_ids: entities.extend( @@ -169,25 +144,3 @@ class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, SynoDSMBinarySensor): return bool( getattr(self._api.storage, self.entity_description.key)(self._device_id) ) - - -class SynoDSMUpgradeBinarySensor(SynoDSMBinarySensor): - """Representation a Synology Upgrade binary sensor.""" - - @property - def is_on(self) -> bool: - """Return the state.""" - return bool(getattr(self._api.upgrade, self.entity_description.key)) - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return bool(self._api.upgrade) - - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return firmware details.""" - return { - "installed_version": self._api.information.version_string, - "latest_available_version": self._api.upgrade.available_version, - } From da960f6ed4e3b572f88f34a857c73720f30f1173 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Oct 2022 15:12:14 -1000 Subject: [PATCH 116/985] Bump bluetooth dependencies (#79514) --- homeassistant/components/bluetooth/manifest.json | 4 ++-- homeassistant/package_constraints.txt | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 8aef8bb42c0..13fe28a5b1e 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -8,9 +8,9 @@ "requirements": [ "bleak==0.18.1", "bleak-retry-connector==2.1.3", - "bluetooth-adapters==0.5.3", + "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.20.0" + "dbus-fast==1.21.17" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a6c3fcceedd..a58e32ca8e4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,12 +12,12 @@ awesomeversion==22.9.0 bcrypt==3.1.7 bleak-retry-connector==2.1.3 bleak==0.18.1 -bluetooth-adapters==0.5.3 +bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.20.0 +dbus-fast==1.21.17 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4851099397c..0e73721418b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -438,7 +438,7 @@ bluemaestro-ble==0.2.0 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.5.3 +bluetooth-adapters==0.6.0 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.3 @@ -543,7 +543,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.20.0 +dbus-fast==1.21.17 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f2a1f6ffb4..2809c4cceab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -352,7 +352,7 @@ blinkpy==0.19.2 bluemaestro-ble==0.2.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.5.3 +bluetooth-adapters==0.6.0 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.3 @@ -423,7 +423,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.20.0 +dbus-fast==1.21.17 # homeassistant.components.debugpy debugpy==1.6.3 From d6a6d0d7548307c143fd2c44a589bd29f729f1e6 Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Sun, 2 Oct 2022 21:14:02 -0400 Subject: [PATCH 117/985] Fix LaCrosse View not updating (#79474) --- homeassistant/components/lacrosse_view/sensor.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py index 1ff3e78812f..684ac884345 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -105,7 +105,7 @@ async def async_setup_entry( sensors: list[Sensor] = coordinator.data sensor_list = [] - for sensor in sensors: + for i, sensor in enumerate(sensors): for field in sensor.sensor_field_names: description = SENSOR_DESCRIPTIONS.get(field) if description is None: @@ -125,6 +125,7 @@ async def async_setup_entry( coordinator=coordinator, description=description, sensor=sensor, + index=i, ) ) @@ -144,6 +145,7 @@ class LaCrosseViewSensor( description: LaCrosseSensorEntityDescription, coordinator: DataUpdateCoordinator[list[Sensor]], sensor: Sensor, + index: int, ) -> None: """Initialize.""" super().__init__(coordinator) @@ -157,11 +159,11 @@ class LaCrosseViewSensor( "model": sensor.model, "via_device": (DOMAIN, sensor.location.id), } - self._sensor = sensor + self.index = index @property def native_value(self) -> float | str: """Return the sensor value.""" return self.entity_description.value_fn( - self._sensor, self.entity_description.key + self.coordinator.data[self.index], self.entity_description.key ) From d7be3c87801b83148e9f8d60da2e5dde11a83928 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 3 Oct 2022 03:19:37 +0200 Subject: [PATCH 118/985] Set Synology DSM update entity to unavailable in case no data from api gathered (#79508) --- homeassistant/components/synology_dsm/update.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/synology_dsm/update.py b/homeassistant/components/synology_dsm/update.py index d3f3cc56eac..445e682651c 100644 --- a/homeassistant/components/synology_dsm/update.py +++ b/homeassistant/components/synology_dsm/update.py @@ -52,6 +52,11 @@ class SynoDSMUpdateEntity(SynologyDSMBaseEntity, UpdateEntity): entity_description: SynologyDSMUpdateEntityEntityDescription _attr_title = "Synology DSM" + @property + def available(self) -> bool: + """Return True if entity is available.""" + return bool(self._api.upgrade) + @property def installed_version(self) -> str | None: """Version installed and in use.""" From 04315751992a71fd48d35d0b6a7372d6b3774664 Mon Sep 17 00:00:00 2001 From: Nyro Date: Mon, 3 Oct 2022 03:22:20 +0200 Subject: [PATCH 119/985] Fix overkiz entity name (#79229) --- homeassistant/components/overkiz/entity.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/overkiz/entity.py b/homeassistant/components/overkiz/entity.py index 4cb5ad1ede7..c17f30393fc 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -34,8 +34,17 @@ class OverkizEntity(CoordinatorEntity[OverkizDataUpdateCoordinator]): self._attr_available = self.device.available self._attr_unique_id = self.device.device_url + if self.is_sub_device: + # In case of sub entity, use the provided label as name + self._attr_name = self.device.label + self._attr_device_info = self.generate_device_info() + @property + def is_sub_device(self) -> bool: + """Return True if device is a sub device.""" + return "#" in self.device_url and not self.device_url.endswith("#1") + @property def device(self) -> Device: """Return Overkiz device linked to this entity.""" @@ -46,7 +55,7 @@ class OverkizEntity(CoordinatorEntity[OverkizDataUpdateCoordinator]): # Some devices, such as the Smart Thermostat have several devices in one physical device, # with same device url, terminated by '#' and a number. # In this case, we use the base device url as the device identifier. - if "#" in self.device_url and not self.device_url.endswith("#1"): + if self.is_sub_device: # Only return the url of the base device, to inherit device name and model from parent device. return { "identifiers": {(DOMAIN, self.executor.base_device_url)}, @@ -103,6 +112,10 @@ class OverkizDescriptiveEntity(OverkizEntity): self.entity_description = description self._attr_unique_id = f"{super().unique_id}-{self.entity_description.key}" + if self.is_sub_device: + # In case of sub device, use the provided label and append the name of the type of entity + self._attr_name = f"{self.device.label} {description.name}" + # Used by state translations for sensor and select entities @unique From 61f0d0ea15d2facbf9b9402be957c1c24e301127 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 2 Oct 2022 21:51:46 -0400 Subject: [PATCH 120/985] Update Nest string (#79516) --- homeassistant/components/nest/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 07ba63ac479..bf68d1988d6 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -25,7 +25,7 @@ }, "device_project": { "title": "Nest: Create a Device Access Project", - "description": "Create a Nest Device Access project which **requires a US $5 fee** to set up.\n1. Go to the [Device Access Console]({device_access_console_url}), and through the payment flow.\n1. Click on **Create project**\n1. Give your Device Access project a name and click **Next**.\n1. Enter your OAuth Client ID\n1. Enable events by clicking **Enable** and **Create project**.\n\nEnter your Device Access Project ID below ([more info]({more_info_url})).\n", + "description": "Create a Nest Device Access project which **requires paying Google a US $5 fee** to set up.\n1. Go to the [Device Access Console]({device_access_console_url}), and through the payment flow.\n1. Click on **Create project**\n1. Give your Device Access project a name and click **Next**.\n1. Enter your OAuth Client ID\n1. Enable events by clicking **Enable** and **Create project**.\n\nEnter your Device Access Project ID below ([more info]({more_info_url})).\n", "data": { "project_id": "Device Access Project ID" } From 47b40e1e619d724adb7de44b45a4f6748ee8790d Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Mon, 3 Oct 2022 02:07:19 -0600 Subject: [PATCH 121/985] Add optional default value to average template function/filter (#77499) * Return None on empty list * Updated to use default value * Update comment. --- homeassistant/helpers/template.py | 22 ++++++++++++++++------ tests/helpers/test_template.py | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 083d0e530aa..f04231ce78a 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1657,7 +1657,7 @@ def min_max_from_filter(builtin_filter: Any, name: str) -> Any: return pass_environment(wrapper) -def average(*args: Any) -> float: +def average(*args: Any, default: Any = _SENTINEL) -> Any: """ Filter and function to calculate the arithmetic mean of an iterable or of two or more arguments. @@ -1666,13 +1666,23 @@ def average(*args: Any) -> float: if len(args) == 0: raise TypeError("average expected at least 1 argument, got 0") - if len(args) == 1: - if isinstance(args[0], Iterable): - return statistics.fmean(args[0]) - + # If first argument is iterable and more then 1 argument provided but not a named default, + # then use 2nd argument as default. + if isinstance(args[0], Iterable): + average_list = args[0] + if len(args) > 1 and default is _SENTINEL: + default = args[1] + elif len(args) == 1: raise TypeError(f"'{type(args[0]).__name__}' object is not iterable") + else: + average_list = args - return statistics.fmean(args) + try: + return statistics.fmean(average_list) + except (TypeError, statistics.StatisticsError): + if default is _SENTINEL: + raise_no_default("average", args) + return default def forgiving_float(value, default=_SENTINEL): diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 9c9a1e42a98..265706c9713 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -973,12 +973,27 @@ def test_average(hass): assert template.Template("{{ average([1, 2, 3]) }}", hass).async_render() == 2 assert template.Template("{{ average(1, 2, 3) }}", hass).async_render() == 2 + # Testing of default values + assert template.Template("{{ average([1, 2, 3], -1) }}", hass).async_render() == 2 + assert template.Template("{{ average([], -1) }}", hass).async_render() == -1 + assert template.Template("{{ average([], default=-1) }}", hass).async_render() == -1 + assert ( + template.Template("{{ average([], 5, default=-1) }}", hass).async_render() == -1 + ) + assert ( + template.Template("{{ average(1, 'a', 3, default=-1) }}", hass).async_render() + == -1 + ) + with pytest.raises(TemplateError): template.Template("{{ 1 | average }}", hass).async_render() with pytest.raises(TemplateError): template.Template("{{ average() }}", hass).async_render() + with pytest.raises(TemplateError): + template.Template("{{ average([]) }}", hass).async_render() + def test_min(hass): """Test the min filter.""" From 825f9502adab0feb10f880e73ff5a4310e6ff557 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 3 Oct 2022 10:09:55 +0200 Subject: [PATCH 122/985] Align temperature conversion with other converters (#79521) * Align temperature conversion with other converters * Add comments and docstring * Align tests --- homeassistant/components/alexa/handlers.py | 4 +- homeassistant/util/temperature.py | 6 +- homeassistant/util/unit_conversion.py | 96 +++++++++------------- tests/util/test_unit_conversion.py | 5 +- 4 files changed, 48 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 73410ba06d2..b4c842dd5b5 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -819,7 +819,9 @@ def temperature_from_object(hass, temp_obj, interval=False): # convert to Celsius if absolute temperature temp -= 273.15 - return TemperatureConverter.convert(temp, from_unit, to_unit, interval=interval) + if interval: + return TemperatureConverter.convert_interval(temp, from_unit, to_unit) + return TemperatureConverter.convert(temp, from_unit, to_unit) @HANDLERS.register(("Alexa.ThermostatController", "SetTargetTemperature")) diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index e08e1207e06..9173fbc5eee 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -43,6 +43,6 @@ def convert( "unit_conversion.TemperatureConverter instead", error_if_core=False, ) - return TemperatureConverter.convert( - temperature, from_unit, to_unit, interval=interval - ) + if interval: + return TemperatureConverter.convert_interval(temperature, from_unit, to_unit) + return TemperatureConverter.convert(temperature, from_unit, to_unit) diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index cb066901b37..6d502ee6e6d 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -1,8 +1,6 @@ """Typing Helpers for Home Assistant.""" from __future__ import annotations -from abc import abstractmethod - from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, @@ -88,20 +86,6 @@ class BaseUnitConverter: NORMALIZED_UNIT: str VALID_UNITS: set[str] - @classmethod - @abstractmethod - def convert(cls, value: float, from_unit: str, to_unit: str) -> float: - """Convert one unit of measurement to another.""" - - @classmethod - @abstractmethod - def get_unit_ratio(cls, from_unit: str, to_unit: str) -> float: - """Get unit ratio between units of measurement.""" - - -class BaseUnitConverterWithUnitConversion(BaseUnitConverter): - """Define the format of a conversion utility.""" - _UNIT_CONVERSION: dict[str, float] @classmethod @@ -133,7 +117,7 @@ class BaseUnitConverterWithUnitConversion(BaseUnitConverter): return cls._UNIT_CONVERSION[from_unit] / cls._UNIT_CONVERSION[to_unit] -class DistanceConverter(BaseUnitConverterWithUnitConversion): +class DistanceConverter(BaseUnitConverter): """Utility to convert distance values.""" UNIT_CLASS = "distance" @@ -160,7 +144,7 @@ class DistanceConverter(BaseUnitConverterWithUnitConversion): } -class EnergyConverter(BaseUnitConverterWithUnitConversion): +class EnergyConverter(BaseUnitConverter): """Utility to convert energy values.""" UNIT_CLASS = "energy" @@ -177,7 +161,7 @@ class EnergyConverter(BaseUnitConverterWithUnitConversion): } -class MassConverter(BaseUnitConverterWithUnitConversion): +class MassConverter(BaseUnitConverter): """Utility to convert mass values.""" UNIT_CLASS = "mass" @@ -200,7 +184,7 @@ class MassConverter(BaseUnitConverterWithUnitConversion): } -class PowerConverter(BaseUnitConverterWithUnitConversion): +class PowerConverter(BaseUnitConverter): """Utility to convert power values.""" UNIT_CLASS = "power" @@ -215,7 +199,7 @@ class PowerConverter(BaseUnitConverterWithUnitConversion): } -class PressureConverter(BaseUnitConverterWithUnitConversion): +class PressureConverter(BaseUnitConverter): """Utility to convert pressure values.""" UNIT_CLASS = "pressure" @@ -244,7 +228,7 @@ class PressureConverter(BaseUnitConverterWithUnitConversion): } -class SpeedConverter(BaseUnitConverterWithUnitConversion): +class SpeedConverter(BaseUnitConverter): """Utility to convert speed values.""" UNIT_CLASS = "speed" @@ -281,47 +265,49 @@ class TemperatureConverter(BaseUnitConverter): TEMP_FAHRENHEIT, TEMP_KELVIN, } - _UNIT_RATIO = { + _UNIT_CONVERSION = { TEMP_CELSIUS: 1.0, TEMP_FAHRENHEIT: 1.8, TEMP_KELVIN: 1.0, } @classmethod - def convert( - cls, value: float, from_unit: str, to_unit: str, *, interval: bool = False - ) -> float: - """Convert a temperature from one unit to another.""" + def convert(cls, value: float, from_unit: str, to_unit: str) -> float: + """Convert a temperature from one unit to another. + + eg. 10°C will return 50°F + + For converting an interval between two temperatures, please use + `convert_interval` instead. + """ + # We cannot use the implementation from BaseUnitConverter here because the temperature + # units do not use the same floor: 0°C, 0°F and 0K do not align if from_unit == to_unit: return value if from_unit == TEMP_CELSIUS: if to_unit == TEMP_FAHRENHEIT: - return cls._celsius_to_fahrenheit(value, interval) + return cls._celsius_to_fahrenheit(value) if to_unit == TEMP_KELVIN: - return cls._celsius_to_kelvin(value, interval) + return cls._celsius_to_kelvin(value) raise HomeAssistantError( UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS) ) if from_unit == TEMP_FAHRENHEIT: if to_unit == TEMP_CELSIUS: - return cls._fahrenheit_to_celsius(value, interval) + return cls._fahrenheit_to_celsius(value) if to_unit == TEMP_KELVIN: - return cls._celsius_to_kelvin( - cls._fahrenheit_to_celsius(value, interval), interval - ) + return cls._celsius_to_kelvin(cls._fahrenheit_to_celsius(value)) raise HomeAssistantError( UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS) ) if from_unit == TEMP_KELVIN: if to_unit == TEMP_CELSIUS: - return cls._kelvin_to_celsius(value, interval) + return cls._kelvin_to_celsius(value) if to_unit == TEMP_FAHRENHEIT: - return cls._celsius_to_fahrenheit( - cls._kelvin_to_celsius(value, interval), interval - ) + return cls._celsius_to_fahrenheit(cls._kelvin_to_celsius(value)) raise HomeAssistantError( UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS) ) @@ -330,40 +316,40 @@ class TemperatureConverter(BaseUnitConverter): ) @classmethod - def _fahrenheit_to_celsius(cls, fahrenheit: float, interval: bool = False) -> float: + def convert_interval(cls, interval: float, from_unit: str, to_unit: str) -> float: + """Convert a temperature interval from one unit to another. + + eg. a 10°C interval (10°C to 20°C) will return a 18°F (50°F to 68°F) interval + + For converting a temperature value, please use `convert` as this method + skips floor adjustment. + """ + # We use BaseUnitConverter implementation here because we are only interested + # in the ratio between the units. + return super().convert(interval, from_unit, to_unit) + + @classmethod + def _fahrenheit_to_celsius(cls, fahrenheit: float) -> float: """Convert a temperature in Fahrenheit to Celsius.""" - if interval: - return fahrenheit / 1.8 return (fahrenheit - 32.0) / 1.8 @classmethod - def _kelvin_to_celsius(cls, kelvin: float, interval: bool = False) -> float: + def _kelvin_to_celsius(cls, kelvin: float) -> float: """Convert a temperature in Kelvin to Celsius.""" - if interval: - return kelvin return kelvin - 273.15 @classmethod - def _celsius_to_fahrenheit(cls, celsius: float, interval: bool = False) -> float: + def _celsius_to_fahrenheit(cls, celsius: float) -> float: """Convert a temperature in Celsius to Fahrenheit.""" - if interval: - return celsius * 1.8 return celsius * 1.8 + 32.0 @classmethod - def _celsius_to_kelvin(cls, celsius: float, interval: bool = False) -> float: + def _celsius_to_kelvin(cls, celsius: float) -> float: """Convert a temperature in Celsius to Kelvin.""" - if interval: - return celsius return celsius + 273.15 - @classmethod - def get_unit_ratio(cls, from_unit: str, to_unit: str) -> float: - """Get unit ratio between units of measurement.""" - return cls._UNIT_RATIO[from_unit] / cls._UNIT_RATIO[to_unit] - -class VolumeConverter(BaseUnitConverterWithUnitConversion): +class VolumeConverter(BaseUnitConverter): """Utility to convert volume values.""" UNIT_CLASS = "volume" diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index ca70af2e53f..ec839a6575c 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -452,10 +452,7 @@ def test_temperature_convert_with_interval( value: float, from_unit: str, expected: float, to_unit: str ) -> None: """Test conversion to other units.""" - assert ( - TemperatureConverter.convert(value, from_unit, to_unit, interval=True) - == expected - ) + assert TemperatureConverter.convert_interval(value, from_unit, to_unit) == expected @pytest.mark.parametrize( From c05d3c10db6aa41c8972f0f0120550f5986a44c8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 3 Oct 2022 11:06:13 +0200 Subject: [PATCH 123/985] Address late comment to deCONZ climate (#79485) Fix late comment to deCONZ climate #59989 --- homeassistant/components/deconz/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 0d13f2639da..c5b9571ed34 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -174,7 +174,7 @@ class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity): ) @property - def hvac_action(self) -> str | None: + def hvac_action(self) -> HVACAction: """Return current hvac operation ie. heat, cool. Preset 'BOOST' is interpreted as 'state_on'. From 125c037deff4ab056aa28f6d28dba720a19a1a84 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Mon, 3 Oct 2022 11:10:25 +0200 Subject: [PATCH 124/985] Address late review of ViCare (#79458) Runn blocking I/O of button entity creation in async_add_executor_job --- homeassistant/components/vicare/button.py | 41 +++++++++++++++-------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/vicare/button.py b/homeassistant/components/vicare/button.py index 6f94c7102c9..95be680f957 100644 --- a/homeassistant/components/vicare/button.py +++ b/homeassistant/components/vicare/button.py @@ -1,4 +1,4 @@ -"""Viessmann ViCare sensor device.""" +"""Viessmann ViCare button device.""" from __future__ import annotations from contextlib import suppress @@ -30,7 +30,7 @@ BUTTON_DHW_ACTIVATE_ONETIME_CHARGE = "activate_onetimecharge" class ViCareButtonEntityDescription( ButtonEntityDescription, ViCareRequiredKeysMixinWithSet ): - """Describes ViCare button sensor entity.""" + """Describes ViCare button entity.""" BUTTON_DESCRIPTIONS: tuple[ViCareButtonEntityDescription, ...] = ( @@ -45,28 +45,41 @@ BUTTON_DESCRIPTIONS: tuple[ViCareButtonEntityDescription, ...] = ( ) +def _build_entity(name, vicare_api, device_config, description): + """Create a ViCare button entity.""" + _LOGGER.debug("Found device %s", name) + try: + description.value_getter(vicare_api) + _LOGGER.debug("Found entity %s", name) + except PyViCareNotSupportedFeatureError: + _LOGGER.info("Feature not supported %s", name) + return None + except AttributeError: + _LOGGER.debug("Attribute Error %s", name) + return None + + return ViCareButton( + name, + vicare_api, + device_config, + description, + ) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Create the ViCare binary sensor devices.""" + """Create the ViCare button entities.""" name = VICARE_NAME api = hass.data[DOMAIN][config_entry.entry_id][VICARE_API] entities = [] for description in BUTTON_DESCRIPTIONS: - try: - description.value_getter(api) - _LOGGER.debug("Found entity %s", description.name) - except PyViCareNotSupportedFeatureError: - _LOGGER.info("Feature not supported %s", description.name) - continue - except AttributeError: - _LOGGER.debug("Attribute Error %s", name) - continue - entity = ViCareButton( + entity = await hass.async_add_executor_job( + _build_entity, f"{name} {description.name}", api, hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], @@ -86,7 +99,7 @@ class ViCareButton(ButtonEntity): def __init__( self, name, api, device_config, description: ViCareButtonEntityDescription ): - """Initialize the sensor.""" + """Initialize the button.""" self.entity_description = description self._device_config = device_config self._api = api From 0fdb7052e9c2cc34a59d8b65201ba4e3393de8a5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 3 Oct 2022 11:40:11 +0200 Subject: [PATCH 125/985] Add comment in recorder about dropping column (#79523) Add comment in recorder --- homeassistant/components/recorder/migration.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index f82ec7ba1eb..0ebd761ca53 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -750,6 +750,9 @@ def _apply_update( # noqa: C901 elif new_version == 30: # This added a column to the statistics_meta table, removed again before # release of HA Core 2022.10.0 + # SQLite 3.31.0 does not support dropping columns. + # Once we require SQLite >= 3.35.5, we should drop the column: + # ALTER TABLE statistics_meta DROP COLUMN state_unit_of_measurement pass else: raise ValueError(f"No schema migration defined for version {new_version}") From cb909b4b05ba4c1732be022134d857d4c2991be3 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 3 Oct 2022 11:44:20 +0200 Subject: [PATCH 126/985] Bumb velbusaio to 2022.10.1 (#79471) --- 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 cbc8db0ca9f..8b15dd1fa9f 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": ["velbus-aio==2022.9.1"], + "requirements": ["velbus-aio==2022.10.1"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "dependencies": ["usb"], diff --git a/requirements_all.txt b/requirements_all.txt index 0e73721418b..54d6a4f49ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2475,7 +2475,7 @@ vallox-websocket-api==2.12.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.9.1 +velbus-aio==2022.10.1 # homeassistant.components.venstar venstarcolortouch==0.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2809c4cceab..27ecfa15ca5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1709,7 +1709,7 @@ vallox-websocket-api==2.12.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.9.1 +velbus-aio==2022.10.1 # homeassistant.components.venstar venstarcolortouch==0.18 From aa3aa913584faf93d71096d8ab9367d996518314 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Oct 2022 03:11:51 -1000 Subject: [PATCH 127/985] Bump dbus-fast to 1.22.0 (#79527) Performance improvements https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.21.17...v1.22.0 --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 13fe28a5b1e..1cb01a7da63 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.21.17" + "dbus-fast==1.22.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a58e32ca8e4..a8b011a1a85 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.21.17 +dbus-fast==1.22.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 54d6a4f49ac..45d52e0ba8f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -543,7 +543,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.21.17 +dbus-fast==1.22.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27ecfa15ca5..6c9e3ee8b98 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.21.17 +dbus-fast==1.22.0 # homeassistant.components.debugpy debugpy==1.6.3 From 40bdcc3ea7fe0cc0d5b662f305be33c08d048066 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 3 Oct 2022 16:17:08 +0200 Subject: [PATCH 128/985] Bump velbusaio to 2022.10.2 (#79537) --- 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 8b15dd1fa9f..1a5d78d24d6 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": ["velbus-aio==2022.10.1"], + "requirements": ["velbus-aio==2022.10.2"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "dependencies": ["usb"], diff --git a/requirements_all.txt b/requirements_all.txt index 45d52e0ba8f..499c288a356 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2475,7 +2475,7 @@ vallox-websocket-api==2.12.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.10.1 +velbus-aio==2022.10.2 # homeassistant.components.venstar venstarcolortouch==0.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c9e3ee8b98..78129e17638 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1709,7 +1709,7 @@ vallox-websocket-api==2.12.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.10.1 +velbus-aio==2022.10.2 # homeassistant.components.venstar venstarcolortouch==0.18 From 55b036ec5ef570b146a6126408da929dd66e431e Mon Sep 17 00:00:00 2001 From: Ben Randall Date: Mon, 3 Oct 2022 10:42:57 -0400 Subject: [PATCH 129/985] Improve device_automation trigger validation (#75044) * improve device_automation trigger validation Validates the trigger configuration against the device_trigger schema before trying to access any of the properties in order to provide better error messages. Updates the error message to include an explicit indication that the error is coming from a trigger configuration. The inner error message from the validator can be accessed by viewing the stack trace. Add test case for trigger missing domain. Make action and condition validation consistent with trigger. This is not strictly necessary, but should be helpful for certain use cases that bypass some of the outer validation. Removed redundant schema elements from humidifier device_trigger. **Blueprint with missing `domain`** ``` 2022-07-12 06:02:18.351 ERROR (MainThread) [homeassistant.setup] Error during setup of component automation Traceback (most recent call last): File "/workspaces/core/homeassistant/setup.py", line 235, in _async_setup_component result = await task File "/workspaces/core/homeassistant/components/automation/__init__.py", line 241, in async_setup if not await _async_process_config(hass, config, component): File "/workspaces/core/homeassistant/components/automation/__init__.py", line 648, in _async_process_config await async_validate_config_item(hass, raw_config), File "/workspaces/core/homeassistant/components/automation/config.py", line 74, in async_validate_config_item config[CONF_TRIGGER] = await async_validate_trigger_config( File "/workspaces/core/homeassistant/helpers/trigger.py", line 59, in async_validate_trigger_config conf = await platform.async_validate_trigger_config(hass, conf) File "/workspaces/core/homeassistant/components/device_automation/trigger.py", line 67, in async_validate_trigger_config hass, config[CONF_DOMAIN], DeviceAutomationType.TRIGGER KeyError: 'domain' ``` **Blueprint with missing `property` (specific to zwave_js event schema)** ``` 2022-07-12 06:09:54.206 ERROR (MainThread) [homeassistant.components.automation] Blueprint Missing Property generated invalid automation with inputs OrderedDict([('control_switch', '498be56d796836a67406e9ad373d23db')]): required key not provided @ data['property']. Got None ``` **Blueprint with missing `domain`** ``` 2022-07-12 06:12:16.080 ERROR (MainThread) [homeassistant.components.automation] Blueprint Missing Domain generated invalid automation with inputs OrderedDict([('control_switch', '498be56d796836a67406e9ad373d23db')]): invalid trigger configuration: required key not provided @ data['domain']. Got ``` **Blueprint with missing `property` (specific to zwave_js event schema)** ``` 2022-07-12 06:12:16.680 ERROR (MainThread) [homeassistant.components.automation] Blueprint Missing Property generated invalid automation with inputs OrderedDict([('control_switch', '498be56d796836a67406e9ad373d23db')]): invalid trigger configuration: required key not provided @ data['property']. Got ``` * Revert humifidier TRIGGER_SCHEMA change. --- .../components/device_automation/action.py | 6 ++- .../components/device_automation/condition.py | 4 +- .../components/device_automation/trigger.py | 5 +- .../components/rfxtrx/device_action.py | 1 - .../components/device_automation/test_init.py | 51 +++++++++++++++++-- .../components/webostv/test_device_trigger.py | 3 +- 6 files changed, 56 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/device_automation/action.py b/homeassistant/components/device_automation/action.py index 081b6bb283a..432ff2fdb7d 100644 --- a/homeassistant/components/device_automation/action.py +++ b/homeassistant/components/device_automation/action.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.const import CONF_DOMAIN from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType from . import DeviceAutomationType, async_get_device_automation_platform @@ -51,14 +52,15 @@ async def async_validate_action_config( ) -> ConfigType: """Validate config.""" try: + config = cv.DEVICE_ACTION_SCHEMA(config) platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], DeviceAutomationType.ACTION ) if hasattr(platform, "async_validate_action_config"): return await platform.async_validate_action_config(hass, config) return cast(ConfigType, platform.ACTION_SCHEMA(config)) - except InvalidDeviceAutomationConfig as err: - raise vol.Invalid(str(err) or "Invalid action configuration") from err + except (vol.Invalid, InvalidDeviceAutomationConfig) as err: + raise vol.Invalid("invalid action configuration: " + str(err)) from err async def async_call_action_from_config( diff --git a/homeassistant/components/device_automation/condition.py b/homeassistant/components/device_automation/condition.py index d656908f4be..3b0a5263f9e 100644 --- a/homeassistant/components/device_automation/condition.py +++ b/homeassistant/components/device_automation/condition.py @@ -58,8 +58,8 @@ async def async_validate_condition_config( if hasattr(platform, "async_validate_condition_config"): return await platform.async_validate_condition_config(hass, config) return cast(ConfigType, platform.CONDITION_SCHEMA(config)) - except InvalidDeviceAutomationConfig as err: - raise vol.Invalid(str(err) or "Invalid condition configuration") from err + except (vol.Invalid, InvalidDeviceAutomationConfig) as err: + raise vol.Invalid("invalid condition configuration: " + str(err)) from err async def async_condition_from_config( diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py index bd72b24d844..aac56b39846 100644 --- a/homeassistant/components/device_automation/trigger.py +++ b/homeassistant/components/device_automation/trigger.py @@ -58,14 +58,15 @@ async def async_validate_trigger_config( ) -> ConfigType: """Validate config.""" try: + config = TRIGGER_SCHEMA(config) platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], DeviceAutomationType.TRIGGER ) if not hasattr(platform, "async_validate_trigger_config"): return cast(ConfigType, platform.TRIGGER_SCHEMA(config)) return await platform.async_validate_trigger_config(hass, config) - except InvalidDeviceAutomationConfig as err: - raise vol.Invalid(str(err) or "Invalid trigger configuration") from err + except (vol.Invalid, InvalidDeviceAutomationConfig) as err: + raise InvalidDeviceAutomationConfig("invalid trigger configuration") from err async def async_attach_trigger( diff --git a/homeassistant/components/rfxtrx/device_action.py b/homeassistant/components/rfxtrx/device_action.py index 15595b88cd2..7ea4ed07423 100644 --- a/homeassistant/components/rfxtrx/device_action.py +++ b/homeassistant/components/rfxtrx/device_action.py @@ -80,7 +80,6 @@ async def async_validate_action_config( hass: HomeAssistant, config: ConfigType ) -> ConfigType: """Validate config.""" - config = ACTION_SCHEMA(config) commands, _ = _get_commands(hass, config[CONF_DEVICE_ID], config[CONF_TYPE]) sub_type = config[CONF_SUBTYPE] diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 3ead6fcb35d..71c062cf7d9 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -720,7 +720,28 @@ async def test_automation_with_bad_condition_action(hass, caplog): assert "required key not provided" in caplog.text -async def test_automation_with_bad_condition(hass, caplog): +async def test_automation_with_bad_condition_missing_domain(hass, caplog): + """Test automation with bad device condition.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": {"condition": "device", "device_id": "hello.device"}, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert ( + "Invalid config for [automation]: required key not provided @ data['condition'][0]['domain']" + in caplog.text + ) + + +async def test_automation_with_bad_condition_missing_device_id(hass, caplog): """Test automation with bad device condition.""" assert await async_setup_component( hass, @@ -735,7 +756,10 @@ async def test_automation_with_bad_condition(hass, caplog): }, ) - assert "required key not provided" in caplog.text + assert ( + "Invalid config for [automation]: required key not provided @ data['condition'][0]['device_id']" + in caplog.text + ) @pytest.fixture @@ -876,8 +900,25 @@ async def test_automation_with_bad_sub_condition(hass, caplog): assert "required key not provided" in caplog.text -async def test_automation_with_bad_trigger(hass, caplog): - """Test automation with bad device trigger.""" +async def test_automation_with_bad_trigger_missing_domain(hass, caplog): + """Test automation with device trigger this is missing domain.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "device", "device_id": "hello.device"}, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert "required key not provided @ data['domain']" in caplog.text + + +async def test_automation_with_bad_trigger_missing_device_id(hass, caplog): + """Test automation with device trigger that is missing device_id.""" assert await async_setup_component( hass, automation.DOMAIN, @@ -890,7 +931,7 @@ async def test_automation_with_bad_trigger(hass, caplog): }, ) - assert "required key not provided" in caplog.text + assert "required key not provided @ data['device_id']" in caplog.text async def test_websocket_device_not_found(hass, hass_ws_client): diff --git a/tests/components/webostv/test_device_trigger.py b/tests/components/webostv/test_device_trigger.py index db15ce3a592..96914885971 100644 --- a/tests/components/webostv/test_device_trigger.py +++ b/tests/components/webostv/test_device_trigger.py @@ -128,8 +128,7 @@ async def test_get_triggers_for_invalid_device_id(hass, caplog): await hass.async_block_till_done() assert ( - "Invalid config for [automation]: Device invalid_device_id is not a valid webostv device" - in caplog.text + "Invalid config for [automation]: invalid trigger configuration" in caplog.text ) From 3e3f7ea99590ead08bfd9d2a7de032644b830e4e Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Mon, 3 Oct 2022 18:21:45 +0200 Subject: [PATCH 130/985] Rework devolo Home Network tests (#74472) --- .../devolo_home_network/__init__.py | 18 +---- .../devolo_home_network/conftest.py | 25 +++---- tests/components/devolo_home_network/mock.py | 57 ++++++++++++++ .../devolo_home_network/test_binary_sensor.py | 41 +++++----- .../devolo_home_network/test_config_flow.py | 13 ++-- .../test_device_tracker.py | 63 ++++++++-------- .../devolo_home_network/test_init.py | 17 ++--- .../devolo_home_network/test_sensor.py | 74 ++++++++++--------- 8 files changed, 172 insertions(+), 136 deletions(-) create mode 100644 tests/components/devolo_home_network/mock.py diff --git a/tests/components/devolo_home_network/__init__.py b/tests/components/devolo_home_network/__init__.py index bb861081517..c8561f485ca 100644 --- a/tests/components/devolo_home_network/__init__.py +++ b/tests/components/devolo_home_network/__init__.py @@ -1,16 +1,9 @@ """Tests for the devolo Home Network integration.""" - -import dataclasses -from typing import Any - -from devolo_plc_api.device_api.deviceapi import DeviceApi -from devolo_plc_api.plcnet_api.plcnetapi import PlcNetApi - from homeassistant.components.devolo_home_network.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant -from .const import DISCOVERY_INFO, IP +from .const import IP from tests.common import MockConfigEntry @@ -24,12 +17,3 @@ def configure_integration(hass: HomeAssistant) -> MockConfigEntry: entry.add_to_hass(hass) return entry - - -async def async_connect(self, session_instance: Any = None): - """Give a mocked device the needed properties.""" - self.plcnet = PlcNetApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) - self.device = DeviceApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) - self.mac = DISCOVERY_INFO.properties["PlcMacAddress"] - self.product = DISCOVERY_INFO.properties["Product"] - self.serial_number = DISCOVERY_INFO.properties["SN"] diff --git a/tests/components/devolo_home_network/conftest.py b/tests/components/devolo_home_network/conftest.py index 1d8d2a6da19..98a79faae54 100644 --- a/tests/components/devolo_home_network/conftest.py +++ b/tests/components/devolo_home_network/conftest.py @@ -1,29 +1,22 @@ """Fixtures for tests.""" - -from unittest.mock import AsyncMock, patch +from itertools import cycle +from unittest.mock import patch import pytest -from . import async_connect -from .const import CONNECTED_STATIONS, DISCOVERY_INFO, NEIGHBOR_ACCESS_POINTS, PLCNET +from .const import DISCOVERY_INFO, IP +from .mock import MockDevice @pytest.fixture() def mock_device(): """Mock connecting to a devolo home network device.""" - with patch("devolo_plc_api.device.Device.async_connect", async_connect), patch( - "devolo_plc_api.device.Device.async_disconnect" - ), patch( - "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_connected_station", - new=AsyncMock(return_value=CONNECTED_STATIONS), - ), patch( - "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_neighbor_access_points", - new=AsyncMock(return_value=NEIGHBOR_ACCESS_POINTS), - ), patch( - "devolo_plc_api.plcnet_api.plcnetapi.PlcNetApi.async_get_network_overview", - new=AsyncMock(return_value=PLCNET), + device = MockDevice(ip=IP) + with patch( + "homeassistant.components.devolo_home_network.Device", + side_effect=cycle([device]), ): - yield + yield device @pytest.fixture(name="info") diff --git a/tests/components/devolo_home_network/mock.py b/tests/components/devolo_home_network/mock.py new file mode 100644 index 00000000000..660cc19f78c --- /dev/null +++ b/tests/components/devolo_home_network/mock.py @@ -0,0 +1,57 @@ +"""Mock of a devolo Home Network device.""" +from __future__ import annotations + +import dataclasses +from typing import Any +from unittest.mock import AsyncMock + +from devolo_plc_api.device import Device +from devolo_plc_api.device_api.deviceapi import DeviceApi +from devolo_plc_api.plcnet_api.plcnetapi import PlcNetApi +import httpx +from zeroconf import Zeroconf +from zeroconf.asyncio import AsyncZeroconf + +from .const import ( + CONNECTED_STATIONS, + DISCOVERY_INFO, + IP, + NEIGHBOR_ACCESS_POINTS, + PLCNET, +) + + +class MockDevice(Device): + """Mock of a devolo Home Network device.""" + + def __init__( + self, + ip: str, + plcnetapi: dict[str, Any] | None = None, + deviceapi: dict[str, Any] | None = None, + zeroconf_instance: AsyncZeroconf | Zeroconf | None = None, + ) -> None: + """Bring mock in a well defined state.""" + super().__init__(ip, plcnetapi, deviceapi, zeroconf_instance) + self.reset() + + async def async_connect( + self, session_instance: httpx.AsyncClient | None = None + ) -> None: + """Give a mocked device the needed properties.""" + self.mac = DISCOVERY_INFO.properties["PlcMacAddress"] + self.product = DISCOVERY_INFO.properties["Product"] + self.serial_number = DISCOVERY_INFO.properties["SN"] + + def reset(self): + """Reset mock to starting point.""" + self.async_disconnect = AsyncMock() + self.device = DeviceApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) + self.device.async_get_wifi_connected_station = AsyncMock( + return_value=CONNECTED_STATIONS + ) + self.device.async_get_wifi_neighbor_access_points = AsyncMock( + return_value=NEIGHBOR_ACCESS_POINTS + ) + self.plcnet = PlcNetApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) + self.plcnet.async_get_network_overview = AsyncMock(return_value=PLCNET) diff --git a/tests/components/devolo_home_network/test_binary_sensor.py b/tests/components/devolo_home_network/test_binary_sensor.py index 8f9936be5bb..d18dbca1f5f 100644 --- a/tests/components/devolo_home_network/test_binary_sensor.py +++ b/tests/components/devolo_home_network/test_binary_sensor.py @@ -1,5 +1,5 @@ """Tests for the devolo Home Network sensors.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock from devolo_plc_api.exceptions.device import DeviceUnavailable import pytest @@ -22,6 +22,7 @@ from homeassistant.util import dt from . import configure_integration from .const import PLCNET_ATTACHED +from .mock import MockDevice from tests.common import async_fire_time_changed @@ -39,8 +40,8 @@ async def test_binary_sensor_setup(hass: HomeAssistant): await hass.config_entries.async_unload(entry.entry_id) -@pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_device") -async def test_update_attached_to_router(hass: HomeAssistant): +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_update_attached_to_router(hass: HomeAssistant, mock_device: MockDevice): """Test state change of a attached_to_router binary sensor device.""" entry = configure_integration(hass) device_name = entry.title.replace(" ", "_").lower() @@ -59,27 +60,25 @@ async def test_update_attached_to_router(hass: HomeAssistant): assert er.async_get(state_key).entity_category == EntityCategory.DIAGNOSTIC # Emulate device failure - with patch( - "devolo_plc_api.plcnet_api.plcnetapi.PlcNetApi.async_get_network_overview", - side_effect=DeviceUnavailable, - ): - async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) - await hass.async_block_till_done() + mock_device.plcnet.async_get_network_overview = AsyncMock( + side_effect=DeviceUnavailable + ) + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_UNAVAILABLE + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE # Emulate state change - with patch( - "devolo_plc_api.plcnet_api.plcnetapi.PlcNetApi.async_get_network_overview", - new=AsyncMock(return_value=PLCNET_ATTACHED), - ): - async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) - await hass.async_block_till_done() + mock_device.plcnet.async_get_network_overview = AsyncMock( + return_value=PLCNET_ATTACHED + ) + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_ON + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_ON await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/devolo_home_network/test_config_flow.py b/tests/components/devolo_home_network/test_config_flow.py index f9d589eb638..9f05d0af2fb 100644 --- a/tests/components/devolo_home_network/test_config_flow.py +++ b/tests/components/devolo_home_network/test_config_flow.py @@ -19,6 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from .const import DISCOVERY_INFO, DISCOVERY_INFO_WRONG_DEVICE, IP +from .mock import MockDevice async def test_form(hass: HomeAssistant, info: dict[str, Any]): @@ -167,10 +168,12 @@ async def test_abort_if_configued(hass: HomeAssistant): assert result3["reason"] == "already_configured" -@pytest.mark.usefixtures("mock_device") -@pytest.mark.usefixtures("mock_zeroconf") async def test_validate_input(hass: HomeAssistant): """Test input validation.""" - info = await config_flow.validate_input(hass, {CONF_IP_ADDRESS: IP}) - assert SERIAL_NUMBER in info - assert TITLE in info + with patch( + "homeassistant.components.devolo_home_network.config_flow.Device", + new=MockDevice, + ): + info = await config_flow.validate_input(hass, {CONF_IP_ADDRESS: IP}) + assert SERIAL_NUMBER in info + assert TITLE in info diff --git a/tests/components/devolo_home_network/test_device_tracker.py b/tests/components/devolo_home_network/test_device_tracker.py index 233a480b5e3..2f8fea3e749 100644 --- a/tests/components/devolo_home_network/test_device_tracker.py +++ b/tests/components/devolo_home_network/test_device_tracker.py @@ -1,8 +1,7 @@ """Tests for the devolo Home Network device tracker.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock from devolo_plc_api.exceptions.device import DeviceUnavailable -import pytest from homeassistant.components.device_tracker import DOMAIN as PLATFORM from homeassistant.components.devolo_home_network.const import ( @@ -23,6 +22,7 @@ from homeassistant.util import dt from . import configure_integration from .const import CONNECTED_STATIONS, DISCOVERY_INFO, NO_CONNECTED_STATIONS +from .mock import MockDevice from tests.common import async_fire_time_changed @@ -30,8 +30,7 @@ STATION = CONNECTED_STATIONS["connected_stations"][0] SERIAL = DISCOVERY_INFO.properties["SN"] -@pytest.mark.usefixtures("mock_device") -async def test_device_tracker(hass: HomeAssistant): +async def test_device_tracker(hass: HomeAssistant, mock_device: MockDevice): """Test device tracker states.""" state_key = f"{PLATFORM}.{DOMAIN}_{SERIAL}_{STATION['mac_address'].lower().replace(':', '_')}" entry = configure_integration(hass) @@ -57,34 +56,31 @@ async def test_device_tracker(hass: HomeAssistant): ) # Emulate state change - with patch( - "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_connected_station", - new=AsyncMock(return_value=NO_CONNECTED_STATIONS), - ): - async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) - await hass.async_block_till_done() + mock_device.device.async_get_wifi_connected_station = AsyncMock( + return_value=NO_CONNECTED_STATIONS + ) + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_NOT_HOME + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_NOT_HOME # Emulate device failure - with patch( - "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_connected_station", - side_effect=DeviceUnavailable, - ): - async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) - await hass.async_block_till_done() + mock_device.device.async_get_wifi_connected_station = AsyncMock( + side_effect=DeviceUnavailable + ) + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_UNAVAILABLE + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE await hass.config_entries.async_unload(entry.entry_id) -@pytest.mark.usefixtures("mock_device") -async def test_restoring_clients(hass: HomeAssistant): +async def test_restoring_clients(hass: HomeAssistant, mock_device: MockDevice): """Test restoring existing device_tracker entities.""" state_key = f"{PLATFORM}.{DOMAIN}_{SERIAL}_{STATION['mac_address'].lower().replace(':', '_')}" entry = configure_integration(hass) @@ -96,12 +92,13 @@ async def test_restoring_clients(hass: HomeAssistant): config_entry=entry, ) - with patch( - "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_connected_station", - new=AsyncMock(return_value=NO_CONNECTED_STATIONS), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_NOT_HOME + mock_device.device.async_get_wifi_connected_station = AsyncMock( + return_value=NO_CONNECTED_STATIONS + ) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_NOT_HOME diff --git a/tests/components/devolo_home_network/test_init.py b/tests/components/devolo_home_network/test_init.py index 1d15f337c17..5d5693c44e3 100644 --- a/tests/components/devolo_home_network/test_init.py +++ b/tests/components/devolo_home_network/test_init.py @@ -9,6 +9,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from . import configure_integration +from .mock import MockDevice @pytest.mark.usefixtures("mock_device") @@ -44,15 +45,11 @@ async def test_unload_entry(hass: HomeAssistant): assert entry.state is ConfigEntryState.NOT_LOADED -@pytest.mark.usefixtures("mock_device") -async def test_hass_stop(hass: HomeAssistant): +async def test_hass_stop(hass: HomeAssistant, mock_device: MockDevice): """Test homeassistant stop event.""" entry = configure_integration(hass) - with patch( - "homeassistant.components.devolo_home_network.Device.async_disconnect" - ) as async_disconnect: - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - async_disconnect.assert_called_once() + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + mock_device.async_disconnect.assert_called_once() diff --git a/tests/components/devolo_home_network/test_sensor.py b/tests/components/devolo_home_network/test_sensor.py index 33499f512fa..3002bd7c5b8 100644 --- a/tests/components/devolo_home_network/test_sensor.py +++ b/tests/components/devolo_home_network/test_sensor.py @@ -1,5 +1,5 @@ """Tests for the devolo Home Network sensors.""" -from unittest.mock import patch +from unittest.mock import AsyncMock from devolo_plc_api.exceptions.device import DeviceUnavailable import pytest @@ -16,6 +16,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.util import dt from . import configure_integration +from .mock import MockDevice from tests.common import async_fire_time_changed @@ -35,8 +36,9 @@ async def test_sensor_setup(hass: HomeAssistant): await hass.config_entries.async_unload(entry.entry_id) -@pytest.mark.usefixtures("mock_device") -async def test_update_connected_wifi_clients(hass: HomeAssistant): +async def test_update_connected_wifi_clients( + hass: HomeAssistant, mock_device: MockDevice +): """Test state change of a connected_wifi_clients sensor device.""" entry = configure_integration(hass) device_name = entry.title.replace(" ", "_").lower() @@ -53,18 +55,18 @@ async def test_update_connected_wifi_clients(hass: HomeAssistant): assert state.attributes["state_class"] == SensorStateClass.MEASUREMENT # Emulate device failure - with patch( - "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_connected_station", - side_effect=DeviceUnavailable, - ): - async_fire_time_changed(hass, dt.utcnow() + SHORT_UPDATE_INTERVAL) - await hass.async_block_till_done() + mock_device.device.async_get_wifi_connected_station = AsyncMock( + side_effect=DeviceUnavailable + ) + async_fire_time_changed(hass, dt.utcnow() + SHORT_UPDATE_INTERVAL) + await hass.async_block_till_done() - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_UNAVAILABLE + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE # Emulate state change + mock_device.reset() async_fire_time_changed(hass, dt.utcnow() + SHORT_UPDATE_INTERVAL) await hass.async_block_till_done() @@ -75,8 +77,10 @@ async def test_update_connected_wifi_clients(hass: HomeAssistant): await hass.config_entries.async_unload(entry.entry_id) -@pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_device") -async def test_update_neighboring_wifi_networks(hass: HomeAssistant): +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_update_neighboring_wifi_networks( + hass: HomeAssistant, mock_device: MockDevice +): """Test state change of a neighboring_wifi_networks sensor device.""" entry = configure_integration(hass) device_name = entry.title.replace(" ", "_").lower() @@ -95,18 +99,18 @@ async def test_update_neighboring_wifi_networks(hass: HomeAssistant): assert er.async_get(state_key).entity_category is EntityCategory.DIAGNOSTIC # Emulate device failure - with patch( - "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_neighbor_access_points", - side_effect=DeviceUnavailable, - ): - async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) - await hass.async_block_till_done() + mock_device.device.async_get_wifi_neighbor_access_points = AsyncMock( + side_effect=DeviceUnavailable + ) + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_UNAVAILABLE + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE # Emulate state change + mock_device.reset() async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) await hass.async_block_till_done() @@ -117,8 +121,10 @@ async def test_update_neighboring_wifi_networks(hass: HomeAssistant): await hass.config_entries.async_unload(entry.entry_id) -@pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_device") -async def test_update_connected_plc_devices(hass: HomeAssistant): +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_update_connected_plc_devices( + hass: HomeAssistant, mock_device: MockDevice +): """Test state change of a connected_plc_devices sensor device.""" entry = configure_integration(hass) device_name = entry.title.replace(" ", "_").lower() @@ -136,18 +142,18 @@ async def test_update_connected_plc_devices(hass: HomeAssistant): assert er.async_get(state_key).entity_category is EntityCategory.DIAGNOSTIC # Emulate device failure - with patch( - "devolo_plc_api.plcnet_api.plcnetapi.PlcNetApi.async_get_network_overview", - side_effect=DeviceUnavailable, - ): - async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) - await hass.async_block_till_done() + mock_device.plcnet.async_get_network_overview = AsyncMock( + side_effect=DeviceUnavailable + ) + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_UNAVAILABLE + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE # Emulate state change + mock_device.reset() async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) await hass.async_block_till_done() From f3007b22c473b7859026bfaad22b777d7199cee4 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Mon, 3 Oct 2022 14:22:05 -0400 Subject: [PATCH 131/985] Allow setting set_percentage and set_preset_mode of template fan without turning on (#75656) * decouple set_percentage and set_preset_mode from entity state * correct set_percent self._state logic * Add tests * remove _VALUD_STATES * decouple percent and preset_mode --- homeassistant/components/template/fan.py | 73 +++++++------ tests/components/template/test_fan.py | 126 +++++++++++++++++++++-- 2 files changed, 160 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index b60e7f53364..b27a6ee3e51 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -22,7 +22,6 @@ from homeassistant.const import ( CONF_FRIENDLY_NAME, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, - STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -58,7 +57,6 @@ CONF_SET_OSCILLATING_ACTION = "set_oscillating" CONF_SET_DIRECTION_ACTION = "set_direction" CONF_SET_PRESET_MODE_ACTION = "set_preset_mode" -_VALID_STATES = [STATE_ON, STATE_OFF] _VALID_DIRECTIONS = [DIRECTION_FORWARD, DIRECTION_REVERSE] FAN_SCHEMA = vol.All( @@ -66,7 +64,7 @@ FAN_SCHEMA = vol.All( vol.Schema( { vol.Optional(CONF_FRIENDLY_NAME): cv.string, - vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_PERCENTAGE_TEMPLATE): cv.template, vol.Optional(CONF_PRESET_MODE_TEMPLATE): cv.template, vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, @@ -144,7 +142,7 @@ class TemplateFan(TemplateEntity, FanEntity): ) friendly_name = self._attr_name - self._template = config[CONF_VALUE_TEMPLATE] + self._template = config.get(CONF_VALUE_TEMPLATE) self._percentage_template = config.get(CONF_PERCENTAGE_TEMPLATE) self._preset_mode_template = config.get(CONF_PRESET_MODE_TEMPLATE) self._oscillating_template = config.get(CONF_OSCILLATING_TEMPLATE) @@ -178,7 +176,7 @@ class TemplateFan(TemplateEntity, FanEntity): hass, set_direction_action, friendly_name, DOMAIN ) - self._state = STATE_OFF + self._state: bool | None = False self._percentage = None self._preset_mode = None self._oscillating = None @@ -215,9 +213,9 @@ class TemplateFan(TemplateEntity, FanEntity): return self._preset_modes @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if device is on.""" - return self._state == STATE_ON + return self._state @property def preset_mode(self) -> str | None: @@ -254,23 +252,27 @@ class TemplateFan(TemplateEntity, FanEntity): }, context=self._context, ) - self._state = STATE_ON if preset_mode is not None: await self.async_set_preset_mode(preset_mode) elif percentage is not None: await self.async_set_percentage(percentage) + if self._template is None: + self._state = True + self.async_write_ha_state() + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the fan.""" await self.async_run_script(self._off_script, context=self._context) - self._state = STATE_OFF + + if self._template is None: + self._state = False + self.async_write_ha_state() async def async_set_percentage(self, percentage: int) -> None: """Set the percentage speed of the fan.""" - self._state = STATE_OFF if percentage == 0 else STATE_ON self._percentage = percentage - self._preset_mode = None if self._set_percentage_script: await self.async_run_script( @@ -279,6 +281,10 @@ class TemplateFan(TemplateEntity, FanEntity): context=self._context, ) + if self._template is None: + self._state = percentage != 0 + self.async_write_ha_state() + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset_mode of the fan.""" if self.preset_modes and preset_mode not in self.preset_modes: @@ -290,9 +296,7 @@ class TemplateFan(TemplateEntity, FanEntity): ) return - self._state = STATE_ON self._preset_mode = preset_mode - self._percentage = None if self._set_preset_mode_script: await self.async_run_script( @@ -301,6 +305,10 @@ class TemplateFan(TemplateEntity, FanEntity): context=self._context, ) + if self._template is None: + self._state = True + self.async_write_ha_state() + async def async_oscillate(self, oscillating: bool) -> None: """Set oscillation of the fan.""" if self._set_oscillating_script is None: @@ -340,23 +348,23 @@ class TemplateFan(TemplateEntity, FanEntity): self._state = None return - # Validate state - if result in _VALID_STATES: + if isinstance(result, bool): self._state = result - elif result in (STATE_UNAVAILABLE, STATE_UNKNOWN): - self._state = None - else: - _LOGGER.error( - "Received invalid fan is_on state: %s for entity %s. Expected: %s", - result, - self.entity_id, - ", ".join(_VALID_STATES), - ) - self._state = None + return + + if isinstance(result, str): + self._state = result.lower() in ("true", STATE_ON) + return + + self._state = False async def async_added_to_hass(self) -> None: """Register callbacks.""" - self.add_template_attribute("_state", self._template, None, self._update_state) + if self._template: + self.add_template_attribute( + "_state", self._template, None, self._update_state + ) + if self._preset_mode_template is not None: self.add_template_attribute( "_preset_mode", @@ -396,19 +404,17 @@ class TemplateFan(TemplateEntity, FanEntity): # Validate percentage try: percentage = int(float(percentage)) - except ValueError: + except (ValueError, TypeError): _LOGGER.error( "Received invalid percentage: %s for entity %s", percentage, self.entity_id, ) self._percentage = 0 - self._preset_mode = None return if 0 <= percentage <= 100: self._percentage = percentage - self._preset_mode = None else: _LOGGER.error( "Received invalid percentage: %s for entity %s", @@ -416,7 +422,6 @@ class TemplateFan(TemplateEntity, FanEntity): self.entity_id, ) self._percentage = 0 - self._preset_mode = None @callback def _update_preset_mode(self, preset_mode): @@ -424,10 +429,8 @@ class TemplateFan(TemplateEntity, FanEntity): preset_mode = str(preset_mode) if self.preset_modes and preset_mode in self.preset_modes: - self._percentage = None self._preset_mode = preset_mode elif preset_mode in (STATE_UNAVAILABLE, STATE_UNKNOWN): - self._percentage = None self._preset_mode = None else: _LOGGER.error( @@ -436,7 +439,6 @@ class TemplateFan(TemplateEntity, FanEntity): self.entity_id, self.preset_mode, ) - self._percentage = None self._preset_mode = None @callback @@ -471,3 +473,8 @@ class TemplateFan(TemplateEntity, FanEntity): ", ".join(_VALID_DIRECTIONS), ) self._direction = None + + @property + def assumed_state(self) -> bool: + """State is assumed, if no template given.""" + return self._template is None diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index 30bb9e00d59..503def425c2 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -491,7 +491,7 @@ async def test_set_percentage(hass, calls): for state, value in [ (STATE_ON, 100), (STATE_ON, 66), - (STATE_OFF, 0), + (STATE_ON, 0), ]: await common.async_set_percentage(hass, _TEST_FAN, value) assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == value @@ -516,7 +516,7 @@ async def test_increase_decrease_speed(hass, calls): (common.async_set_percentage, 100, STATE_ON, 100), (common.async_decrease_speed, None, STATE_ON, 66), (common.async_decrease_speed, None, STATE_ON, 33), - (common.async_decrease_speed, None, STATE_OFF, 0), + (common.async_decrease_speed, None, STATE_ON, 0), (common.async_increase_speed, None, STATE_ON, 33), ]: await func(hass, _TEST_FAN, extra) @@ -524,6 +524,116 @@ async def test_increase_decrease_speed(hass, calls): _verify(hass, state, value, None, None, None) +async def test_no_value_template(hass, calls): + """Test a fan without a value_template.""" + await _register_fan_sources(hass) + + with assert_setup_component(1, "fan"): + test_fan_config = { + "preset_mode_template": "{{ states('input_select.preset_mode') }}", + "percentage_template": "{{ states('input_number.percentage') }}", + "oscillating_template": "{{ states('input_select.osc') }}", + "direction_template": "{{ states('input_select.direction') }}", + "turn_on": [ + { + "service": "input_boolean.turn_on", + "entity_id": _STATE_INPUT_BOOLEAN, + }, + { + "service": "test.automation", + "data_template": { + "action": "turn_on", + "caller": "{{ this.entity_id }}", + }, + }, + ], + "turn_off": [ + { + "service": "input_boolean.turn_off", + "entity_id": _STATE_INPUT_BOOLEAN, + }, + { + "service": "test.automation", + "data_template": { + "action": "turn_off", + "caller": "{{ this.entity_id }}", + }, + }, + ], + "set_preset_mode": [ + { + "service": "input_select.select_option", + "data_template": { + "entity_id": _PRESET_MODE_INPUT_SELECT, + "option": "{{ preset_mode }}", + }, + }, + { + "service": "test.automation", + "data_template": { + "action": "set_preset_mode", + "caller": "{{ this.entity_id }}", + "option": "{{ preset_mode }}", + }, + }, + ], + "set_percentage": [ + { + "service": "input_number.set_value", + "data_template": { + "entity_id": _PERCENTAGE_INPUT_NUMBER, + "value": "{{ percentage }}", + }, + }, + { + "service": "test.automation", + "data_template": { + "action": "set_value", + "caller": "{{ this.entity_id }}", + "value": "{{ percentage }}", + }, + }, + ], + } + assert await setup.async_setup_component( + hass, + "fan", + {"fan": {"platform": "template", "fans": {"test_fan": test_fan_config}}}, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + await common.async_turn_on(hass, _TEST_FAN) + _verify(hass, STATE_ON, 0, None, None, None) + + await common.async_turn_off(hass, _TEST_FAN) + _verify(hass, STATE_OFF, 0, None, None, None) + + percent = 100 + await common.async_set_percentage(hass, _TEST_FAN, percent) + assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == percent + _verify(hass, STATE_ON, percent, None, None, None) + + await common.async_turn_off(hass, _TEST_FAN) + _verify(hass, STATE_OFF, percent, None, None, None) + + preset = "auto" + await common.async_set_preset_mode(hass, _TEST_FAN, preset) + assert hass.states.get(_PRESET_MODE_INPUT_SELECT).state == preset + _verify(hass, STATE_ON, percent, None, None, preset) + + await common.async_turn_off(hass, _TEST_FAN) + _verify(hass, STATE_OFF, percent, None, None, preset) + + await common.async_set_direction(hass, _TEST_FAN, True) + _verify(hass, STATE_OFF, percent, None, None, preset) + + await common.async_oscillate(hass, _TEST_FAN, True) + _verify(hass, STATE_OFF, percent, None, None, preset) + + async def test_increase_decrease_speed_default_speed_count(hass, calls): """Test set valid increase and decrease speed.""" await _register_components(hass) @@ -585,10 +695,7 @@ def _verify( assert attributes.get(ATTR_PRESET_MODE) == expected_preset_mode -async def _register_components( - hass, speed_list=None, preset_modes=None, speed_count=None -): - """Register basic components for testing.""" +async def _register_fan_sources(hass): with assert_setup_component(1, "input_boolean"): assert await setup.async_setup_component( hass, "input_boolean", {"input_boolean": {"state": None}} @@ -630,6 +737,13 @@ async def _register_components( }, ) + +async def _register_components( + hass, speed_list=None, preset_modes=None, speed_count=None +): + """Register basic components for testing.""" + await _register_fan_sources(hass) + with assert_setup_component(1, "fan"): value_template = """ {% if is_state('input_boolean.state', 'on') %} From 9b7eb6b5a191a9f88dc96463f177496d97f59f62 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 3 Oct 2022 14:24:11 -0400 Subject: [PATCH 132/985] Reduce coverage gaps for zwave_js (#79520) Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/__init__.py | 30 +++++------- .../components/zwave_js/diagnostics.py | 35 ++------------ homeassistant/components/zwave_js/helpers.py | 41 +++++++++++++++- tests/components/zwave_js/common.py | 25 ++++++++++ tests/components/zwave_js/test_addon.py | 15 ++++++ .../components/zwave_js/test_binary_sensor.py | 48 ++++++++++++++++++- tests/components/zwave_js/test_climate.py | 29 +++++++++++ 7 files changed, 170 insertions(+), 53 deletions(-) create mode 100644 tests/components/zwave_js/test_addon.py diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index f8828e8cdd0..9082048badf 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -142,7 +142,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await client.connect() except InvalidServerVersion as err: if use_addon: - async_ensure_addon_updated(hass) + addon_manager = _get_addon_manager(hass) + addon_manager.async_schedule_update_addon(catch_error=True) else: async_create_issue( hass, @@ -205,8 +206,7 @@ async def start_client( LOGGER.info("Connection to Zwave JS Server initialized") - if client.driver is None: - raise RuntimeError("Driver not ready.") + assert client.driver await driver_events.setup(client.driver) @@ -789,17 +789,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: info = hass.data[DOMAIN][entry.entry_id] driver_events: DriverEvents = info[DATA_DRIVER_EVENTS] - tasks: list[asyncio.Task | Coroutine] = [] - for platform, task in driver_events.platform_setup_tasks.items(): - if task.done(): - tasks.append( - hass.config_entries.async_forward_entry_unload(entry, platform) - ) - else: - task.cancel() - tasks.append(task) + tasks: list[Coroutine] = [ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform, task in driver_events.platform_setup_tasks.items() + if not task.cancel() + ] - unload_ok = all(await asyncio.gather(*tasks)) + unload_ok = all(await asyncio.gather(*tasks)) if tasks else True if DATA_CLIENT_LISTEN_TASK in info: await disconnect_client(hass, entry) @@ -842,9 +838,7 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) -> None: """Ensure that Z-Wave JS add-on is installed and running.""" - addon_manager: AddonManager = get_addon_manager(hass) - if addon_manager.task_in_progress(): - raise ConfigEntryNotReady + addon_manager = _get_addon_manager(hass) try: addon_info = await addon_manager.async_get_addon_info() except AddonError as err: @@ -911,9 +905,9 @@ async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) -> @callback -def async_ensure_addon_updated(hass: HomeAssistant) -> None: +def _get_addon_manager(hass: HomeAssistant) -> AddonManager: """Ensure that Z-Wave JS add-on is updated and running.""" addon_manager: AddonManager = get_addon_manager(hass) if addon_manager.task_in_progress(): raise ConfigEntryNotReady - addon_manager.async_schedule_update_addon(catch_error=True) + return addon_manager diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index ef34a2f12de..068be7feb0b 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -2,7 +2,6 @@ from __future__ import annotations from copy import deepcopy -from dataclasses import astuple, dataclass from typing import Any from zwave_js_server.client import Client @@ -21,27 +20,13 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DATA_CLIENT, DOMAIN, USER_AGENT from .helpers import ( + ZwaveValueMatcher, get_home_and_node_id_from_device_entry, get_state_key_from_unique_id, get_value_id_from_unique_id, + value_matches_matcher, ) - -@dataclass -class ZwaveValueMatcher: - """Class to allow matching a Z-Wave Value.""" - - property_: str | int | None = None - command_class: int | None = None - endpoint: int | None = None - property_key: str | int | None = None - - def __post_init__(self) -> None: - """Post initialization check.""" - if all(val is None for val in astuple(self)): - raise ValueError("At least one of the fields must be set.") - - KEYS_TO_REDACT = {"homeId", "location"} VALUES_TO_REDACT = ( @@ -55,21 +40,7 @@ def redact_value_of_zwave_value(zwave_value: ValueDataType) -> ValueDataType: if zwave_value.get("value") in (None, ""): return zwave_value for value_to_redact in VALUES_TO_REDACT: - command_class = None - if "commandClass" in zwave_value: - command_class = CommandClass(zwave_value["commandClass"]) - zwave_value_id = ZwaveValueMatcher( - property_=zwave_value.get("property"), - command_class=command_class, - endpoint=zwave_value.get("endpoint"), - property_key=zwave_value.get("propertyKey"), - ) - if all( - redacted_field_val is None or redacted_field_val == zwave_value_field_val - for redacted_field_val, zwave_value_field_val in zip( - astuple(value_to_redact), astuple(zwave_value_id) - ) - ): + if value_matches_matcher(value_to_redact, zwave_value): redacted_value: ValueDataType = deepcopy(zwave_value) redacted_value["value"] = REDACTED return redacted_value diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 6949f3654a5..792bd4fc1b1 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -2,18 +2,19 @@ from __future__ import annotations from collections.abc import Callable -from dataclasses import dataclass +from dataclasses import astuple, dataclass import logging from typing import Any, cast import voluptuous as vol from zwave_js_server.client import Client as ZwaveClient -from zwave_js_server.const import ConfigurationValueType +from zwave_js_server.const import CommandClass, ConfigurationValueType from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import ( ConfigurationValue, Value as ZwaveValue, + ValueDataType, get_value_id_str, ) @@ -55,6 +56,42 @@ class ZwaveValueID: property_key: str | int | None = None +@dataclass +class ZwaveValueMatcher: + """Class to allow matching a Z-Wave Value.""" + + property_: str | int | None = None + command_class: int | None = None + endpoint: int | None = None + property_key: str | int | None = None + + def __post_init__(self) -> None: + """Post initialization check.""" + if all(val is None for val in astuple(self)): + raise ValueError("At least one of the fields must be set.") + + +def value_matches_matcher( + matcher: ZwaveValueMatcher, value_data: ValueDataType +) -> bool: + """Return whether value matches matcher.""" + command_class = None + if "commandClass" in value_data: + command_class = CommandClass(value_data["commandClass"]) + zwave_value_id = ZwaveValueMatcher( + property_=value_data.get("property"), + command_class=command_class, + endpoint=value_data.get("endpoint"), + property_key=value_data.get("propertyKey"), + ) + return all( + redacted_field_val is None or redacted_field_val == zwave_value_field_val + for redacted_field_val, zwave_value_field_val in zip( + astuple(matcher), astuple(zwave_value_id) + ) + ) + + @callback def get_value_id_from_unique_id(unique_id: str) -> str | None: """ diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index c2079564dcf..49fbe96f162 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -1,4 +1,16 @@ """Provide common test tools for Z-Wave JS.""" +from __future__ import annotations + +from copy import deepcopy +from typing import Any + +from zwave_js_server.model.node.data_model import NodeDataType + +from homeassistant.components.zwave_js.helpers import ( + ZwaveValueMatcher, + value_matches_matcher, +) + AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature" BATTERY_SENSOR = "sensor.multisensor_6_battery_level" TAMPER_SENSOR = "binary_sensor.multisensor_6_tampering_product_cover_removed" @@ -37,3 +49,16 @@ HUMIDIFIER_ADC_T3000_ENTITY = "humidifier.adc_t3000_humidifier" DEHUMIDIFIER_ADC_T3000_ENTITY = "humidifier.adc_t3000_dehumidifier" PROPERTY_ULTRAVIOLET = "Ultraviolet" + + +def replace_value_of_zwave_value( + node_data: NodeDataType, matchers: list[ZwaveValueMatcher], new_value: Any +) -> NodeDataType: + """Replace the value of a zwave value that matches the input matchers.""" + new_node_data = deepcopy(node_data) + for value_data in new_node_data["values"]: + for matcher in matchers: + if value_matches_matcher(matcher, value_data): + value_data["value"] = new_value + + return new_node_data diff --git a/tests/components/zwave_js/test_addon.py b/tests/components/zwave_js/test_addon.py new file mode 100644 index 00000000000..754be808cea --- /dev/null +++ b/tests/components/zwave_js/test_addon.py @@ -0,0 +1,15 @@ +"""Tests for Z-Wave JS addon module.""" +import pytest + +from homeassistant.components.zwave_js.addon import AddonError, get_addon_manager + + +async def test_not_installed_raises_exception(hass, addon_not_installed): + """Test addon not installed raises exception.""" + addon_manager = get_addon_manager(hass) + + with pytest.raises(AddonError): + await addon_manager.async_configure_addon("/test", "123", "456", "789", "012") + + with pytest.raises(AddonError): + await addon_manager.async_update_addon() diff --git a/tests/components/zwave_js/test_binary_sensor.py b/tests/components/zwave_js/test_binary_sensor.py index 2a1c13b0db2..3d4971e1ce4 100644 --- a/tests/components/zwave_js/test_binary_sensor.py +++ b/tests/components/zwave_js/test_binary_sensor.py @@ -3,7 +3,7 @@ from zwave_js_server.event import Event from zwave_js_server.model.node import Node from homeassistant.components.binary_sensor import BinarySensorDeviceClass -from homeassistant.const import ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory @@ -69,6 +69,29 @@ async def test_enabled_legacy_sensor(hass, ecolink_door_sensor, integration): state = hass.states.get(ENABLED_LEGACY_BINARY_SENSOR) assert state.state == STATE_ON + # Test state updates from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 53, + "args": { + "commandClassName": "Binary Sensor", + "commandClass": 48, + "endpoint": 0, + "property": "Any", + "newValue": None, + "prevValue": True, + "propertyName": "Any", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(ENABLED_LEGACY_BINARY_SENSOR) + assert state.state == STATE_UNKNOWN + async def test_disabled_legacy_sensor(hass, multisensor_6, integration): """Test disabled legacy boolean binary sensor.""" @@ -198,3 +221,26 @@ async def test_property_sensor_door_status(hass, lock_august_pro, integration): state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR) assert state assert state.state == STATE_OFF + + # door state unknown + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 6, + "args": { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "doorStatus", + "newValue": None, + "prevValue": "open", + "propertyName": "doorStatus", + }, + }, + ) + node.receive_event(event) + state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR) + assert state + assert state.state == STATE_UNKNOWN diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index 4b4519c07b9..755423e5e43 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -1,6 +1,11 @@ """Test the Z-Wave JS climate platform.""" import pytest +from zwave_js_server.const import CommandClass +from zwave_js_server.const.command_class.thermostat import ( + THERMOSTAT_OPERATING_STATE_PROPERTY, +) from zwave_js_server.event import Event +from zwave_js_server.model.node import Node from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, @@ -25,6 +30,7 @@ from homeassistant.components.climate import ( HVACMode, ) from homeassistant.components.zwave_js.climate import ATTR_FAN_STATE +from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -37,6 +43,7 @@ from .common import ( CLIMATE_FLOOR_THERMOSTAT_ENTITY, CLIMATE_MAIN_HEAT_ACTIONNER, CLIMATE_RADIO_THERMOSTAT_ENTITY, + replace_value_of_zwave_value, ) @@ -632,3 +639,25 @@ async def test_temp_unit_fix( state = hass.states.get("climate.z_wave_thermostat") assert state assert state.attributes["current_temperature"] == 21.1 + + +async def test_thermostat_unknown_values( + hass, client, climate_radio_thermostat_ct100_plus_state, integration +): + """Test a thermostat v2 with unknown values.""" + node_state = replace_value_of_zwave_value( + climate_radio_thermostat_ct100_plus_state, + [ + ZwaveValueMatcher( + THERMOSTAT_OPERATING_STATE_PROPERTY, + command_class=CommandClass.THERMOSTAT_OPERATING_STATE, + ) + ], + None, + ) + node = Node(client, node_state) + client.driver.controller.emit("node added", {"node": node}) + await hass.async_block_till_done() + state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY) + + assert ATTR_HVAC_ACTION not in state.attributes From e8650dd4b7261f269e8386379f6c30a069b9fc2f Mon Sep 17 00:00:00 2001 From: Jafar Atili Date: Mon, 3 Oct 2022 21:34:02 +0300 Subject: [PATCH 133/985] Add climate platform to switchbee integration (#78385) * Added Climate platform to switchbee integration * uploaded missing file * Applied code review feedback from other PR * Addressed comments from previous PRs * fixed misspell error * fixed mistake in the code * added type hints * fixes * fixes * Update homeassistant/components/switchbee/climate.py Co-authored-by: Shay Levy * Update homeassistant/components/switchbee/entity.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/switchbee/climate.py Co-authored-by: Shay Levy * Update homeassistant/components/switchbee/climate.py Co-authored-by: Shay Levy * Update homeassistant/components/switchbee/climate.py Co-authored-by: Shay Levy * Update homeassistant/components/switchbee/climate.py Co-authored-by: Shay Levy * Update homeassistant/components/switchbee/climate.py Co-authored-by: Shay Levy * Update homeassistant/components/switchbee/climate.py Co-authored-by: Shay Levy * fixes * Update homeassistant/components/switchbee/climate.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * fixes * Update homeassistant/components/switchbee/climate.py * Update homeassistant/components/switchbee/climate.py Co-authored-by: Shay Levy * more fixes Co-authored-by: Shay Levy Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .coveragerc | 1 + .../components/switchbee/__init__.py | 2 +- homeassistant/components/switchbee/climate.py | 182 ++++++++++++++++++ homeassistant/components/switchbee/entity.py | 9 +- 4 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/switchbee/climate.py diff --git a/.coveragerc b/.coveragerc index 45b9c8d1086..298bc9020ef 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1232,6 +1232,7 @@ omit = homeassistant/components/swisscom/device_tracker.py homeassistant/components/switchbee/__init__.py homeassistant/components/switchbee/button.py + homeassistant/components/switchbee/climate.py homeassistant/components/switchbee/coordinator.py homeassistant/components/switchbee/cover.py homeassistant/components/switchbee/entity.py diff --git a/homeassistant/components/switchbee/__init__.py b/homeassistant/components/switchbee/__init__.py index 7a843697e8d..5848477ec71 100644 --- a/homeassistant/components/switchbee/__init__.py +++ b/homeassistant/components/switchbee/__init__.py @@ -15,6 +15,7 @@ from .coordinator import SwitchBeeCoordinator PLATFORMS: list[Platform] = [ Platform.BUTTON, + Platform.CLIMATE, Platform.COVER, Platform.LIGHT, Platform.SWITCH, @@ -27,7 +28,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: central_unit = entry.data[CONF_HOST] user = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] - websession = async_get_clientsession(hass, verify_ssl=False) api = CentralUnitAPI(central_unit, user, password, websession) try: diff --git a/homeassistant/components/switchbee/climate.py b/homeassistant/components/switchbee/climate.py new file mode 100644 index 00000000000..8d0024b75ed --- /dev/null +++ b/homeassistant/components/switchbee/climate.py @@ -0,0 +1,182 @@ +"""Support for SwitchBee climate.""" +from __future__ import annotations + +from typing import Any + +from switchbee.api import SwitchBeeDeviceOfflineError, SwitchBeeError +from switchbee.const import ( + ApiAttribute, + ThermostatFanSpeed, + ThermostatMode, + ThermostatTemperatureUnit, +) +from switchbee.device import ApiStateCommand, SwitchBeeThermostat + +from homeassistant.components.climate import ( + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + ClimateEntity, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import SwitchBeeCoordinator +from .entity import SwitchBeeDeviceEntity + +FAN_SB_TO_HASS = { + ThermostatFanSpeed.AUTO: FAN_AUTO, + ThermostatFanSpeed.LOW: FAN_LOW, + ThermostatFanSpeed.MEDIUM: FAN_MEDIUM, + ThermostatFanSpeed.HIGH: FAN_HIGH, +} + +FAN_HASS_TO_SB: dict[str | None, str] = { + FAN_AUTO: ThermostatFanSpeed.AUTO, + FAN_LOW: ThermostatFanSpeed.LOW, + FAN_MEDIUM: ThermostatFanSpeed.MEDIUM, + FAN_HIGH: ThermostatFanSpeed.HIGH, +} + +HVAC_MODE_SB_TO_HASS = { + ThermostatMode.COOL: HVACMode.COOL, + ThermostatMode.HEAT: HVACMode.HEAT, + ThermostatMode.FAN: HVACMode.FAN_ONLY, +} + +HVAC_MODE_HASS_TO_SB: dict[HVACMode | str | None, str] = { + HVACMode.COOL: ThermostatMode.COOL, + HVACMode.HEAT: ThermostatMode.HEAT, + HVACMode.FAN_ONLY: ThermostatMode.FAN, +} + +HVAC_ACTION_SB_TO_HASS = { + ThermostatMode.COOL: HVACAction.COOLING, + ThermostatMode.HEAT: HVACAction.HEATING, + ThermostatMode.FAN: HVACAction.FAN, +} + +HVAC_UNIT_SB_TO_HASS = { + ThermostatTemperatureUnit.CELSIUS: TEMP_CELSIUS, + ThermostatTemperatureUnit.FAHRENHEIT: TEMP_FAHRENHEIT, +} + +SUPPORTED_FAN_MODES = [FAN_AUTO, FAN_HIGH, FAN_MEDIUM, FAN_LOW] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up SwitchBee climate.""" + coordinator: SwitchBeeCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + SwitchBeeClimateEntity(switchbee_device, coordinator) + for switchbee_device in coordinator.data.values() + if isinstance(switchbee_device, SwitchBeeThermostat) + ) + + +class SwitchBeeClimateEntity(SwitchBeeDeviceEntity[SwitchBeeThermostat], ClimateEntity): + """Representation of a SwitchBee climate.""" + + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ) + _attr_fan_modes = SUPPORTED_FAN_MODES + _attr_target_temperature_step = 1 + + def __init__( + self, + device: SwitchBeeThermostat, + coordinator: SwitchBeeCoordinator, + ) -> None: + """Initialize the Switchbee switch.""" + super().__init__(device, coordinator) + # set HVAC capabilities + self._attr_max_temp = device.max_temperature + self._attr_min_temp = device.min_temperature + self._attr_temperature_unit = HVAC_UNIT_SB_TO_HASS[device.unit] + self._attr_hvac_modes = [HVAC_MODE_SB_TO_HASS[mode] for mode in device.modes] + self._attr_hvac_modes.append(HVACMode.OFF) + self._update_attrs_from_coordinator() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_attrs_from_coordinator() + super()._handle_coordinator_update() + + def _update_attrs_from_coordinator(self) -> None: + + coordinator_device = self._get_coordinator_device() + + self._attr_hvac_mode: HVACMode = ( + HVACMode.OFF + if coordinator_device.state == ApiStateCommand.OFF + else HVAC_MODE_SB_TO_HASS[coordinator_device.mode] + ) + self._attr_fan_mode = FAN_SB_TO_HASS[coordinator_device.fan] + self._attr_current_temperature = coordinator_device.temperature + self._attr_target_temperature = coordinator_device.target_temperature + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set hvac mode.""" + + if hvac_mode == HVACMode.OFF: + await self._operate(power=ApiStateCommand.OFF) + else: + await self._operate( + power=ApiStateCommand.ON, mode=HVAC_MODE_HASS_TO_SB[hvac_mode] + ) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + await self._operate(target_temperature=kwargs[ATTR_TEMPERATURE]) + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set AC fan mode.""" + await self._operate(fan=FAN_HASS_TO_SB[fan_mode]) + + async def _operate( + self, + power: str | None = None, + mode: str | None = None, + fan: str | None = None, + target_temperature: int | None = None, + ) -> None: + """Send request to central unit.""" + + if power is None: + power = ApiStateCommand.ON + if self.hvac_mode == HVACMode.OFF: + power = ApiStateCommand.OFF + if mode is None: + mode = HVAC_MODE_HASS_TO_SB[self.hvac_mode] + if fan is None: + fan = FAN_HASS_TO_SB[self.fan_mode] + if target_temperature is None: + target_temperature = int(self.target_temperature or 0) + + state: dict[str, int | str] = { + ApiAttribute.POWER: power, + ApiAttribute.MODE: mode, + ApiAttribute.FAN: fan, + ApiAttribute.CONFIGURED_TEMPERATURE: target_temperature, + } + + try: + await self.coordinator.api.set_state(self._device.id, state) + except (SwitchBeeError, SwitchBeeDeviceOfflineError) as exp: + raise HomeAssistantError( + f"Failed to set {self.name} state {state}, error: {str(exp)}" + ) from exp + else: + await self.coordinator.async_refresh() diff --git a/homeassistant/components/switchbee/entity.py b/homeassistant/components/switchbee/entity.py index 4fed0c61393..5129446a204 100644 --- a/homeassistant/components/switchbee/entity.py +++ b/homeassistant/components/switchbee/entity.py @@ -4,7 +4,7 @@ from typing import Generic, TypeVar, cast from switchbee import SWITCHBEE_BRAND from switchbee.api import SwitchBeeDeviceOfflineError, SwitchBeeError -from switchbee.device import SwitchBeeBaseDevice +from switchbee.device import DeviceType, SwitchBeeBaseDevice from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -46,12 +46,15 @@ class SwitchBeeDeviceEntity(SwitchBeeEntity[_DeviceTypeT]): """Initialize the Switchbee device.""" super().__init__(device, coordinator) self._is_online: bool = True + identifier = ( + device.id if device.type == DeviceType.Thermostat else device.unit_id + ) self._attr_device_info = DeviceInfo( - name=f"SwitchBee {device.unit_id}", + name=f"SwitchBee {identifier}", identifiers={ ( DOMAIN, - f"{device.unit_id}-{coordinator.mac_formatted}", + f"{identifier}-{coordinator.mac_formatted}", ) }, manufacturer=SWITCHBEE_BRAND, From a2e3978d5312282bc50b2163b817ca738b560a6f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 3 Oct 2022 21:42:44 +0200 Subject: [PATCH 134/985] Don't normalize units of long term statistics (#79320) * Don't normalize units of long term statistics * Update statistics.py --- .../components/recorder/statistics.py | 37 +-- homeassistant/components/sensor/recorder.py | 52 ++-- .../components/recorder/test_websocket_api.py | 54 ++-- tests/components/sensor/test_recorder.py | 233 ++++++------------ 4 files changed, 155 insertions(+), 221 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index ef066b82060..ad948b560bb 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -125,14 +125,14 @@ QUERY_STATISTIC_META = [ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { - DistanceConverter.NORMALIZED_UNIT: DistanceConverter, - EnergyConverter.NORMALIZED_UNIT: EnergyConverter, - MassConverter.NORMALIZED_UNIT: MassConverter, - PowerConverter.NORMALIZED_UNIT: PowerConverter, - PressureConverter.NORMALIZED_UNIT: PressureConverter, - SpeedConverter.NORMALIZED_UNIT: SpeedConverter, - TemperatureConverter.NORMALIZED_UNIT: TemperatureConverter, - VolumeConverter.NORMALIZED_UNIT: VolumeConverter, + **{unit: DistanceConverter for unit in DistanceConverter.VALID_UNITS}, + **{unit: EnergyConverter for unit in EnergyConverter.VALID_UNITS}, + **{unit: MassConverter for unit in MassConverter.VALID_UNITS}, + **{unit: PowerConverter for unit in PowerConverter.VALID_UNITS}, + **{unit: PressureConverter for unit in PressureConverter.VALID_UNITS}, + **{unit: SpeedConverter for unit in SpeedConverter.VALID_UNITS}, + **{unit: TemperatureConverter for unit in TemperatureConverter.VALID_UNITS}, + **{unit: VolumeConverter for unit in VolumeConverter.VALID_UNITS}, } @@ -140,7 +140,7 @@ _LOGGER = logging.getLogger(__name__) def _get_unit_class(unit: str | None) -> str | None: - """Get corresponding unit class from from the normalized statistics unit.""" + """Get corresponding unit class from from the statistics unit.""" if converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(unit): return converter.UNIT_CLASS return None @@ -151,7 +151,7 @@ def _get_statistic_to_display_unit_converter( state_unit: str | None, requested_units: dict[str, str] | None, ) -> Callable[[float | None], float | None]: - """Prepare a converter from the normalized statistics unit to display unit.""" + """Prepare a converter from the statistics unit to display unit.""" def no_conversion(val: float | None) -> float | None: """Return val.""" @@ -175,21 +175,26 @@ def _get_statistic_to_display_unit_converter( return no_conversion def from_normalized_unit( - val: float | None, conv: type[BaseUnitConverter], to_unit: str + val: float | None, conv: type[BaseUnitConverter], from_unit: str, to_unit: str ) -> float | None: """Return val.""" if val is None: return val - return conv.convert(val, from_unit=conv.NORMALIZED_UNIT, to_unit=to_unit) + return conv.convert(val, from_unit=from_unit, to_unit=to_unit) - return partial(from_normalized_unit, conv=converter, to_unit=display_unit) + return partial( + from_normalized_unit, + conv=converter, + from_unit=statistic_unit, + to_unit=display_unit, + ) def _get_display_to_statistic_unit_converter( display_unit: str | None, statistic_unit: str | None, ) -> Callable[[float], float]: - """Prepare a converter from the display unit to the normalized statistics unit.""" + """Prepare a converter from the display unit to the statistics unit.""" def no_conversion(val: float) -> float: """Return val.""" @@ -201,9 +206,7 @@ def _get_display_to_statistic_unit_converter( if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None: return no_conversion - return partial( - converter.convert, from_unit=display_unit, to_unit=converter.NORMALIZED_UNIT - ) + return partial(converter.convert, from_unit=display_unit, to_unit=statistic_unit) def _get_unit_converter( diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 4380efbd2c3..144502dd81a 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -164,10 +164,10 @@ def _normalize_states( if device_class not in UNIT_CONVERTERS or ( old_metadata and old_metadata["unit_of_measurement"] - != UNIT_CONVERTERS[device_class].NORMALIZED_UNIT + not in UNIT_CONVERTERS[device_class].VALID_UNITS ): # We're either not normalizing this device class or this entity is not stored - # normalized, return the states as they are + # in a supported unit, return the states as they are fstates = [] for state in entity_history: try: @@ -205,6 +205,10 @@ def _normalize_states( converter = UNIT_CONVERTERS[device_class] fstates = [] + statistics_unit: str | None = None + if old_metadata: + statistics_unit = old_metadata["unit_of_measurement"] + for state in entity_history: try: fstate = _parse_float(state.state) @@ -224,17 +228,19 @@ def _normalize_states( device_class, ) continue + if statistics_unit is None: + statistics_unit = state_unit fstates.append( ( converter.convert( - fstate, from_unit=state_unit, to_unit=converter.NORMALIZED_UNIT + fstate, from_unit=state_unit, to_unit=statistics_unit ), state, ) ) - return UNIT_CONVERTERS[device_class].NORMALIZED_UNIT, state_unit, fstates + return statistics_unit, state_unit, fstates def _suggest_report_issue(hass: HomeAssistant, entity_id: str) -> str: @@ -423,7 +429,7 @@ def _compile_statistics( # noqa: C901 device_class = _state.attributes.get(ATTR_DEVICE_CLASS) entity_history = history_list[entity_id] - normalized_unit, state_unit, fstates = _normalize_states( + statistics_unit, state_unit, fstates = _normalize_states( hass, session, old_metadatas, @@ -438,7 +444,7 @@ def _compile_statistics( # noqa: C901 state_class = _state.attributes[ATTR_STATE_CLASS] to_process.append( - (entity_id, normalized_unit, state_unit, state_class, fstates) + (entity_id, statistics_unit, state_unit, state_class, fstates) ) if "sum" in wanted_statistics[entity_id]: to_query.append(entity_id) @@ -448,14 +454,14 @@ def _compile_statistics( # noqa: C901 ) for ( # pylint: disable=too-many-nested-blocks entity_id, - normalized_unit, + statistics_unit, state_unit, state_class, fstates, ) in to_process: # Check metadata if old_metadata := old_metadatas.get(entity_id): - if old_metadata[1]["unit_of_measurement"] != normalized_unit: + if old_metadata[1]["unit_of_measurement"] != statistics_unit: if WARN_UNSTABLE_UNIT not in hass.data: hass.data[WARN_UNSTABLE_UNIT] = set() if entity_id not in hass.data[WARN_UNSTABLE_UNIT]: @@ -467,7 +473,7 @@ def _compile_statistics( # noqa: C901 "Go to %s to fix this", "normalized " if device_class in UNIT_CONVERTERS else "", entity_id, - normalized_unit, + statistics_unit, old_metadata[1]["unit_of_measurement"], old_metadata[1]["unit_of_measurement"], LINK_DEV_STATISTICS, @@ -481,7 +487,7 @@ def _compile_statistics( # noqa: C901 "name": None, "source": RECORDER_DOMAIN, "statistic_id": entity_id, - "unit_of_measurement": normalized_unit, + "unit_of_measurement": statistics_unit, } # Make calculations @@ -629,14 +635,13 @@ def list_statistic_ids( if state_unit not in converter.VALID_UNITS: continue - statistics_unit = converter.NORMALIZED_UNIT result[state.entity_id] = { "has_mean": "mean" in provided_statistics, "has_sum": "sum" in provided_statistics, "name": None, "source": RECORDER_DOMAIN, "statistic_id": state.entity_id, - "unit_of_measurement": statistics_unit, + "unit_of_measurement": state_unit, } return result @@ -680,13 +685,13 @@ def validate_statistics( metadata_unit = metadata[1]["unit_of_measurement"] if device_class not in UNIT_CONVERTERS: - issue_type = ( - "units_changed_can_convert" - if statistics.can_convert_units(metadata_unit, state_unit) - else "units_changed" - ) if state_unit != metadata_unit: # The unit has changed + issue_type = ( + "units_changed_can_convert" + if statistics.can_convert_units(metadata_unit, state_unit) + else "units_changed" + ) validation_result[entity_id].append( statistics.ValidationIssue( issue_type, @@ -697,22 +702,19 @@ def validate_statistics( }, ) ) - elif metadata_unit != UNIT_CONVERTERS[device_class].NORMALIZED_UNIT: + elif metadata_unit not in UNIT_CONVERTERS[device_class].VALID_UNITS: # The unit in metadata is not supported for this device class - statistics_unit = UNIT_CONVERTERS[device_class].NORMALIZED_UNIT - issue_type = ( - "unsupported_unit_metadata_can_convert" - if statistics.can_convert_units(metadata_unit, statistics_unit) - else "unsupported_unit_metadata" + valid_units = ", ".join( + sorted(UNIT_CONVERTERS[device_class].VALID_UNITS) ) validation_result[entity_id].append( statistics.ValidationIssue( - issue_type, + "unsupported_unit_metadata", { "statistic_id": entity_id, "device_class": device_class, "metadata_unit": metadata_unit, - "supported_unit": statistics_unit, + "supported_unit": valid_units, }, ) ) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 58893ee3bb1..6058fd6a2e5 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -591,26 +591,26 @@ async def test_statistics_during_period_bad_end_time( [ (IMPERIAL_SYSTEM, DISTANCE_SENSOR_M_ATTRIBUTES, "m", "m", "distance"), (METRIC_SYSTEM, DISTANCE_SENSOR_M_ATTRIBUTES, "m", "m", "distance"), - (IMPERIAL_SYSTEM, DISTANCE_SENSOR_FT_ATTRIBUTES, "ft", "m", "distance"), - (METRIC_SYSTEM, DISTANCE_SENSOR_FT_ATTRIBUTES, "ft", "m", "distance"), - (IMPERIAL_SYSTEM, ENERGY_SENSOR_WH_ATTRIBUTES, "Wh", "kWh", "energy"), - (METRIC_SYSTEM, ENERGY_SENSOR_WH_ATTRIBUTES, "Wh", "kWh", "energy"), - (IMPERIAL_SYSTEM, GAS_SENSOR_FT3_ATTRIBUTES, "ft³", "m³", "volume"), - (METRIC_SYSTEM, GAS_SENSOR_FT3_ATTRIBUTES, "ft³", "m³", "volume"), - (IMPERIAL_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "W", "power"), - (METRIC_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "W", "power"), - (IMPERIAL_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "Pa", "pressure"), - (METRIC_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "Pa", "pressure"), - (IMPERIAL_SYSTEM, SPEED_SENSOR_KPH_ATTRIBUTES, "km/h", "m/s", "speed"), - (METRIC_SYSTEM, SPEED_SENSOR_KPH_ATTRIBUTES, "km/h", "m/s", "speed"), + (IMPERIAL_SYSTEM, DISTANCE_SENSOR_FT_ATTRIBUTES, "ft", "ft", "distance"), + (METRIC_SYSTEM, DISTANCE_SENSOR_FT_ATTRIBUTES, "ft", "ft", "distance"), + (IMPERIAL_SYSTEM, ENERGY_SENSOR_WH_ATTRIBUTES, "Wh", "Wh", "energy"), + (METRIC_SYSTEM, ENERGY_SENSOR_WH_ATTRIBUTES, "Wh", "Wh", "energy"), + (IMPERIAL_SYSTEM, GAS_SENSOR_FT3_ATTRIBUTES, "ft³", "ft³", "volume"), + (METRIC_SYSTEM, GAS_SENSOR_FT3_ATTRIBUTES, "ft³", "ft³", "volume"), + (IMPERIAL_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "kW", "power"), + (METRIC_SYSTEM, POWER_SENSOR_KW_ATTRIBUTES, "kW", "kW", "power"), + (IMPERIAL_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "hPa", "pressure"), + (METRIC_SYSTEM, PRESSURE_SENSOR_HPA_ATTRIBUTES, "hPa", "hPa", "pressure"), + (IMPERIAL_SYSTEM, SPEED_SENSOR_KPH_ATTRIBUTES, "km/h", "km/h", "speed"), + (METRIC_SYSTEM, SPEED_SENSOR_KPH_ATTRIBUTES, "km/h", "km/h", "speed"), (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "°C", "temperature"), (METRIC_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "°C", "temperature"), - (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°C", "temperature"), - (METRIC_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°C", "temperature"), - (IMPERIAL_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES, "ft³", "m³", "volume"), - (METRIC_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES, "ft³", "m³", "volume"), - (IMPERIAL_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES_TOTAL, "ft³", "m³", "volume"), - (METRIC_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES_TOTAL, "ft³", "m³", "volume"), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°F", "temperature"), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°F", "temperature"), + (IMPERIAL_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES, "ft³", "ft³", "volume"), + (METRIC_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES, "ft³", "ft³", "volume"), + (IMPERIAL_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES_TOTAL, "ft³", "ft³", "volume"), + (METRIC_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES_TOTAL, "ft³", "ft³", "volume"), ], ) async def test_list_statistic_ids( @@ -904,7 +904,7 @@ async def test_update_statistics_metadata( "name": None, "source": "recorder", "statistics_unit_of_measurement": "kW", - "unit_class": None, + "unit_class": "power", } ] @@ -994,7 +994,7 @@ async def test_change_statistics_unit(hass, hass_ws_client, recorder_mock): "name": None, "source": "recorder", "statistics_unit_of_measurement": "kW", - "unit_class": None, + "unit_class": "power", } ] @@ -1101,7 +1101,7 @@ async def test_change_statistics_unit_errors( "name": None, "source": "recorder", "statistics_unit_of_measurement": "kW", - "unit_class": None, + "unit_class": "power", } ] @@ -1504,7 +1504,7 @@ async def test_get_statistics_metadata( "has_sum": has_sum, "name": None, "source": "recorder", - "statistics_unit_of_measurement": unit, + "statistics_unit_of_measurement": attributes["unit_of_measurement"], "unit_class": unit_class, } ] @@ -1531,7 +1531,7 @@ async def test_get_statistics_metadata( "has_sum": has_sum, "name": None, "source": "recorder", - "statistics_unit_of_measurement": unit, + "statistics_unit_of_measurement": attributes["unit_of_measurement"], "unit_class": unit_class, } ] @@ -2160,9 +2160,9 @@ async def test_adjust_sum_statistics_gas( "state_unit, statistic_unit, unit_class, factor, valid_units, invalid_units", ( ("kWh", "kWh", "energy", 1, ("Wh", "kWh", "MWh"), ("ft³", "m³", "cats", None)), - ("MWh", "MWh", None, 1, ("MWh",), ("Wh", "kWh", "ft³", "m³", "cats", None)), + ("MWh", "MWh", "energy", 1, ("Wh", "kWh", "MWh"), ("ft³", "m³", "cats", None)), ("m³", "m³", "volume", 1, ("ft³", "m³"), ("Wh", "kWh", "MWh", "cats", None)), - ("ft³", "ft³", None, 1, ("ft³",), ("m³", "Wh", "kWh", "MWh", "cats", None)), + ("ft³", "ft³", "volume", 1, ("ft³", "m³"), ("Wh", "kWh", "MWh", "cats", None)), ("dogs", "dogs", None, 1, ("dogs",), ("cats", None)), (None, None, None, 1, (None,), ("cats",)), ), @@ -2262,7 +2262,7 @@ async def test_adjust_sum_statistics_errors( "statistic_id": statistic_id, "name": "Total imported energy", "source": source, - "statistics_unit_of_measurement": statistic_unit, + "statistics_unit_of_measurement": state_unit, "unit_class": unit_class, } ] @@ -2276,7 +2276,7 @@ async def test_adjust_sum_statistics_errors( "name": "Total imported energy", "source": source, "statistic_id": statistic_id, - "unit_of_measurement": statistic_unit, + "unit_of_measurement": state_unit, }, ) } diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 637d17e21a8..99aa3a3bf8e 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -86,22 +86,22 @@ def set_time_zone(): ("battery", "%", "%", "%", None, 13.050847, -10, 30), ("battery", None, None, None, None, 13.050847, -10, 30), ("distance", "m", "m", "m", "distance", 13.050847, -10, 30), - ("distance", "mi", "mi", "m", "distance", 13.050847, -10, 30), + ("distance", "mi", "mi", "mi", "distance", 13.050847, -10, 30), ("humidity", "%", "%", "%", None, 13.050847, -10, 30), ("humidity", None, None, None, None, 13.050847, -10, 30), ("pressure", "Pa", "Pa", "Pa", "pressure", 13.050847, -10, 30), - ("pressure", "hPa", "hPa", "Pa", "pressure", 13.050847, -10, 30), - ("pressure", "mbar", "mbar", "Pa", "pressure", 13.050847, -10, 30), - ("pressure", "inHg", "inHg", "Pa", "pressure", 13.050847, -10, 30), - ("pressure", "psi", "psi", "Pa", "pressure", 13.050847, -10, 30), + ("pressure", "hPa", "hPa", "hPa", "pressure", 13.050847, -10, 30), + ("pressure", "mbar", "mbar", "mbar", "pressure", 13.050847, -10, 30), + ("pressure", "inHg", "inHg", "inHg", "pressure", 13.050847, -10, 30), + ("pressure", "psi", "psi", "psi", "pressure", 13.050847, -10, 30), ("speed", "m/s", "m/s", "m/s", "speed", 13.050847, -10, 30), - ("speed", "mph", "mph", "m/s", "speed", 13.050847, -10, 30), + ("speed", "mph", "mph", "mph", "speed", 13.050847, -10, 30), ("temperature", "°C", "°C", "°C", "temperature", 13.050847, -10, 30), - ("temperature", "°F", "°F", "°C", "temperature", 13.050847, -10, 30), + ("temperature", "°F", "°F", "°F", "temperature", 13.050847, -10, 30), ("volume", "m³", "m³", "m³", "volume", 13.050847, -10, 30), - ("volume", "ft³", "ft³", "m³", "volume", 13.050847, -10, 30), + ("volume", "ft³", "ft³", "ft³", "volume", 13.050847, -10, 30), ("weight", "g", "g", "g", "mass", 13.050847, -10, 30), - ("weight", "oz", "oz", "g", "mass", 13.050847, -10, 30), + ("weight", "oz", "oz", "oz", "mass", 13.050847, -10, 30), ], ) def test_compile_hourly_statistics( @@ -355,29 +355,29 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes "units, device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ (IMPERIAL_SYSTEM, "distance", "m", "m", "m", "distance", 1), - (IMPERIAL_SYSTEM, "distance", "mi", "mi", "m", "distance", 1), + (IMPERIAL_SYSTEM, "distance", "mi", "mi", "mi", "distance", 1), (IMPERIAL_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 1), - (IMPERIAL_SYSTEM, "energy", "Wh", "Wh", "kWh", "energy", 1), + (IMPERIAL_SYSTEM, "energy", "Wh", "Wh", "Wh", "energy", 1), (IMPERIAL_SYSTEM, "gas", "m³", "m³", "m³", "volume", 1), - (IMPERIAL_SYSTEM, "gas", "ft³", "ft³", "m³", "volume", 1), + (IMPERIAL_SYSTEM, "gas", "ft³", "ft³", "ft³", "volume", 1), (IMPERIAL_SYSTEM, "monetary", "EUR", "EUR", "EUR", None, 1), (IMPERIAL_SYSTEM, "monetary", "SEK", "SEK", "SEK", None, 1), (IMPERIAL_SYSTEM, "volume", "m³", "m³", "m³", "volume", 1), - (IMPERIAL_SYSTEM, "volume", "ft³", "ft³", "m³", "volume", 1), + (IMPERIAL_SYSTEM, "volume", "ft³", "ft³", "ft³", "volume", 1), (IMPERIAL_SYSTEM, "weight", "g", "g", "g", "mass", 1), - (IMPERIAL_SYSTEM, "weight", "oz", "oz", "g", "mass", 1), + (IMPERIAL_SYSTEM, "weight", "oz", "oz", "oz", "mass", 1), (METRIC_SYSTEM, "distance", "m", "m", "m", "distance", 1), - (METRIC_SYSTEM, "distance", "mi", "mi", "m", "distance", 1), + (METRIC_SYSTEM, "distance", "mi", "mi", "mi", "distance", 1), (METRIC_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 1), - (METRIC_SYSTEM, "energy", "Wh", "Wh", "kWh", "energy", 1), + (METRIC_SYSTEM, "energy", "Wh", "Wh", "Wh", "energy", 1), (METRIC_SYSTEM, "gas", "m³", "m³", "m³", "volume", 1), - (METRIC_SYSTEM, "gas", "ft³", "ft³", "m³", "volume", 1), + (METRIC_SYSTEM, "gas", "ft³", "ft³", "ft³", "volume", 1), (METRIC_SYSTEM, "monetary", "EUR", "EUR", "EUR", None, 1), (METRIC_SYSTEM, "monetary", "SEK", "SEK", "SEK", None, 1), (METRIC_SYSTEM, "volume", "m³", "m³", "m³", "volume", 1), - (METRIC_SYSTEM, "volume", "ft³", "ft³", "m³", "volume", 1), + (METRIC_SYSTEM, "volume", "ft³", "ft³", "ft³", "volume", 1), (METRIC_SYSTEM, "weight", "g", "g", "g", "mass", 1), - (METRIC_SYSTEM, "weight", "oz", "oz", "g", "mass", 1), + (METRIC_SYSTEM, "weight", "oz", "oz", "oz", "mass", 1), ], ) async def test_compile_hourly_sum_statistics_amount( @@ -548,11 +548,11 @@ async def test_compile_hourly_sum_statistics_amount( "device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ ("energy", "kWh", "kWh", "kWh", "energy", 1), - ("energy", "Wh", "Wh", "kWh", "energy", 1), + ("energy", "Wh", "Wh", "Wh", "energy", 1), ("monetary", "EUR", "EUR", "EUR", None, 1), ("monetary", "SEK", "SEK", "SEK", None, 1), ("gas", "m³", "m³", "m³", "volume", 1), - ("gas", "ft³", "ft³", "m³", "volume", 1), + ("gas", "ft³", "ft³", "ft³", "volume", 1), ], ) def test_compile_hourly_sum_statistics_amount_reset_every_state_change( @@ -957,11 +957,11 @@ def test_compile_hourly_sum_statistics_negative_state( "device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ ("energy", "kWh", "kWh", "kWh", "energy", 1), - ("energy", "Wh", "Wh", "kWh", "energy", 1), + ("energy", "Wh", "Wh", "Wh", "energy", 1), ("monetary", "EUR", "EUR", "EUR", None, 1), ("monetary", "SEK", "SEK", "SEK", None, 1), ("gas", "m³", "m³", "m³", "volume", 1), - ("gas", "ft³", "ft³", "m³", "volume", 1), + ("gas", "ft³", "ft³", "ft³", "volume", 1), ], ) def test_compile_hourly_sum_statistics_total_no_reset( @@ -1061,9 +1061,9 @@ def test_compile_hourly_sum_statistics_total_no_reset( "device_class, state_unit, display_unit, statistics_unit, unit_class, factor", [ ("energy", "kWh", "kWh", "kWh", "energy", 1), - ("energy", "Wh", "Wh", "kWh", "energy", 1), + ("energy", "Wh", "Wh", "Wh", "energy", 1), ("gas", "m³", "m³", "m³", "volume", 1), - ("gas", "ft³", "ft³", "m³", "volume", 1), + ("gas", "ft³", "ft³", "ft³", "volume", 1), ], ) def test_compile_hourly_sum_statistics_total_increasing( @@ -1431,7 +1431,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "has_sum": True, "name": None, "source": "recorder", - "statistics_unit_of_measurement": "kWh", + "statistics_unit_of_measurement": "Wh", "unit_class": "energy", }, ] @@ -1728,40 +1728,40 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog): ("measurement", "battery", "%", "%", "%", None, "mean"), ("measurement", "battery", None, None, None, None, "mean"), ("measurement", "distance", "m", "m", "m", "distance", "mean"), - ("measurement", "distance", "mi", "mi", "m", "distance", "mean"), + ("measurement", "distance", "mi", "mi", "mi", "distance", "mean"), ("total", "distance", "m", "m", "m", "distance", "sum"), - ("total", "distance", "mi", "mi", "m", "distance", "sum"), - ("total", "energy", "Wh", "Wh", "kWh", "energy", "sum"), + ("total", "distance", "mi", "mi", "mi", "distance", "sum"), + ("total", "energy", "Wh", "Wh", "Wh", "energy", "sum"), ("total", "energy", "kWh", "kWh", "kWh", "energy", "sum"), - ("measurement", "energy", "Wh", "Wh", "kWh", "energy", "mean"), + ("measurement", "energy", "Wh", "Wh", "Wh", "energy", "mean"), ("measurement", "energy", "kWh", "kWh", "kWh", "energy", "mean"), ("measurement", "humidity", "%", "%", "%", None, "mean"), ("measurement", "humidity", None, None, None, None, "mean"), ("total", "monetary", "USD", "USD", "USD", None, "sum"), ("total", "monetary", "None", "None", "None", None, "sum"), ("total", "gas", "m³", "m³", "m³", "volume", "sum"), - ("total", "gas", "ft³", "ft³", "m³", "volume", "sum"), + ("total", "gas", "ft³", "ft³", "ft³", "volume", "sum"), ("measurement", "monetary", "USD", "USD", "USD", None, "mean"), ("measurement", "monetary", "None", "None", "None", None, "mean"), ("measurement", "gas", "m³", "m³", "m³", "volume", "mean"), - ("measurement", "gas", "ft³", "ft³", "m³", "volume", "mean"), + ("measurement", "gas", "ft³", "ft³", "ft³", "volume", "mean"), ("measurement", "pressure", "Pa", "Pa", "Pa", "pressure", "mean"), - ("measurement", "pressure", "hPa", "hPa", "Pa", "pressure", "mean"), - ("measurement", "pressure", "mbar", "mbar", "Pa", "pressure", "mean"), - ("measurement", "pressure", "inHg", "inHg", "Pa", "pressure", "mean"), - ("measurement", "pressure", "psi", "psi", "Pa", "pressure", "mean"), + ("measurement", "pressure", "hPa", "hPa", "hPa", "pressure", "mean"), + ("measurement", "pressure", "mbar", "mbar", "mbar", "pressure", "mean"), + ("measurement", "pressure", "inHg", "inHg", "inHg", "pressure", "mean"), + ("measurement", "pressure", "psi", "psi", "psi", "pressure", "mean"), ("measurement", "speed", "m/s", "m/s", "m/s", "speed", "mean"), - ("measurement", "speed", "mph", "mph", "m/s", "speed", "mean"), + ("measurement", "speed", "mph", "mph", "mph", "speed", "mean"), ("measurement", "temperature", "°C", "°C", "°C", "temperature", "mean"), - ("measurement", "temperature", "°F", "°F", "°C", "temperature", "mean"), + ("measurement", "temperature", "°F", "°F", "°F", "temperature", "mean"), ("measurement", "volume", "m³", "m³", "m³", "volume", "mean"), - ("measurement", "volume", "ft³", "ft³", "m³", "volume", "mean"), + ("measurement", "volume", "ft³", "ft³", "ft³", "volume", "mean"), ("total", "volume", "m³", "m³", "m³", "volume", "sum"), - ("total", "volume", "ft³", "ft³", "m³", "volume", "sum"), + ("total", "volume", "ft³", "ft³", "ft³", "volume", "sum"), ("measurement", "weight", "g", "g", "g", "mass", "mean"), - ("measurement", "weight", "oz", "oz", "g", "mass", "mean"), + ("measurement", "weight", "oz", "oz", "oz", "mass", "mean"), ("total", "weight", "g", "g", "g", "mass", "sum"), - ("total", "weight", "oz", "oz", "g", "mass", "sum"), + ("total", "weight", "oz", "oz", "oz", "mass", "sum"), ], ) def test_list_statistic_ids( @@ -2134,7 +2134,7 @@ def test_compile_hourly_statistics_changing_units_3( @pytest.mark.parametrize( "device_class, state_unit, statistic_unit, unit_class, mean1, mean2, min, max", [ - ("power", "kW", "W", None, 13.050847, 13.333333, -10, 30), + ("power", "kW", "kW", "power", 13.050847, 13.333333, -10, 30), ], ) def test_compile_hourly_statistics_changing_device_class_1( @@ -2207,7 +2207,7 @@ def test_compile_hourly_statistics_changing_device_class_1( hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) - # Run statistics again, we get a warning, and no additional statistics is generated + # Run statistics again, additional statistics is generated do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) @@ -2265,13 +2265,9 @@ def test_compile_hourly_statistics_changing_device_class_1( hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) - # Run statistics again, we get a warning, and no additional statistics is generated + # Run statistics again, additional statistics is generated do_adhoc_statistics(hass, start=zero + timedelta(minutes=20)) wait_recording_done(hass) - assert ( - f"The normalized unit of sensor.test1 ({statistic_unit}) does not match the " - f"unit of already compiled statistics ({state_unit})" in caplog.text - ) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { @@ -2311,15 +2307,28 @@ def test_compile_hourly_statistics_changing_device_class_1( "state": None, "sum": None, }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat( + zero + timedelta(minutes=20) + ), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=25)), + "mean": approx(mean2), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + }, ] } assert "Error while processing event StatisticsTask" not in caplog.text @pytest.mark.parametrize( - "device_class, state_unit, display_unit, statistic_unit, unit_class, mean, min, max", + "device_class, state_unit, display_unit, statistic_unit, unit_class, mean, mean2, min, max", [ - ("power", "kW", "kW", "W", "power", 13.050847, -10, 30), + ("power", "kW", "kW", "kW", "power", 13.050847, 13.333333, -10, 30), ], ) def test_compile_hourly_statistics_changing_device_class_2( @@ -2331,6 +2340,7 @@ def test_compile_hourly_statistics_changing_device_class_2( statistic_unit, unit_class, mean, + mean2, min, max, ): @@ -2393,13 +2403,9 @@ def test_compile_hourly_statistics_changing_device_class_2( hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) - # Run statistics again, we get a warning, and no additional statistics is generated + # Run statistics again, additional statistics is generated do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) wait_recording_done(hass) - assert ( - f"The unit of sensor.test1 ({state_unit}) does not match the " - f"unit of already compiled statistics ({statistic_unit})" in caplog.text - ) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { @@ -2425,7 +2431,20 @@ def test_compile_hourly_statistics_changing_device_class_2( "last_reset": None, "state": None, "sum": None, - } + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat( + zero + timedelta(minutes=10) + ), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=15)), + "mean": approx(mean2), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + }, ] } assert "Error while processing event StatisticsTask" not in caplog.text @@ -3120,13 +3139,13 @@ async def test_validate_statistics_supported_device_class( @pytest.mark.parametrize( - "units, attributes, unit", + "units, attributes, valid_units", [ - (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W, kW"), ], ) async def test_validate_statistics_supported_device_class_2( - hass, hass_ws_client, recorder_mock, units, attributes, unit + hass, hass_ws_client, recorder_mock, units, attributes, valid_units ): """Test validate_statistics.""" id = 1 @@ -3172,7 +3191,7 @@ async def test_validate_statistics_supported_device_class_2( "device_class": attributes["device_class"], "metadata_unit": None, "statistic_id": "sensor.test", - "supported_unit": unit, + "supported_unit": valid_units, }, "type": "unsupported_unit_metadata", } @@ -3192,7 +3211,7 @@ async def test_validate_statistics_supported_device_class_2( "device_class": attributes["device_class"], "metadata_unit": None, "statistic_id": "sensor.test", - "supported_unit": unit, + "supported_unit": valid_units, }, "type": "unsupported_unit_metadata", }, @@ -3209,96 +3228,6 @@ async def test_validate_statistics_supported_device_class_2( await assert_validation_result(client, expected) -@pytest.mark.parametrize( - "units, attributes, unit", - [ - (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), - ], -) -async def test_validate_statistics_supported_device_class_3( - hass, hass_ws_client, recorder_mock, units, attributes, unit -): - """Test validate_statistics.""" - id = 1 - - def next_id(): - nonlocal id - id += 1 - return id - - async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == expected_result - - now = dt_util.utcnow() - - hass.config.units = units - await async_setup_component(hass, "sensor", {}) - await async_recorder_block_till_done(hass) - client = await hass_ws_client() - - # No statistics, no state - empty response - await assert_validation_result(client, {}) - - # No statistics, valid state - empty response - initial_attributes = {"state_class": "measurement", "unit_of_measurement": "kW"} - hass.states.async_set("sensor.test", 10, attributes=initial_attributes) - await hass.async_block_till_done() - await assert_validation_result(client, {}) - - # Statistics has run, device class set - expect error - do_adhoc_statistics(hass, start=now) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", 12, attributes=attributes) - await hass.async_block_till_done() - expected = { - "sensor.test": [ - { - "data": { - "device_class": attributes["device_class"], - "metadata_unit": "kW", - "statistic_id": "sensor.test", - "supported_unit": unit, - }, - "type": "unsupported_unit_metadata_can_convert", - } - ], - } - await assert_validation_result(client, expected) - - # Invalid state too, expect double errors - hass.states.async_set( - "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": "dogs"}} - ) - await async_recorder_block_till_done(hass) - expected = { - "sensor.test": [ - { - "data": { - "device_class": attributes["device_class"], - "metadata_unit": "kW", - "statistic_id": "sensor.test", - "supported_unit": unit, - }, - "type": "unsupported_unit_metadata_can_convert", - }, - { - "data": { - "device_class": attributes["device_class"], - "state_unit": "dogs", - "statistic_id": "sensor.test", - }, - "type": "unsupported_unit_state", - }, - ], - } - await assert_validation_result(client, expected) - - @pytest.mark.parametrize( "units, attributes, unit", [ From bbaac01da5b25542bf3cdd31bc5fd87ab7a5c15d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 3 Oct 2022 21:45:28 +0200 Subject: [PATCH 135/985] Update frontend to 20221003.0 (#79551) --- 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 3dbf73bdeaf..fde637657dd 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==20221002.0"], + "requirements": ["home-assistant-frontend==20221003.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a8b011a1a85..aeb65b379ba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.22.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20221002.0 +home-assistant-frontend==20221003.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 499c288a356..bbdf8f8f345 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,7 +868,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221002.0 +home-assistant-frontend==20221003.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 78129e17638..2baa78fb281 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,7 +648,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221002.0 +home-assistant-frontend==20221003.0 # homeassistant.components.home_connect homeconnect==0.7.2 From cfda36ef36272dbaf43f41386194ba5915adfabb Mon Sep 17 00:00:00 2001 From: mbo18 Date: Mon, 3 Oct 2022 22:12:30 +0200 Subject: [PATCH 136/985] Use device_class duration for NUT sensors (#79353) --- homeassistant/components/nut/const.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 64dc95d7b95..a8591349b56 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -90,7 +90,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="ups.delay.start", name="Load Restart Delay", native_unit_of_measurement=TIME_SECONDS, - icon="mdi:timer-outline", + device_class=SensorDeviceClass.DURATION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -98,7 +98,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="ups.delay.reboot", name="UPS Reboot Delay", native_unit_of_measurement=TIME_SECONDS, - icon="mdi:timer-outline", + device_class=SensorDeviceClass.DURATION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -106,7 +106,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="ups.delay.shutdown", name="UPS Shutdown Delay", native_unit_of_measurement=TIME_SECONDS, - icon="mdi:timer-outline", + device_class=SensorDeviceClass.DURATION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -114,7 +114,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="ups.timer.start", name="Load Start Timer", native_unit_of_measurement=TIME_SECONDS, - icon="mdi:timer-outline", + device_class=SensorDeviceClass.DURATION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -122,7 +122,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="ups.timer.reboot", name="Load Reboot Timer", native_unit_of_measurement=TIME_SECONDS, - icon="mdi:timer-outline", + device_class=SensorDeviceClass.DURATION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -130,7 +130,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="ups.timer.shutdown", name="Load Shutdown Timer", native_unit_of_measurement=TIME_SECONDS, - icon="mdi:timer-outline", + device_class=SensorDeviceClass.DURATION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -138,7 +138,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="ups.test.interval", name="Self-Test Interval", native_unit_of_measurement=TIME_SECONDS, - icon="mdi:timer-outline", + device_class=SensorDeviceClass.DURATION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -369,7 +369,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="battery.runtime", name="Battery Runtime", native_unit_of_measurement=TIME_SECONDS, - icon="mdi:timer-outline", + device_class=SensorDeviceClass.DURATION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -377,7 +377,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="battery.runtime.low", name="Low Battery Runtime", native_unit_of_measurement=TIME_SECONDS, - icon="mdi:timer-outline", + device_class=SensorDeviceClass.DURATION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -385,7 +385,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="battery.runtime.restart", name="Minimum Battery Runtime to Start", native_unit_of_measurement=TIME_SECONDS, - icon="mdi:timer-outline", + device_class=SensorDeviceClass.DURATION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), From 42de69b6d568bffa581a060d57b982197c6ff147 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 3 Oct 2022 23:21:53 +0200 Subject: [PATCH 137/985] Update mypy to 0.982 (#79560) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 9eccb9abb68..b09ba924fc5 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,7 +12,7 @@ codecov==2.1.12 coverage==6.4.4 freezegun==1.2.1 mock-open==1.4.0 -mypy==0.981 +mypy==0.982 pre-commit==2.20.0 pylint==2.15.0 pipdeptree==2.3.1 From d08f7f952653eeb494fc9e480aad323389c9a106 Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Tue, 4 Oct 2022 01:02:20 +0300 Subject: [PATCH 138/985] Add clickatell to strict typing (#79497) * type clickatell * follow review --- .strict-typing | 1 + homeassistant/components/clickatell/notify.py | 19 ++++++++++++++----- mypy.ini | 10 ++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.strict-typing b/.strict-typing index cc90af1b98b..303f732018c 100644 --- a/.strict-typing +++ b/.strict-typing @@ -77,6 +77,7 @@ homeassistant.components.calendar.* homeassistant.components.camera.* homeassistant.components.canary.* homeassistant.components.cover.* +homeassistant.components.clickatell.* homeassistant.components.cpuspeed.* homeassistant.components.crownstone.* homeassistant.components.deconz.* diff --git a/homeassistant/components/clickatell/notify.py b/homeassistant/components/clickatell/notify.py index fdefb25aef4..8422f7295b3 100644 --- a/homeassistant/components/clickatell/notify.py +++ b/homeassistant/components/clickatell/notify.py @@ -1,13 +1,18 @@ """Clickatell platform for notify component.""" +from __future__ import annotations + from http import HTTPStatus import logging +from typing import Any import requests import voluptuous as vol from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_API_KEY, CONF_RECIPIENT +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -20,7 +25,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def get_service(hass, config, discovery_info=None): +def get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> ClickatellNotificationService: """Get the Clickatell notification service.""" return ClickatellNotificationService(config) @@ -28,12 +37,12 @@ def get_service(hass, config, discovery_info=None): class ClickatellNotificationService(BaseNotificationService): """Implementation of a notification service for the Clickatell service.""" - def __init__(self, config): + def __init__(self, config: ConfigType) -> None: """Initialize the service.""" - self.api_key = config[CONF_API_KEY] - self.recipient = config[CONF_RECIPIENT] + self.api_key: str = config[CONF_API_KEY] + self.recipient: str = config[CONF_RECIPIENT] - def send_message(self, message="", **kwargs): + def send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message to a user.""" data = {"apiKey": self.api_key, "to": self.recipient, "content": message} diff --git a/mypy.ini b/mypy.ini index 04986db451c..4aa0b5cbeb3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -522,6 +522,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.clickatell.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.cpuspeed.*] check_untyped_defs = true disallow_incomplete_defs = true From 27effc93ad4b3c11a215197878ae9262a1f3dcf8 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 4 Oct 2022 00:45:31 +0200 Subject: [PATCH 139/985] Netatmo bump pyatmo to 7.1.0 (#79562) Bump pyatmo to 7.1.0 --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index b198c43bb39..4095762c666 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -2,7 +2,7 @@ "domain": "netatmo", "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==7.0.1"], + "requirements": ["pyatmo==7.1.0"], "after_dependencies": ["cloud", "media_source"], "dependencies": ["application_credentials", "webhook"], "codeowners": ["@cgtobi"], diff --git a/requirements_all.txt b/requirements_all.txt index bbdf8f8f345..e1a53d15462 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1439,7 +1439,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.0.1 +pyatmo==7.1.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2baa78fb281..949d8ee0ca6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1027,7 +1027,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.0.1 +pyatmo==7.1.0 # homeassistant.components.apple_tv pyatv==0.10.3 From 7eb101b0c7c0c551e60c4b92d4ffb48c6667c51e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 4 Oct 2022 00:37:13 +0000 Subject: [PATCH 140/985] [ci skip] Translation update --- .../components/adguard/translations/bg.json | 1 + .../binary_sensor/translations/bg.json | 4 ++++ .../components/braviatv/translations/bg.json | 8 ++++++- .../components/braviatv/translations/cs.json | 3 +++ .../components/braviatv/translations/de.json | 11 +++++++++- .../components/braviatv/translations/es.json | 11 +++++++++- .../components/braviatv/translations/et.json | 11 +++++++++- .../components/braviatv/translations/hu.json | 11 +++++++++- .../components/braviatv/translations/ru.json | 11 +++++++++- .../braviatv/translations/zh-Hant.json | 11 +++++++++- .../buienradar/translations/bg.json | 18 ++++++++++++++++ .../coronavirus/translations/bg.json | 7 +++++++ .../components/deconz/translations/bg.json | 4 ++++ .../devolo_home_control/translations/bg.json | 7 +++++++ .../components/dlna_dms/translations/bg.json | 3 ++- .../components/emonitor/translations/bg.json | 19 ++++++++++++++++- .../enphase_envoy/translations/bg.json | 8 ++++++- .../components/epson/translations/bg.json | 12 +++++++++++ .../components/esphome/translations/hu.json | 6 +++--- .../components/ezviz/translations/bg.json | 21 ++++++++++++++++++- .../components/flume/translations/bg.json | 6 +++++- .../forked_daapd/translations/ru.json | 10 ++++----- .../components/fritz/translations/bg.json | 14 +++++++++++++ .../google_travel_time/translations/bg.json | 13 ++++++++++++ .../components/group/translations/bg.json | 5 +++++ .../components/hive/translations/bg.json | 8 +++++++ .../home_plus_control/translations/bg.json | 17 +++++++++++++++ .../huisbaasje/translations/bg.json | 3 +++ .../kostal_plenticore/translations/bg.json | 12 +++++++++++ .../components/kraken/translations/bg.json | 5 +++++ .../components/lyric/translations/bg.json | 8 +++++++ .../components/met/translations/bg.json | 3 +++ .../met_eireann/translations/bg.json | 5 +++++ .../components/mikrotik/translations/bg.json | 10 ++++++++- .../components/motioneye/translations/bg.json | 8 ++++++- .../components/mutesync/translations/bg.json | 15 +++++++++++++ .../components/myq/translations/bg.json | 8 ++++++- .../components/nest/translations/bg.json | 3 +++ .../components/nest/translations/de.json | 2 +- .../components/nest/translations/en.json | 2 +- .../components/nest/translations/es.json | 2 +- .../components/nest/translations/et.json | 2 +- .../components/nest/translations/hu.json | 2 +- .../components/nest/translations/pt-BR.json | 2 +- .../components/nest/translations/ru.json | 2 +- .../components/nest/translations/zh-Hant.json | 2 +- .../components/nina/translations/bg.json | 4 ++++ .../components/nuki/translations/bg.json | 6 ++++++ .../components/octoprint/translations/bg.json | 7 +++++++ .../open_meteo/translations/bg.json | 3 +++ .../components/picnic/translations/bg.json | 5 ++++- .../components/qnap_qsw/translations/bg.json | 6 ++++++ .../components/roomba/translations/ru.json | 2 +- .../components/schedule/translations/bg.json | 6 ++++++ .../screenlogic/translations/bg.json | 4 ++++ .../sensibo/translations/sensor.bg.json | 8 +++++++ .../components/sensor/translations/cs.json | 10 +++++++-- .../components/shelly/translations/bg.json | 1 + .../components/shelly/translations/cs.json | 8 +++++++ .../simplepush/translations/bg.json | 17 +++++++++++++++ .../components/smarttub/translations/bg.json | 3 +++ .../soundtouch/translations/bg.json | 7 +++++++ .../components/sun/translations/bg.json | 5 +++++ .../system_bridge/translations/bg.json | 5 +++++ .../components/tautulli/translations/cs.json | 1 + .../components/vulcan/translations/bg.json | 5 +++++ .../waze_travel_time/translations/bg.json | 9 ++++++++ .../components/zha/translations/bg.json | 6 ++++++ 68 files changed, 449 insertions(+), 35 deletions(-) create mode 100644 homeassistant/components/buienradar/translations/bg.json create mode 100644 homeassistant/components/coronavirus/translations/bg.json create mode 100644 homeassistant/components/epson/translations/bg.json create mode 100644 homeassistant/components/home_plus_control/translations/bg.json create mode 100644 homeassistant/components/mutesync/translations/bg.json create mode 100644 homeassistant/components/sensibo/translations/sensor.bg.json create mode 100644 homeassistant/components/simplepush/translations/bg.json diff --git a/homeassistant/components/adguard/translations/bg.json b/homeassistant/components/adguard/translations/bg.json index 9838fd97c13..6ee3d4bd8fc 100644 --- a/homeassistant/components/adguard/translations/bg.json +++ b/homeassistant/components/adguard/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", "existing_instance_updated": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, "error": { diff --git a/homeassistant/components/binary_sensor/translations/bg.json b/homeassistant/components/binary_sensor/translations/bg.json index 621625cb457..603d64418cd 100644 --- a/homeassistant/components/binary_sensor/translations/bg.json +++ b/homeassistant/components/binary_sensor/translations/bg.json @@ -113,6 +113,10 @@ "off": "\u041d\u043e\u0440\u043c\u0430\u043b\u043d\u0430", "on": "\u0418\u0437\u0442\u043e\u0449\u0435\u043d\u0430" }, + "battery_charging": { + "off": "\u041d\u0435 \u0441\u0435 \u0437\u0430\u0440\u0435\u0436\u0434\u0430", + "on": "\u0417\u0430\u0440\u0435\u0436\u0434\u0430\u043d\u0435" + }, "cold": { "off": "\u041d\u043e\u0440\u043c\u0430\u043b\u043d\u043e", "on": "\u0421\u0442\u0443\u0434\u0435\u043d\u043e" diff --git a/homeassistant/components/braviatv/translations/bg.json b/homeassistant/components/braviatv/translations/bg.json index f43846e2283..08ab17032d4 100644 --- a/homeassistant/components/braviatv/translations/bg.json +++ b/homeassistant/components/braviatv/translations/bg.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "not_bravia_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Bravia." + "not_bravia_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Bravia.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", @@ -19,6 +20,11 @@ "confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?" }, + "reauth_confirm": { + "data": { + "pin": "\u041f\u0418\u041d \u043a\u043e\u0434" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/braviatv/translations/cs.json b/homeassistant/components/braviatv/translations/cs.json index f08c5d82861..59eed7f4187 100644 --- a/homeassistant/components/braviatv/translations/cs.json +++ b/homeassistant/components/braviatv/translations/cs.json @@ -16,6 +16,9 @@ "description": "Zadejte PIN k\u00f3d zobrazen\u00fd na televizi Sony Bravia.\n\nPokud se PIN k\u00f3d nezobraz\u00ed, je t\u0159eba zru\u0161it registraci Home Assistant na televizi, p\u0159ejd\u011bte na: Nastaven\u00ed -> S\u00ed\u0165 -> Nastaven\u00ed vzd\u00e1len\u00e9ho za\u0159\u00edzen\u00ed -> Zru\u0161it registraci vzd\u00e1len\u00e9ho za\u0159\u00edzen\u00ed.", "title": "Autorizujte televizi Sony Bravia" }, + "confirm": { + "description": "Chcete za\u010d\u00edt nastavovat?" + }, "user": { "data": { "host": "Hostitel" diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json index 18863481bc0..f62d496f2d3 100644 --- a/homeassistant/components/braviatv/translations/de.json +++ b/homeassistant/components/braviatv/translations/de.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "no_ip_control": "IP-Steuerung ist auf deinen Fernseher deaktiviert oder der Fernseher wird nicht unterst\u00fctzt.", - "not_bravia_device": "Das Ger\u00e4t ist kein Bravia-Fernseher." + "not_bravia_device": "Das Ger\u00e4t ist kein Bravia-Fernseher.", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "reauth_unsuccessful": "Die erneute Authentifizierung war nicht erfolgreich. Bitte entferne die Integration und richte sie erneut ein." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -23,6 +25,13 @@ "confirm": { "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" }, + "reauth_confirm": { + "data": { + "pin": "PIN-Code", + "use_psk": "PSK-Authentifizierung verwenden" + }, + "description": "Gib den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, musst du die Registrierung von Home Assistant auf Ihrem Fernseher aufheben, gehe zu: Einstellungen - > Netzwerk - > Remote-Ger\u00e4teeinstellungen - > Remote-Ger\u00e4t abmelden. \n\nDu kannst PSK (Pre-Shared-Key) anstelle der PIN verwenden. PSK ist ein benutzerdefinierter geheimer Schl\u00fcssel, der f\u00fcr die Zugriffskontrolle verwendet wird. Diese Authentifizierungsmethode wird als stabiler empfohlen. Um PSK auf deinem Fernseher zu aktivieren, gehe zu: Einstellungen - > Netzwerk - > Heimnetzwerk-Setup - > IP-Steuerung. Aktiviere dann das Kontrollk\u00e4stchen \u00abPSK-Authentifizierung verwenden\u00bb und gib deinen PSK anstelle der PIN ein." + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/es.json b/homeassistant/components/braviatv/translations/es.json index 325ccb4c535..fbcd69a0bec 100644 --- a/homeassistant/components/braviatv/translations/es.json +++ b/homeassistant/components/braviatv/translations/es.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "no_ip_control": "El Control IP est\u00e1 desactivado en tu TV o la TV no es compatible.", - "not_bravia_device": "El dispositivo no es una TV Bravia." + "not_bravia_device": "El dispositivo no es una TV Bravia.", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", + "reauth_unsuccessful": "No se pudo volver a autenticar, por favor, elimina la integraci\u00f3n y vuelve a configurarla." }, "error": { "cannot_connect": "No se pudo conectar", @@ -23,6 +25,13 @@ "confirm": { "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" }, + "reauth_confirm": { + "data": { + "pin": "C\u00f3digo PIN", + "use_psk": "Usar autenticaci\u00f3n PSK" + }, + "description": "Introduce el c\u00f3digo PIN que se muestra en la TV Sony Bravia. \n\nSi no se muestra el c\u00f3digo PIN, debes cancelar el registro de Home Assistant en tu TV, ve a: Configuraci\u00f3n -> Red -> Configuraci\u00f3n del dispositivo remoto -> Cancelar el registro del dispositivo remoto. \n\nPuedes usar PSK (clave precompartida) en lugar de PIN. PSK es una clave secreta definida por el usuario que se utiliza para el control de acceso. Este m\u00e9todo de autenticaci\u00f3n se recomienda como m\u00e1s estable. Para habilitar PSK en tu TV, ve a: Configuraci\u00f3n -> Red -> Configuraci\u00f3n de red dom\u00e9stica -> Control de IP. Luego marca la casilla \u00abUsar autenticaci\u00f3n PSK\u00bb e introduce tu PSK en lugar de PIN." + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/et.json b/homeassistant/components/braviatv/translations/et.json index b2863001eba..d057ea37151 100644 --- a/homeassistant/components/braviatv/translations/et.json +++ b/homeassistant/components/braviatv/translations/et.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "no_ip_control": "Teleris on IP-juhtimine keelatud v\u00f5i telerit ei toetata.", - "not_bravia_device": "Seade ei ole Bravia teler." + "not_bravia_device": "Seade ei ole Bravia teler.", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "reauth_unsuccessful": "Taasautentimine eba\u00f5nnestus, eemalda sidumine ja seadista see uuesti." }, "error": { "cannot_connect": "\u00dchendamine nurjus", @@ -23,6 +25,13 @@ "confirm": { "description": "Kas alustada seadistamist?" }, + "reauth_confirm": { + "data": { + "pin": "PIN kood", + "use_psk": "PSK autentimise kasutamine" + }, + "description": "Sisestage Sony Bravia teleril n\u00e4idatud PIN-kood. \n\nKui PIN-koodi ei kuvata, peate teleril Home Assistant'i registreerimise t\u00fchistama, minge aadressile: Seaded -> Network -> Remote device settings -> Deregister remote device. \n\nPIN-koodi asemel v\u00f5ite kasutada PSK (Pre-Shared-Key). PSK on kasutaja m\u00e4\u00e4ratud salajane v\u00f5ti, mida kasutatakse juurdep\u00e4\u00e4su kontrollimiseks. See autentimismeetod on soovitatav kui stabiilsem. PSK lubamiseks teleril minge aadressil: Settings -> Network -> Home Network Setup -> IP Control. Seej\u00e4rel m\u00e4rgistage ruut \"Kasutage PSK autentimist\" ja sisestage PIN-koodi asemel PSK." + }, "user": { "data": { "host": "" diff --git a/homeassistant/components/braviatv/translations/hu.json b/homeassistant/components/braviatv/translations/hu.json index 02b64050ee2..0912003f74f 100644 --- a/homeassistant/components/braviatv/translations/hu.json +++ b/homeassistant/components/braviatv/translations/hu.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "no_ip_control": "Az IP-vez\u00e9rl\u00e9s le van tiltva a TV-n, vagy a TV nem t\u00e1mogatja.", - "not_bravia_device": "A k\u00e9sz\u00fcl\u00e9k nem egy Bravia TV." + "not_bravia_device": "A k\u00e9sz\u00fcl\u00e9k nem egy Bravia TV.", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "reauth_unsuccessful": "Az \u00fajrahiteles\u00edt\u00e9s sikertelen volt, k\u00e9rem, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -23,6 +25,13 @@ "confirm": { "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" }, + "reauth_confirm": { + "data": { + "pin": "PIN-k\u00f3d", + "use_psk": "PSK hiteles\u00edt\u00e9s haszn\u00e1lata" + }, + "description": "\u00cdrja be a Sony Bravia TV -n l\u00e1that\u00f3 PIN -k\u00f3dot. \n\nHa a PIN -k\u00f3d nem jelenik meg, t\u00f6r\u00f6lje a Home Assistant regisztr\u00e1ci\u00f3j\u00e1t a t\u00e9v\u00e9n, az al\u00e1bbiak szerint: Be\u00e1ll\u00edt\u00e1sok - > H\u00e1l\u00f3zat - > T\u00e1voli eszk\u00f6z be\u00e1ll\u00edt\u00e1sai - > T\u00e1vol\u00edtsa el a t\u00e1voli eszk\u00f6z regisztr\u00e1ci\u00f3j\u00e1t.\n\nA PIN-k\u00f3d helyett haszn\u00e1lhat PSK-t (Pre-Shared-Key). A PSK egy felhaszn\u00e1l\u00f3 \u00e1ltal meghat\u00e1rozott titkos kulcs, amelyet a hozz\u00e1f\u00e9r\u00e9s ellen\u0151rz\u00e9s\u00e9re haszn\u00e1lnak. Ez a hiteles\u00edt\u00e9si m\u00f3dszer aj\u00e1nlott, mivel stabilabb. A PSK enged\u00e9lyez\u00e9s\u00e9hez a TV-n, l\u00e9pjen a k\u00f6vetkez\u0151 oldalra: Be\u00e1ll\u00edt\u00e1sok -> H\u00e1l\u00f3zat -> Otthoni h\u00e1l\u00f3zat be\u00e1ll\u00edt\u00e1sa -> IP-vez\u00e9rl\u00e9s. Ezut\u00e1n jel\u00f6lje be a \"PSK hiteles\u00edt\u00e9s haszn\u00e1lata\" jel\u00f6l\u0151n\u00e9gyzetet, \u00e9s adja meg a PSK-t a PIN-k\u00f3d helyett." + }, "user": { "data": { "host": "C\u00edm" diff --git a/homeassistant/components/braviatv/translations/ru.json b/homeassistant/components/braviatv/translations/ru.json index f191e3607cc..8416d9e5ede 100644 --- a/homeassistant/components/braviatv/translations/ru.json +++ b/homeassistant/components/braviatv/translations/ru.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "no_ip_control": "\u041d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u043e IP, \u043b\u0438\u0431\u043e \u044d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", - "not_bravia_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Bravia." + "not_bravia_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Bravia.", + "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.", + "reauth_unsuccessful": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0439\u0442\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0443\u044e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0435\u0451 \u0441\u043d\u043e\u0432\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.", @@ -23,6 +25,13 @@ "confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" }, + "reauth_confirm": { + "data": { + "pin": "PIN-\u043a\u043e\u0434", + "use_psk": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c PSK-\u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u044e\u0449\u0438\u0439\u0441\u044f \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 Sony Bravia. \n\n\u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0435 \u0432\u0438\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e Home Assistant \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 -> \u0421\u0435\u0442\u044c -> \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 -> \u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c PSK (Pre-Shared-Key) \u0432\u043c\u0435\u0441\u0442\u043e PIN-\u043a\u043e\u0434\u0430. PSK \u2014 \u044d\u0442\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u043c \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043b\u044e\u0447, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0434\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u043e\u043c. \u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u0431\u043e\u043b\u0435\u0435 \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u0439. \u0427\u0442\u043e\u0431\u044b \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c PSK \u043d\u0430 \u0412\u0430\u0448\u0435\u043c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 - > \u0421\u0435\u0442\u044c - > \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u043c\u0430\u0448\u043d\u0435\u0439 \u0441\u0435\u0442\u0438 - > \u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 IP. \u0417\u0430\u0442\u0435\u043c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 \u0444\u043b\u0430\u0436\u043e\u043a \u00ab\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e PSK\u00bb \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 PSK \u0432\u043c\u0435\u0441\u0442\u043e PIN-\u043a\u043e\u0434\u0430." + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/braviatv/translations/zh-Hant.json b/homeassistant/components/braviatv/translations/zh-Hant.json index 9753715eec1..e30142c947b 100644 --- a/homeassistant/components/braviatv/translations/zh-Hant.json +++ b/homeassistant/components/braviatv/translations/zh-Hant.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_ip_control": "\u96fb\u8996\u4e0a\u7684 IP \u5df2\u95dc\u9589\u6216\u4e0d\u652f\u63f4\u6b64\u6b3e\u96fb\u8996\u3002", - "not_bravia_device": "\u88dd\u7f6e\u4e26\u975e Bravia TV\u3002" + "not_bravia_device": "\u88dd\u7f6e\u4e26\u975e Bravia TV\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "reauth_unsuccessful": "\u91cd\u65b0\u9a57\u8b49\u5931\u6557\uff0c\u8acb\u79fb\u9664\u88dd\u7f6e\u4e26\u91cd\u65b0\u8a2d\u5b9a\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -23,6 +25,13 @@ "confirm": { "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" }, + "reauth_confirm": { + "data": { + "pin": "PIN \u78bc", + "use_psk": "\u4f7f\u7528 PSK \u9a57\u8b49" + }, + "description": "\u8f38\u5165 Sony Bravia \u96fb\u8996\u6240\u986f\u793a\u4e4b PIN \u78bc\u3002\n\n\u5047\u5982 PIN \u78bc\u672a\u986f\u793a\uff0c\u5fc5\u9808\u5148\u65bc\u96fb\u8996\u89e3\u9664 Home Assistant \u8a3b\u518a\uff0c\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u9060\u7aef\u88dd\u7f6e\u8a2d\u5b9a -> \u89e3\u9664\u9060\u7aef\u88dd\u7f6e\u8a3b\u518a\u3002\n\n\u53ef\u4f7f\u7528 PSK (Pre-Shared-Key) \u53d6\u4ee3 PIN \u78bc\u3002PSK \u70ba\u4f7f\u7528\u8005\u81ea\u5b9a\u5bc6\u9470\u7528\u4ee5\u5b58\u53d6\u63a7\u5236\u3002\u5efa\u8b70\u63a1\u7528\u6b64\u8a8d\u8b49\u65b9\u5f0f\u66f4\u70ba\u7a69\u5b9a\u3002\u6b32\u65bc\u96fb\u8996\u555f\u7528 PSK\u3002\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u5bb6\u5ead\u7db2\u8def\u8a2d\u5b9a -> IP \u63a7\u5236\u3002\u7136\u5f8c\u52fe\u9078 \u00ab\u4f7f\u7528 PSK \u8a8d\u8b49\u00bb \u4e26\u8f38\u5165 PSK \u78bc\u3002" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef" diff --git a/homeassistant/components/buienradar/translations/bg.json b/homeassistant/components/buienradar/translations/bg.json new file mode 100644 index 00000000000..ca1a967a7ea --- /dev/null +++ b/homeassistant/components/buienradar/translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "step": { + "user": { + "data": { + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/translations/bg.json b/homeassistant/components/coronavirus/translations/bg.json new file mode 100644 index 00000000000..c30e629d8ad --- /dev/null +++ b/homeassistant/components/coronavirus/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/bg.json b/homeassistant/components/deconz/translations/bg.json index f8b1e351fb0..b047c361681 100644 --- a/homeassistant/components/deconz/translations/bg.json +++ b/homeassistant/components/deconz/translations/bg.json @@ -35,6 +35,10 @@ "button_2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "button_4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_5": "\u041f\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_6": "\u0428\u0435\u0441\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_7": "\u0421\u0435\u0434\u043c\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_8": "\u041e\u0441\u043c\u0438 \u0431\u0443\u0442\u043e\u043d", "close": "\u0417\u0430\u0442\u0432\u0430\u0440\u044f\u043d\u0435", "dim_down": "\u0417\u0430\u0442\u044a\u043c\u043d\u044f\u0432\u0430\u043d\u0435", "dim_up": "\u041e\u0441\u0432\u0435\u0442\u044f\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/devolo_home_control/translations/bg.json b/homeassistant/components/devolo_home_control/translations/bg.json index d5f922c14ff..a22746a1dd4 100644 --- a/homeassistant/components/devolo_home_control/translations/bg.json +++ b/homeassistant/components/devolo_home_control/translations/bg.json @@ -9,6 +9,13 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo URL", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u0418\u043c\u0435\u0439\u043b / devolo ID" + } } } } diff --git a/homeassistant/components/dlna_dms/translations/bg.json b/homeassistant/components/dlna_dms/translations/bg.json index da5fbf4c01c..1e65e7f6ae8 100644 --- a/homeassistant/components/dlna_dms/translations/bg.json +++ b/homeassistant/components/dlna_dms/translations/bg.json @@ -12,7 +12,8 @@ "user": { "data": { "host": "\u0425\u043e\u0441\u0442" - } + }, + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0437\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435" } } } diff --git a/homeassistant/components/emonitor/translations/bg.json b/homeassistant/components/emonitor/translations/bg.json index e8940bef26a..6290f483074 100644 --- a/homeassistant/components/emonitor/translations/bg.json +++ b/homeassistant/components/emonitor/translations/bg.json @@ -1,5 +1,22 @@ { "config": { - "flow_title": "{name}" + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name} ({host})?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/bg.json b/homeassistant/components/enphase_envoy/translations/bg.json index 1fce5cf396e..7d794942093 100644 --- a/homeassistant/components/enphase_envoy/translations/bg.json +++ b/homeassistant/components/enphase_envoy/translations/bg.json @@ -1,15 +1,21 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, + "flow_title": "{serial} ({host})", "step": { "user": { "data": { - "host": "\u0425\u043e\u0441\u0442" + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } } diff --git a/homeassistant/components/epson/translations/bg.json b/homeassistant/components/epson/translations/bg.json new file mode 100644 index 00000000000..a051d6ca487 --- /dev/null +++ b/homeassistant/components/epson/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/hu.json b/homeassistant/components/esphome/translations/hu.json index 1f98953678c..41550d02a43 100644 --- a/homeassistant/components/esphome/translations/hu.json +++ b/homeassistant/components/esphome/translations/hu.json @@ -20,8 +20,8 @@ "description": "K\u00e9rem, adja meg a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151, {name} jelszav\u00e1t." }, "discovery_confirm": { - "description": "Szeretn\u00e9 hozz\u00e1adni `{name}` ESPHome csom\u00f3pontot Home Assistanthoz?", - "title": "ESPHome csom\u00f3pont felfedezve" + "description": "Szeretn\u00e9 hozz\u00e1adni a `{name}` ESPHome v\u00e9gpontot Home Assistanthoz?", + "title": "ESPHome v\u00e9gpont felfedezve" }, "encryption_key": { "data": { @@ -40,7 +40,7 @@ "host": "C\u00edm", "port": "Port" }, - "description": "K\u00e9rem, adja meg az [ESPHome]({esphome_url}) csom\u00f3pontj\u00e1nak kapcsol\u00f3d\u00e1si be\u00e1ll\u00edt\u00e1sait." + "description": "K\u00e9rem, adja meg az [ESPHome]({esphome_url}) v\u00e9gpontj\u00e1nak kapcsol\u00f3d\u00e1si be\u00e1ll\u00edt\u00e1sait." } } } diff --git a/homeassistant/components/ezviz/translations/bg.json b/homeassistant/components/ezviz/translations/bg.json index 7e54efd88a9..d380e383fcf 100644 --- a/homeassistant/components/ezviz/translations/bg.json +++ b/homeassistant/components/ezviz/translations/bg.json @@ -1,17 +1,36 @@ { "config": { + "abort": { + "already_configured_account": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "invalid_host": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441" }, "flow_title": "{serial}", "step": { "confirm": { "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } }, "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "url": "URL", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "url": "URL", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } } } } diff --git a/homeassistant/components/flume/translations/bg.json b/homeassistant/components/flume/translations/bg.json index 14aa8f088f3..1ceb53d2be7 100644 --- a/homeassistant/components/flume/translations/bg.json +++ b/homeassistant/components/flume/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", @@ -10,6 +11,9 @@ }, "step": { "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, "description": "\u041f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0437\u0430 {username} \u0432\u0435\u0447\u0435 \u043d\u0435 \u0435 \u0432\u0430\u043b\u0438\u0434\u043d\u0430." }, "user": { diff --git a/homeassistant/components/forked_daapd/translations/ru.json b/homeassistant/components/forked_daapd/translations/ru.json index ba8946ed112..00ab1d3f635 100644 --- a/homeassistant/components/forked_daapd/translations/ru.json +++ b/homeassistant/components/forked_daapd/translations/ru.json @@ -5,12 +5,12 @@ "not_forked_daapd": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 Owntone." }, "error": { - "forbidden": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u044f forked-daapd.", + "forbidden": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u044f Owntone.", "unknown_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", "websocket_not_enabled": "\u0412\u0435\u0431-\u0441\u043e\u043a\u0435\u0442 Owntone \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d.", "wrong_host_or_port": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430.", "wrong_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.", - "wrong_server_type": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0432\u0435\u0440 forked-daapd \u0432\u0435\u0440\u0441\u0438\u0438 27.0 \u0438\u043b\u0438 \u0432\u044b\u0448\u0435." + "wrong_server_type": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0432\u0435\u0440 Owntone \u0432\u0435\u0440\u0441\u0438\u0438 27.0 \u0438\u043b\u0438 \u0432\u044b\u0448\u0435." }, "flow_title": "{name} ({host})", "step": { @@ -21,7 +21,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c API (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0435\u0441\u043b\u0438 \u043d\u0435\u0442 \u043f\u0430\u0440\u043e\u043b\u044f)", "port": "\u041f\u043e\u0440\u0442 API" }, - "title": "forked-daapd" + "title": "Owntone" } } }, @@ -34,8 +34,8 @@ "tts_pause_time": "\u0412\u0440\u0435\u043c\u044f \u043f\u0430\u0443\u0437\u044b \u0434\u043e \u0438 \u043f\u043e\u0441\u043b\u0435 TTS (\u0441\u0435\u043a.)", "tts_volume": "\u0413\u0440\u043e\u043c\u043a\u043e\u0441\u0442\u044c TTS (\u0447\u0438\u0441\u043b\u043e \u0432 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u0435 \u043e\u0442 0 \u0434\u043e 1)" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 forked-daapd.", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 forked-daapd" + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Owntone.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Owntone" } } } diff --git a/homeassistant/components/fritz/translations/bg.json b/homeassistant/components/fritz/translations/bg.json index e9162b8b35b..7341a275d46 100644 --- a/homeassistant/components/fritz/translations/bg.json +++ b/homeassistant/components/fritz/translations/bg.json @@ -1,14 +1,28 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "flow_title": "{name}", "step": { + "confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/google_travel_time/translations/bg.json b/homeassistant/components/google_travel_time/translations/bg.json index 215f8c00629..2530cf7a4ce 100644 --- a/homeassistant/components/google_travel_time/translations/bg.json +++ b/homeassistant/components/google_travel_time/translations/bg.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { @@ -11,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "language": "\u0415\u0437\u0438\u043a", + "units": "\u0415\u0434\u0438\u043d\u0438\u0446\u0438" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/group/translations/bg.json b/homeassistant/components/group/translations/bg.json index f39166c65d8..5d63f29aff0 100644 --- a/homeassistant/components/group/translations/bg.json +++ b/homeassistant/components/group/translations/bg.json @@ -3,12 +3,16 @@ "step": { "binary_sensor": { "data": { + "all": "\u0412\u0441\u0438\u0447\u043a\u0438 \u043e\u0431\u0435\u043a\u0442\u0438", "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435", "name": "\u0418\u043c\u0435" }, "title": "\u041d\u043e\u0432\u0430 \u0433\u0440\u0443\u043f\u0430" }, "cover": { + "data": { + "name": "\u0418\u043c\u0435" + }, "title": "\u0414\u043e\u0431\u0430\u0432\u044f\u043d\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0430" }, "fan": { @@ -68,6 +72,7 @@ }, "light": { "data": { + "all": "\u0412\u0441\u0438\u0447\u043a\u0438 \u043e\u0431\u0435\u043a\u0442\u0438", "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" } }, diff --git a/homeassistant/components/hive/translations/bg.json b/homeassistant/components/hive/translations/bg.json index 082fb940fca..c027da47da5 100644 --- a/homeassistant/components/hive/translations/bg.json +++ b/homeassistant/components/hive/translations/bg.json @@ -1,9 +1,17 @@ { "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, "error": { "no_internet_available": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u043e\u0441\u0442 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435\u0442\u0435 \u0441 Hive." }, "step": { + "configuration": { + "data": { + "device_name": "\u0418\u043c\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e" + } + }, "reauth": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" diff --git a/homeassistant/components/home_plus_control/translations/bg.json b/homeassistant/components/home_plus_control/translations/bg.json new file mode 100644 index 00000000000..e62469db0ec --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/bg.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430.", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "step": { + "pick_implementation": { + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u043d\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/bg.json b/homeassistant/components/huisbaasje/translations/bg.json index 67a484573aa..059e100270f 100644 --- a/homeassistant/components/huisbaasje/translations/bg.json +++ b/homeassistant/components/huisbaasje/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/kostal_plenticore/translations/bg.json b/homeassistant/components/kostal_plenticore/translations/bg.json index 23968d0a06a..e9dbb5a0a7d 100644 --- a/homeassistant/components/kostal_plenticore/translations/bg.json +++ b/homeassistant/components/kostal_plenticore/translations/bg.json @@ -1,8 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/bg.json b/homeassistant/components/kraken/translations/bg.json index 3ac8e39cb8d..7357576b244 100644 --- a/homeassistant/components/kraken/translations/bg.json +++ b/homeassistant/components/kraken/translations/bg.json @@ -2,6 +2,11 @@ "config": { "abort": { "already_configured": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "step": { + "user": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/bg.json b/homeassistant/components/lyric/translations/bg.json index 2f756377e31..5d9459cac2c 100644 --- a/homeassistant/components/lyric/translations/bg.json +++ b/homeassistant/components/lyric/translations/bg.json @@ -1,7 +1,15 @@ { "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, "create_entry": { "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "step": { + "reauth_confirm": { + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + } } } } \ No newline at end of file diff --git a/homeassistant/components/met/translations/bg.json b/homeassistant/components/met/translations/bg.json index ee2071403c4..cf70857a415 100644 --- a/homeassistant/components/met/translations/bg.json +++ b/homeassistant/components/met/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "\u0412 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Home Assistant \u043d\u0435 \u0441\u0430 \u0437\u0430\u0434\u0430\u0434\u0435\u043d\u0438 \u0434\u043e\u043c\u0430\u0448\u043d\u0438 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0438" + }, "error": { "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" }, diff --git a/homeassistant/components/met_eireann/translations/bg.json b/homeassistant/components/met_eireann/translations/bg.json index 2c39cd06b7d..9f826873b7b 100644 --- a/homeassistant/components/met_eireann/translations/bg.json +++ b/homeassistant/components/met_eireann/translations/bg.json @@ -1,8 +1,13 @@ { "config": { + "error": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, "step": { "user": { "data": { + "elevation": "\u041d\u0430\u0434\u043c\u043e\u0440\u0441\u043a\u0430 \u0432\u0438\u0441\u043e\u0447\u0438\u043d\u0430", + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430", "name": "\u0418\u043c\u0435" } diff --git a/homeassistant/components/mikrotik/translations/bg.json b/homeassistant/components/mikrotik/translations/bg.json index d81e97f2d68..3316c8f5a6c 100644 --- a/homeassistant/components/mikrotik/translations/bg.json +++ b/homeassistant/components/mikrotik/translations/bg.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "name_exists": "\u0418\u043c\u0435\u0442\u043e \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u041f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0437\u0430 {username} \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/motioneye/translations/bg.json b/homeassistant/components/motioneye/translations/bg.json index d2db5257b51..f5716dcf951 100644 --- a/homeassistant/components/motioneye/translations/bg.json +++ b/homeassistant/components/motioneye/translations/bg.json @@ -1,8 +1,14 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", - "invalid_url": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d URL" + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "invalid_url": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d URL", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { "user": { diff --git a/homeassistant/components/mutesync/translations/bg.json b/homeassistant/components/mutesync/translations/bg.json new file mode 100644 index 00000000000..dcdcdcfc186 --- /dev/null +++ b/homeassistant/components/mutesync/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/bg.json b/homeassistant/components/myq/translations/bg.json index 9c1d3ecccb8..728682f531e 100644 --- a/homeassistant/components/myq/translations/bg.json +++ b/homeassistant/components/myq/translations/bg.json @@ -1,12 +1,18 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + }, "user": { "data": { "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" diff --git a/homeassistant/components/nest/translations/bg.json b/homeassistant/components/nest/translations/bg.json index 41f20ca4a5f..ed5adaef5e6 100644 --- a/homeassistant/components/nest/translations/bg.json +++ b/homeassistant/components/nest/translations/bg.json @@ -27,6 +27,9 @@ "description": "\u0417\u0430 \u0434\u0430 \u0441\u0432\u044a\u0440\u0436\u0435\u0442\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u0430 \u0441\u0438 \u0432 Nest, [\u043e\u0442\u043e\u0440\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u0430 \u0441\u0438]({url}). \n\n \u0421\u043b\u0435\u0434 \u043a\u0430\u0442\u043e \u0441\u0442\u0435 \u043e\u0442\u043e\u0440\u0438\u0437\u0438\u0440\u0430\u043d\u0435, \u043a\u043e\u043f\u0438\u0440\u0430\u0439\u0442\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0438\u044f \u043f\u043e-\u0434\u043e\u043b\u0443 PIN \u043a\u043e\u0434.", "title": "\u0421\u0432\u044a\u0440\u0436\u0435\u0442\u0435 \u0412\u0430\u0448\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b \u0432 Nest" }, + "pubsub": { + "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 Google Cloud" + }, "reauth_confirm": { "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" } diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index 18ecafda58e..85fe8acd16f 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -52,7 +52,7 @@ "data": { "project_id": "Ger\u00e4tezugriffsprojekt ID" }, - "description": "Erstelle ein Nest Ger\u00e4tezugriffsprojekt, f\u00fcr dessen Einrichtung **eine Geb\u00fchr von 5 US-Dollar** anf\u00e4llt.\n1. Gehe zur [Device Access Console]({device_access_console_url}) und durchlaufe den Zahlungsablauf.\n1. Dr\u00fccke auf **Projekt erstellen**.\n1. Gib deinem Device Access-Projekt einen Namen und dr\u00fccke auf **Weiter**.\n1. Gib deine OAuth-Client-ID ein\n1. Aktiviere Ereignisse, indem du auf **Aktivieren** und **Projekt erstellen** dr\u00fcckst.\n\nGib unten deine Ger\u00e4tezugriffsprojekt ID ein ([more info]({more_info_url})).", + "description": "Erstelle ein Nest Device Access-Projekt, f\u00fcr dessen Einrichtung **eine Geb\u00fchr von 5 US-Dollar an Google zu zahlen ist**.\n 1. Gehe zur [Ger\u00e4tezugriffskonsole] ( {device_access_console_url} ) und durch den Zahlungsablauf.\n 1. Klicke auf **Projekt erstellen**\n 1. Gib deinem Device Access-Projekt einen Namen und klicke auf **Weiter**.\n 1. Gib deine OAuth-Client-ID ein\n 1. Aktiviere Ereignisse, indem du auf **Aktivieren** und **Projekt erstellen** klickst. \n\n Gib unten deine Projekt-ID f\u00fcr den Ger\u00e4tezugriff ein ([weitere Informationen]( {more_info_url} )).\n", "title": "Nest: Erstelle ein Ger\u00e4tezugriffsprojekt" }, "device_project_upgrade": { diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index e0c0b8e67a5..07678227547 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -52,7 +52,7 @@ "data": { "project_id": "Device Access Project ID" }, - "description": "Create a Nest Device Access project which **requires a US $5 fee** to set up.\n1. Go to the [Device Access Console]({device_access_console_url}), and through the payment flow.\n1. Click on **Create project**\n1. Give your Device Access project a name and click **Next**.\n1. Enter your OAuth Client ID\n1. Enable events by clicking **Enable** and **Create project**.\n\nEnter your Device Access Project ID below ([more info]({more_info_url})).\n", + "description": "Create a Nest Device Access project which **requires paying Google a US $5 fee** to set up.\n1. Go to the [Device Access Console]({device_access_console_url}), and through the payment flow.\n1. Click on **Create project**\n1. Give your Device Access project a name and click **Next**.\n1. Enter your OAuth Client ID\n1. Enable events by clicking **Enable** and **Create project**.\n\nEnter your Device Access Project ID below ([more info]({more_info_url})).\n", "title": "Nest: Create a Device Access Project" }, "device_project_upgrade": { diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index 3cd92fd644d..95c8aa5ef04 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -52,7 +52,7 @@ "data": { "project_id": "ID de proyecto de acceso a dispositivos" }, - "description": "Crea un proyecto de acceso a dispositivos Nest que **requiere una cuota de 5$** para configurarlo.\n 1. Ve a la [Consola de acceso al dispositivo]({device_access_console_url}) y sigue el flujo de pago.\n 1. Haz clic en **Crear proyecto**\n 1. Asigna un nombre a tu proyecto de acceso a dispositivos y haz clic en **Siguiente**.\n 1. Introduce tu ID de cliente de OAuth\n 1. Habilita los eventos haciendo clic en **Habilitar** y **Crear proyecto**. \n\n Introduce tu ID de proyecto de acceso a dispositivos a continuaci\u00f3n ([m\u00e1s informaci\u00f3n]({more_info_url})).", + "description": "Crea un proyecto de acceso a dispositivos Nest que **requiere pagarle a Google una tarifa de 5$ US** para configurarlo.\n1. Ve a la [Consola de acceso al dispositivo]({device_access_console_url}) y sigue el flujo de pago.\n1. Haz clic en **Crear proyecto**\n1. Asigna un nombre a tu proyecto de Acceso al dispositivo y haz clic en **Siguiente**.\n1. Introduce tu ID de cliente de OAuth\n1. Habilita los eventos haciendo clic en **Habilitar** y **Crear proyecto**. \n\nIntroduce tu ID de proyecto de acceso a dispositivos a continuaci\u00f3n ([m\u00e1s informaci\u00f3n]({more_info_url})).\n", "title": "Nest: Crear un proyecto de acceso a dispositivos" }, "device_project_upgrade": { diff --git a/homeassistant/components/nest/translations/et.json b/homeassistant/components/nest/translations/et.json index b79c320a35b..655515e93f8 100644 --- a/homeassistant/components/nest/translations/et.json +++ b/homeassistant/components/nest/translations/et.json @@ -52,7 +52,7 @@ "data": { "project_id": "Seadme juurdep\u00e4\u00e4su projekti ID" }, - "description": "Loo Nest Device Accessi projekt, mille seadistamiseks on vaja 5 USA dollari suurust tasu**.\n1. Ava [Seadme juurdep\u00e4\u00e4sukonsool]({device_access_console_url}) ja maksevoo kaudu.\n1. Vajuta **Loo projekt**\n1. Anna oma seadmele juurdep\u00e4\u00e4su projektile nimi ja kl\u00f5psa nuppu **Next**.\n1. Sisesta oma OAuth Kliendi ID\n1. Luba s\u00fcndmused, kl\u00f5psates nuppu **Luba** ja **Loo projekt**.\n\nSisesta allpool seadme accessi projekti ID ([lisateave]({more_info_url})).\n", + "description": "Loo Nest Device Accessi projekt, mille seadistamiseks on vaja 5 USA dollari suurust tasu**.\n1. Ava [Seadme juurdep\u00e4\u00e4sukonsool]({device_access_console_url}) ja maksevoo kaudu.\n1. Vajuta **Loo projekt**\n1. Anna oma seadmele juurdep\u00e4\u00e4su projektile nimi ja kl\u00f5psa nuppu **Next**.\n1. Sisesta oma OAuth Kliendi ID\n1. Luba s\u00fcndmused, kl\u00f5psates nuppu **Luba** ja **Loo projekt**.\n\nSisesta allpool seadme accessi projekti ID ([lisateave]({more_info_url})).", "title": "Nest: seadmele juurdep\u00e4\u00e4su projekti loomine" }, "device_project_upgrade": { diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json index f453fdef150..6f868eb7aab 100644 --- a/homeassistant/components/nest/translations/hu.json +++ b/homeassistant/components/nest/translations/hu.json @@ -52,7 +52,7 @@ "data": { "project_id": "Eszk\u00f6z-hozz\u00e1f\u00e9r\u00e9s Projekt azonos\u00edt\u00f3" }, - "description": "Hozzon l\u00e9tre egy Nest Device Access projektet, amelynek **be\u00e1ll\u00edt\u00e1sa 5 USD d\u00edjat** ig\u00e9nyel.\n1. Menjen a [Device Access Console]({device_access_console_url}) oldalra, \u00e9s a fizet\u00e9si folyamaton kereszt\u00fcl.\n1. Kattintson a **Projekt l\u00e9trehoz\u00e1sa** gombra.\n1. Adjon nevet a Device Access projektnek, \u00e9s kattintson a **K\u00f6vetkez\u0151** gombra.\n1. Adja meg az OAuth \u00fcgyf\u00e9l azonos\u00edt\u00f3j\u00e1t\n1. Enged\u00e9lyezze az esem\u00e9nyeket a **Enable** \u00e9s a **Create project** gombra kattintva.\n\nAdja meg a Device Access projekt azonos\u00edt\u00f3j\u00e1t az al\u00e1bbiakban ([more info]({more_info_url})).\n", + "description": "Hozzon l\u00e9tre egy Nest Device Access projektet, amelynek be\u00e1ll\u00edt\u00e1sa **5 USD d\u00edjat** ig\u00e9nyel.\n1. L\u00e1togasson el a [Device Access Console]({device_access_console_url}) oldalra, \u00e9s a fizet\u00e9si folyamaton menjen kereszt\u00fcl.\n1. Kattintson a **Projekt l\u00e9trehoz\u00e1sa** gombra.\n1. Adjon nevet a projektnek, \u00e9s kattintson a **K\u00f6vetkez\u0151** gombra.\n1. Adja meg az OAuth \u00fcgyf\u00e9l azonos\u00edt\u00f3j\u00e1t (Client ID)\n1. Enged\u00e9lyezze az esem\u00e9nyeket a **Enable** \u00e9s a **Create project** gombra kattintva.\n\nAdja meg a Device Access projekt azonos\u00edt\u00f3j\u00e1t az al\u00e1bbiakban ([more info]({more_info_url})).\n", "title": "Nest: Hozzon l\u00e9tre egy eszk\u00f6z-hozz\u00e1f\u00e9r\u00e9si projektet" }, "device_project_upgrade": { diff --git a/homeassistant/components/nest/translations/pt-BR.json b/homeassistant/components/nest/translations/pt-BR.json index 9f5f9b9eff7..74f68f01775 100644 --- a/homeassistant/components/nest/translations/pt-BR.json +++ b/homeassistant/components/nest/translations/pt-BR.json @@ -52,7 +52,7 @@ "data": { "project_id": "C\u00f3digo do projeto de acesso ao dispositivo" }, - "description": "Crie um projeto Nest Device Access que **exija uma taxa de US$ 5** para ser configurado.\n 1. V\u00e1 para o [Device Access Console]( {device_access_console_url} ) e atrav\u00e9s do fluxo de pagamento.\n 1. Clique em **Criar projeto**\n 1. D\u00ea um nome ao seu projeto Device Access e clique em **Pr\u00f3ximo**.\n 1. Insira seu ID do cliente OAuth\n 1. Ative os eventos clicando em **Ativar** e **Criar projeto**. \n\n Insira o ID do projeto de acesso ao dispositivo abaixo ([mais informa\u00e7\u00f5es]( {more_info_url} )).", + "description": "Crie um projeto Nest Device Access que **exije o pagamento de uma taxa de US$ 5 ao Google** para ser configurado.\n 1. V\u00e1 para o [Device Access Console]({device_access_console_url}) e atrav\u00e9s do fluxo de pagamento.\n 1. Clique em **Criar projeto**\n 1. D\u00ea um nome ao seu projeto Device Access e clique em **Next**.\n 1. Insira seu ID do cliente OAuth\n 1. Ative os eventos clicando em **Ativar** e **Criar projeto**. \n\n Insira o ID do projeto de acesso ao dispositivo abaixo ([mais informa\u00e7\u00f5es]({more_info_url})).\n", "title": "Nest: criar um projeto de acesso ao dispositivo" }, "device_project_upgrade": { diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index 44295514840..5a995fb35ae 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -52,7 +52,7 @@ "data": { "project_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, - "description": "\u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u043f\u0440\u043e\u0435\u043a\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443, \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e **\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043b\u0430\u0442\u0430 \u0432 \u0440\u0430\u0437\u043c\u0435\u0440\u0435 5 \u0434\u043e\u043b\u043b\u0430\u0440\u043e\u0432 \u0421\u0428\u0410**.\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [\u041a\u043e\u043d\u0441\u043e\u043b\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c]({device_access_console_url}) \u0438 \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u043e\u043f\u043b\u0430\u0442\u044b.\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 **Create project**.\n3. \u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Next**.\n4. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth.\n5. \u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0439\u0442\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f, \u043d\u0430\u0436\u0430\u0432 **Enable** \u0438 **Create project**. \n\n\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c \u043d\u0438\u0436\u0435 ([\u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435]({more_info_url})).", + "description": "\u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u043f\u0440\u043e\u0435\u043a\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443, \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e **Google \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043e\u043f\u043b\u0430\u0442\u0443 \u0432 \u0440\u0430\u0437\u043c\u0435\u0440\u0435 5 \u0434\u043e\u043b\u043b\u0430\u0440\u043e\u0432 \u0421\u0428\u0410**.\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [\u041a\u043e\u043d\u0441\u043e\u043b\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c]({device_access_console_url}) \u0438 \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u043e\u043f\u043b\u0430\u0442\u044b.\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 **Create project**.\n3. \u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Next**.\n4. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth.\n5. \u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0439\u0442\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f, \u043d\u0430\u0436\u0430\u0432 **Enable** \u0438 **Create project**. \n\n\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c \u043d\u0438\u0436\u0435 ([\u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435]({more_info_url})).", "title": "Nest: \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, "device_project_upgrade": { diff --git a/homeassistant/components/nest/translations/zh-Hant.json b/homeassistant/components/nest/translations/zh-Hant.json index 77f271518a5..a0ff9cab7f8 100644 --- a/homeassistant/components/nest/translations/zh-Hant.json +++ b/homeassistant/components/nest/translations/zh-Hant.json @@ -52,7 +52,7 @@ "data": { "project_id": "\u88dd\u7f6e\u5b58\u53d6\u5c08\u6848 ID" }, - "description": "\u5efa\u8b70 Nest \u88dd\u7f6e\u5b58\u53d6\u5c08\u6848 **\u5c07\u6703\u9700\u8981\u652f\u4ed8 $5 \u7f8e\u91d1\u8cbb\u7528** \u4ee5\u9032\u884c\u8a2d\u5b9a\u3002\n1. \u9023\u7dda\u81f3 [\u88dd\u7f6e\u5b58\u53d6\u63a7\u5236\u53f0]({device_access_console_url})\u3001\u4e26\u9032\u884c\u4ed8\u6b3e\u7a0b\u5e8f\u3002\n1. \u9ede\u9078 **\u5efa\u7acb\u5c08\u6848**\n1. \u9032\u884c\u88dd\u7f6e\u5b58\u53d6\u5c08\u6848\u547d\u540d\u3001\u4e26\u9ede\u9078 **\u4e0b\u4e00\u6b65**\u3002\n1. \u8f38\u5165 OAuth \u5ba2\u6236\u7aef ID\n1. \u9ede\u9078 **\u555f\u7528** \u4ee5\u555f\u7528\u4e8b\u4ef6\u4e26 **\u5efa\u7acb\u5c08\u6848**\u3002\n\n\u65bc\u4e0b\u65b9 ([\u66f4\u591a\u8cc7\u8a0a]({more_info_url})) \u8f38\u5165\u88dd\u7f6e\u5b58\u53d6\u5c08\u6848 ID\u3002\n", + "description": "\u5efa\u7acb Nest \u88dd\u7f6e\u5b58\u53d6\u5c08\u6848 **\u5c07\u6703\u9700\u8981\u652f\u4ed8 Google $5 \u7f8e\u91d1\u8cbb\u7528** \u4ee5\u9032\u884c\u8a2d\u5b9a\u3002\n1. \u9023\u7dda\u81f3 [\u88dd\u7f6e\u5b58\u53d6\u63a7\u5236\u53f0]({device_access_console_url})\u3001\u4e26\u9032\u884c\u4ed8\u6b3e\u7a0b\u5e8f\u3002\n1. \u9ede\u9078 **\u5efa\u7acb\u5c08\u6848**\n1. \u9032\u884c\u88dd\u7f6e\u5b58\u53d6\u5c08\u6848\u547d\u540d\u3001\u4e26\u9ede\u9078 **\u4e0b\u4e00\u6b65**\u3002\n1. \u8f38\u5165 OAuth \u5ba2\u6236\u7aef ID\n1. \u9ede\u9078 **\u555f\u7528** \u4ee5\u555f\u7528\u4e8b\u4ef6\u4e26 **\u5efa\u7acb\u5c08\u6848**\u3002\n\n\u65bc\u4e0b\u65b9 ([\u66f4\u591a\u8cc7\u8a0a]({more_info_url})) \u8f38\u5165\u88dd\u7f6e\u5b58\u53d6\u5c08\u6848 ID\u3002\n", "title": "Nest\uff1a\u5efa\u7acb\u88dd\u7f6e\u5b58\u53d6\u5c08\u6848" }, "device_project_upgrade": { diff --git a/homeassistant/components/nina/translations/bg.json b/homeassistant/components/nina/translations/bg.json index be3ffecd284..4fdba83979a 100644 --- a/homeassistant/components/nina/translations/bg.json +++ b/homeassistant/components/nina/translations/bg.json @@ -9,6 +9,10 @@ } }, "options": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, "step": { "init": { "title": "\u041e\u043f\u0446\u0438\u0438" diff --git a/homeassistant/components/nuki/translations/bg.json b/homeassistant/components/nuki/translations/bg.json index 37e8e854866..1a6aff3fe4c 100644 --- a/homeassistant/components/nuki/translations/bg.json +++ b/homeassistant/components/nuki/translations/bg.json @@ -1,9 +1,15 @@ { "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { + "reauth_confirm": { + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, "user": { "data": { "port": "\u041f\u043e\u0440\u0442" diff --git a/homeassistant/components/octoprint/translations/bg.json b/homeassistant/components/octoprint/translations/bg.json index 670311552c9..0635640be7d 100644 --- a/homeassistant/components/octoprint/translations/bg.json +++ b/homeassistant/components/octoprint/translations/bg.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { @@ -10,10 +11,16 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "reauth_confirm": { + "data": { + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", "port": "\u041d\u043e\u043c\u0435\u0440 \u043d\u0430 \u043f\u043e\u0440\u0442", + "ssl": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 SSL", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } diff --git a/homeassistant/components/open_meteo/translations/bg.json b/homeassistant/components/open_meteo/translations/bg.json index 24a982db36a..2675f2ca117 100644 --- a/homeassistant/components/open_meteo/translations/bg.json +++ b/homeassistant/components/open_meteo/translations/bg.json @@ -2,6 +2,9 @@ "config": { "step": { "user": { + "data": { + "zone": "\u0417\u043e\u043d\u0430" + }, "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u043a\u043e\u0435\u0442\u043e \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0437\u0430 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u0437\u0430 \u0432\u0440\u0435\u043c\u0435\u0442\u043e" } } diff --git a/homeassistant/components/picnic/translations/bg.json b/homeassistant/components/picnic/translations/bg.json index aaf9f767fff..24fa035f619 100644 --- a/homeassistant/components/picnic/translations/bg.json +++ b/homeassistant/components/picnic/translations/bg.json @@ -5,13 +5,16 @@ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { "user": { "data": { - "country_code": "\u041a\u043e\u0434 \u043d\u0430 \u0434\u044a\u0440\u0436\u0430\u0432\u0430\u0442\u0430" + "country_code": "\u041a\u043e\u0434 \u043d\u0430 \u0434\u044a\u0440\u0436\u0430\u0432\u0430\u0442\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } } diff --git a/homeassistant/components/qnap_qsw/translations/bg.json b/homeassistant/components/qnap_qsw/translations/bg.json index 33ce2a4028f..091a2f4466d 100644 --- a/homeassistant/components/qnap_qsw/translations/bg.json +++ b/homeassistant/components/qnap_qsw/translations/bg.json @@ -8,6 +8,12 @@ "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { + "discovered_connection": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/roomba/translations/ru.json b/homeassistant/components/roomba/translations/ru.json index efba186e20c..52b68a45466 100644 --- a/homeassistant/components/roomba/translations/ru.json +++ b/homeassistant/components/roomba/translations/ru.json @@ -12,7 +12,7 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0438 \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0439\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 Home \u043d\u0430 {name}, \u043f\u043e\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0438\u0437\u0434\u0430\u0441\u0442 \u0437\u0432\u0443\u043a (\u043e\u043a\u043e\u043b\u043e \u0434\u0432\u0443\u0445 \u0441\u0435\u043a\u0443\u043d\u0434). \u0417\u0430\u0442\u0435\u043c \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 30 \u0441\u0435\u043a\u0443\u043d\u0434 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\".", + "description": "\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 iRobot \u043d\u0435 \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u043e \u043d\u0438 \u043d\u0430 \u043e\u0434\u043d\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0438 \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0439\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 Home \u043d\u0430 {name}, \u043f\u043e\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0438\u0437\u0434\u0430\u0441\u0442 \u0437\u0432\u0443\u043a (\u043e\u043a\u043e\u043b\u043e \u0434\u0432\u0443\u0445 \u0441\u0435\u043a\u0443\u043d\u0434). \u0417\u0430\u0442\u0435\u043c \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 30 \u0441\u0435\u043a\u0443\u043d\u0434 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\".", "title": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u043f\u0430\u0440\u043e\u043b\u044f" }, "link_manual": { diff --git a/homeassistant/components/schedule/translations/bg.json b/homeassistant/components/schedule/translations/bg.json index 292b4186ce8..2bc24e10980 100644 --- a/homeassistant/components/schedule/translations/bg.json +++ b/homeassistant/components/schedule/translations/bg.json @@ -1,3 +1,9 @@ { + "state": { + "_": { + "off": "\u0418\u0437\u043a\u043b.", + "on": "\u0412\u043a\u043b." + } + }, "title": "\u0413\u0440\u0430\u0444\u0438\u043a" } \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/bg.json b/homeassistant/components/screenlogic/translations/bg.json index b8fccb94a47..9531331d8d5 100644 --- a/homeassistant/components/screenlogic/translations/bg.json +++ b/homeassistant/components/screenlogic/translations/bg.json @@ -1,9 +1,13 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "flow_title": "{name}", "step": { "gateway_entry": { "data": { + "ip_address": "IP \u0430\u0434\u0440\u0435\u0441", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/sensibo/translations/sensor.bg.json b/homeassistant/components/sensibo/translations/sensor.bg.json new file mode 100644 index 00000000000..0ab81a5de11 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.bg.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "\u041d\u043e\u0440\u043c\u0430\u043b\u0435\u043d", + "s": "\u0427\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/cs.json b/homeassistant/components/sensor/translations/cs.json index 23ba416dd34..d8f6dd25b57 100644 --- a/homeassistant/components/sensor/translations/cs.json +++ b/homeassistant/components/sensor/translations/cs.json @@ -3,6 +3,7 @@ "condition_type": { "is_battery_level": "Aktu\u00e1ln\u00ed \u00farove\u0148 nabit\u00ed baterie {entity_name}", "is_current": "Aktu\u00e1ln\u00ed proud {entity_name}", + "is_distance": "Aktu\u00e1ln\u00ed vzd\u00e1lenost {entity_name}", "is_energy": "Aktu\u00e1ln\u00ed energie {entity_name}", "is_gas": "Aktu\u00e1ln\u00ed mno\u017estv\u00ed plynu {entity_name}", "is_humidity": "Aktu\u00e1ln\u00ed vlhkost {entity_name}", @@ -12,14 +13,17 @@ "is_power_factor": "Aktu\u00e1ln\u00ed \u00fa\u010din\u00edk {entity_name}", "is_pressure": "Aktu\u00e1ln\u00ed tlak {entity_name}", "is_signal_strength": "Aktu\u00e1ln\u00ed s\u00edla sign\u00e1lu {entity_name}", + "is_speed": "Aktu\u00e1ln\u00ed rychlost {entity_name}", "is_sulphur_dioxide": "Aktu\u00e1ln\u00ed \u00farove\u0148 koncentrace oxidu si\u0159i\u010dit\u00e9ho {entity_name}", "is_temperature": "Aktu\u00e1ln\u00ed teplota {entity_name}", "is_value": "Aktu\u00e1ln\u00ed hodnota {entity_name}", - "is_voltage": "Aktu\u00e1ln\u00ed nap\u011bt\u00ed {entity_name}" + "is_voltage": "Aktu\u00e1ln\u00ed nap\u011bt\u00ed {entity_name}", + "is_volume": "Aktu\u00e1ln\u00ed objem {entity_name}" }, "trigger_type": { "battery_level": "P\u0159i zm\u011bn\u011b \u00farovn\u011b baterie {entity_name}", "current": "P\u0159i zm\u011bn\u011b proudu {entity_name}", + "distance": "P\u0159i zm\u011bn\u011b vzd\u00e1lenosti {entity_name}", "energy": "P\u0159i zm\u011bn\u011b energie {entity_name}", "gas": "P\u0159i zm\u011bn\u011b mno\u017estv\u00ed plynu {entity_name}", "humidity": "P\u0159i zm\u011bn\u011b vlhkosti {entity_name}", @@ -30,9 +34,11 @@ "power_factor": "P\u0159i zm\u011bn\u011b \u00fa\u010din\u00edku {entity_name}", "pressure": "P\u0159i zm\u011bn\u011b tlaku {entity_name}", "signal_strength": "P\u0159i zm\u011bn\u011b s\u00edly sign\u00e1lu {entity_name}", + "speed": "P\u0159i zm\u011bn\u011b rychlosti {entity_name}", "temperature": "P\u0159i zm\u011bn\u011b teploty {entity_name}", "value": "P\u0159i zm\u011bn\u011b hodnoty {entity_name}", - "voltage": "P\u0159i zm\u011bn\u011b nap\u011bt\u00ed {entity_name}" + "voltage": "P\u0159i zm\u011bn\u011b nap\u011bt\u00ed {entity_name}", + "volume": "P\u0159i zm\u011bn\u011b objemu {entity_name}" } }, "state": { diff --git a/homeassistant/components/shelly/translations/bg.json b/homeassistant/components/shelly/translations/bg.json index 131d4bf19c6..e856ebe4a54 100644 --- a/homeassistant/components/shelly/translations/bg.json +++ b/homeassistant/components/shelly/translations/bg.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e.", "unsupported_firmware": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0432\u0435\u0440\u0441\u0438\u044f \u043d\u0430 \u0444\u044a\u0440\u043c\u0443\u0435\u0440\u0430." }, "error": { diff --git a/homeassistant/components/shelly/translations/cs.json b/homeassistant/components/shelly/translations/cs.json index d7b817eb994..c2f45d0f4c7 100644 --- a/homeassistant/components/shelly/translations/cs.json +++ b/homeassistant/components/shelly/translations/cs.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", + "reauth_unsuccessful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed se nezda\u0159ilo, odeberte pros\u00edm integraci a nastavte ji znovu.", "unsupported_firmware": "Za\u0159\u00edzen\u00ed pou\u017e\u00edv\u00e1 nepodporovanou verzi firmwaru." }, "error": { @@ -20,6 +22,12 @@ "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } }, + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, "user": { "data": { "host": "Hostitel" diff --git a/homeassistant/components/simplepush/translations/bg.json b/homeassistant/components/simplepush/translations/bg.json new file mode 100644 index 00000000000..3e581f0623d --- /dev/null +++ b/homeassistant/components/simplepush/translations/bg.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/bg.json b/homeassistant/components/smarttub/translations/bg.json index ebfcda2158d..48078b96c08 100644 --- a/homeassistant/components/smarttub/translations/bg.json +++ b/homeassistant/components/smarttub/translations/bg.json @@ -4,6 +4,9 @@ "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { + "reauth_confirm": { + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" diff --git a/homeassistant/components/soundtouch/translations/bg.json b/homeassistant/components/soundtouch/translations/bg.json index ab665e1e59b..5d235f77133 100644 --- a/homeassistant/components/soundtouch/translations/bg.json +++ b/homeassistant/components/soundtouch/translations/bg.json @@ -5,6 +5,13 @@ }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/sun/translations/bg.json b/homeassistant/components/sun/translations/bg.json index 81ead95c95f..cc1a533ab9d 100644 --- a/homeassistant/components/sun/translations/bg.json +++ b/homeassistant/components/sun/translations/bg.json @@ -2,6 +2,11 @@ "config": { "abort": { "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "step": { + "user": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?" + } } }, "state": { diff --git a/homeassistant/components/system_bridge/translations/bg.json b/homeassistant/components/system_bridge/translations/bg.json index ccf68c66d57..25a1b280f57 100644 --- a/homeassistant/components/system_bridge/translations/bg.json +++ b/homeassistant/components/system_bridge/translations/bg.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/tautulli/translations/cs.json b/homeassistant/components/tautulli/translations/cs.json index 45e02001105..e65f964f82d 100644 --- a/homeassistant/components/tautulli/translations/cs.json +++ b/homeassistant/components/tautulli/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { diff --git a/homeassistant/components/vulcan/translations/bg.json b/homeassistant/components/vulcan/translations/bg.json index f99cd3cca14..db0b6604e3f 100644 --- a/homeassistant/components/vulcan/translations/bg.json +++ b/homeassistant/components/vulcan/translations/bg.json @@ -8,6 +8,11 @@ "unknown": "\u0412\u044a\u0437\u043d\u0438\u043a\u043d\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "auth": { + "data": { + "region": "\u0421\u0438\u043c\u0432\u043e\u043b" + } + }, "select_saved_credentials": { "data": { "credentials": "\u0412\u0445\u043e\u0434" diff --git a/homeassistant/components/waze_travel_time/translations/bg.json b/homeassistant/components/waze_travel_time/translations/bg.json index fb5df032671..5b18b5ba021 100644 --- a/homeassistant/components/waze_travel_time/translations/bg.json +++ b/homeassistant/components/waze_travel_time/translations/bg.json @@ -11,5 +11,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "units": "\u0415\u0434\u0438\u043d\u0438\u0446\u0438" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/bg.json b/homeassistant/components/zha/translations/bg.json index 3bd7629cd95..01a0ed3e134 100644 --- a/homeassistant/components/zha/translations/bg.json +++ b/homeassistant/components/zha/translations/bg.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 ZHA \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, + "flow_title": "{name}", "step": { "choose_formation_strategy": { "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0440\u0435\u0436\u043e\u0432\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0437\u0430 \u0432\u0430\u0448\u0435\u0442\u043e \u0440\u0430\u0434\u0438\u043e.", @@ -61,6 +62,11 @@ } } }, + "config_panel": { + "zha_options": { + "title": "\u0413\u043b\u043e\u0431\u0430\u043b\u043d\u0438 \u043e\u043f\u0446\u0438\u0438" + } + }, "device_automation": { "action_type": { "squawk": "\u041a\u0432\u0430\u043a", From 3c07d40fe7cc99459e5366d229abfebce7916482 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 3 Oct 2022 20:58:53 -0400 Subject: [PATCH 141/985] Bump ZHA dependencies (#79565) Bump all ZHA dependencies --- homeassistant/components/zha/manifest.json | 14 +++++++------- requirements_all.txt | 14 +++++++------- requirements_test_all.txt | 14 +++++++------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 3f07d81ddb5..322f93e8373 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,15 +4,15 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.33.1", + "bellows==0.34.0", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.80", - "zigpy-deconz==0.18.1", - "zigpy==0.50.3", - "zigpy-xbee==0.15.0", - "zigpy-zigate==0.9.2", - "zigpy-znp==0.8.2" + "zha-quirks==0.0.81", + "zigpy-deconz==0.19.0", + "zigpy==0.51.1", + "zigpy-xbee==0.16.0", + "zigpy-zigate==0.10.0", + "zigpy-znp==0.9.0" ], "usb": [ { diff --git a/requirements_all.txt b/requirements_all.txt index e1a53d15462..aec36632cb8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.33.1 +bellows==0.34.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.4 @@ -2595,7 +2595,7 @@ zengge==0.2 zeroconf==0.39.1 # homeassistant.components.zha -zha-quirks==0.0.80 +zha-quirks==0.0.81 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2604,19 +2604,19 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.18.1 +zigpy-deconz==0.19.0 # homeassistant.components.zha -zigpy-xbee==0.15.0 +zigpy-xbee==0.16.0 # homeassistant.components.zha -zigpy-zigate==0.9.2 +zigpy-zigate==0.10.0 # homeassistant.components.zha -zigpy-znp==0.8.2 +zigpy-znp==0.9.0 # homeassistant.components.zha -zigpy==0.50.3 +zigpy==0.51.1 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 949d8ee0ca6..370db3cef7c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -331,7 +331,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.33.1 +bellows==0.34.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.4 @@ -1796,22 +1796,22 @@ youless-api==0.16 zeroconf==0.39.1 # homeassistant.components.zha -zha-quirks==0.0.80 +zha-quirks==0.0.81 # homeassistant.components.zha -zigpy-deconz==0.18.1 +zigpy-deconz==0.19.0 # homeassistant.components.zha -zigpy-xbee==0.15.0 +zigpy-xbee==0.16.0 # homeassistant.components.zha -zigpy-zigate==0.9.2 +zigpy-zigate==0.10.0 # homeassistant.components.zha -zigpy-znp==0.8.2 +zigpy-znp==0.9.0 # homeassistant.components.zha -zigpy==0.50.3 +zigpy==0.51.1 # homeassistant.components.zwave_js zwave-js-server-python==0.42.0 From 90637a721c0a0890bfd1dbc34294bda19787df0a Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 3 Oct 2022 18:10:28 -0700 Subject: [PATCH 142/985] Add option to set a stun server for RTSPtoWebRTC (#72574) --- .../components/rtsp_to_webrtc/__init__.py | 35 +++++++++- .../components/rtsp_to_webrtc/config_flow.py | 42 ++++++++++- .../components/rtsp_to_webrtc/strings.json | 9 +++ .../rtsp_to_webrtc/translations/en.json | 9 +++ tests/components/rtsp_to_webrtc/conftest.py | 15 +++- .../rtsp_to_webrtc/test_config_flow.py | 46 +++++++++++++ tests/components/rtsp_to_webrtc/test_init.py | 69 ++++++++++++++++++- 7 files changed, 219 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rtsp_to_webrtc/__init__.py b/homeassistant/components/rtsp_to_webrtc/__init__.py index 185cfcb0240..f0e013fc02f 100644 --- a/homeassistant/components/rtsp_to_webrtc/__init__.py +++ b/homeassistant/components/rtsp_to_webrtc/__init__.py @@ -24,10 +24,11 @@ import async_timeout from rtsp_to_webrtc.client import get_adaptive_client from rtsp_to_webrtc.exceptions import ClientError, ResponseError from rtsp_to_webrtc.interface import WebRTCClientInterface +import voluptuous as vol -from homeassistant.components import camera +from homeassistant.components import camera, websocket_api from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -37,6 +38,7 @@ DOMAIN = "rtsp_to_webrtc" DATA_SERVER_URL = "server_url" DATA_UNSUB = "unsub" TIMEOUT = 10 +CONF_STUN_SERVER = "stun_server" async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -54,6 +56,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (TimeoutError, ClientError) as err: raise ConfigEntryNotReady from err + hass.data[DOMAIN][CONF_STUN_SERVER] = entry.options.get(CONF_STUN_SERVER, "") + async def async_offer_for_stream_source( stream_source: str, offer_sdp: str, @@ -78,10 +82,37 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, DOMAIN, async_offer_for_stream_source ) ) + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) + + websocket_api.async_register_command(hass, ws_get_settings) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + if DOMAIN in hass.data: + del hass.data[DOMAIN] return True + + +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Reload config entry when options change.""" + if hass.data[DOMAIN][CONF_STUN_SERVER] != entry.options.get(CONF_STUN_SERVER, ""): + await hass.config_entries.async_reload(entry.entry_id) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "rtsp_to_webrtc/get_settings", + } +) +@callback +def ws_get_settings( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Handle the websocket command.""" + connection.send_result( + msg["id"], + {CONF_STUN_SERVER: hass.data.get(DOMAIN, {}).get(CONF_STUN_SERVER, "")}, + ) diff --git a/homeassistant/components/rtsp_to_webrtc/config_flow.py b/homeassistant/components/rtsp_to_webrtc/config_flow.py index 815c5e5db7b..865a6bafcb6 100644 --- a/homeassistant/components/rtsp_to_webrtc/config_flow.py +++ b/homeassistant/components/rtsp_to_webrtc/config_flow.py @@ -11,10 +11,11 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.hassio import HassioServiceInfo from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from . import DATA_SERVER_URL, DOMAIN +from . import CONF_STUN_SERVER, DATA_SERVER_URL, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -104,3 +105,42 @@ class RTSPToWebRTCConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=self._hassio_discovery["addon"], data={DATA_SERVER_URL: url}, ) + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Create an options flow.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """RTSPtoWeb Options flow.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_STUN_SERVER, + description={ + "suggested_value": self.config_entry.options.get( + CONF_STUN_SERVER + ), + }, + ): str, + } + ), + ) diff --git a/homeassistant/components/rtsp_to_webrtc/strings.json b/homeassistant/components/rtsp_to_webrtc/strings.json index 5ef91eaf206..939c30766e2 100644 --- a/homeassistant/components/rtsp_to_webrtc/strings.json +++ b/homeassistant/components/rtsp_to_webrtc/strings.json @@ -23,5 +23,14 @@ "server_failure": "RTSPtoWebRTC server returned an error. Check logs for more information.", "server_unreachable": "Unable to communicate with RTSPtoWebRTC server. Check logs for more information." } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Stun server address (host:port)" + } + } + } } } diff --git a/homeassistant/components/rtsp_to_webrtc/translations/en.json b/homeassistant/components/rtsp_to_webrtc/translations/en.json index c54983d63d3..a519883b764 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/en.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/en.json @@ -23,5 +23,14 @@ "title": "Configure RTSPtoWebRTC" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Stun server address (host:port)" + } + } + } } } \ No newline at end of file diff --git a/tests/components/rtsp_to_webrtc/conftest.py b/tests/components/rtsp_to_webrtc/conftest.py index 5e737efc397..5a0d6de01df 100644 --- a/tests/components/rtsp_to_webrtc/conftest.py +++ b/tests/components/rtsp_to_webrtc/conftest.py @@ -65,9 +65,20 @@ async def config_entry_data() -> dict[str, Any]: @pytest.fixture -async def config_entry(config_entry_data: dict[str, Any]) -> MockConfigEntry: +def config_entry_options() -> dict[str, Any] | None: + """Fixture to set initial config entry options.""" + return None + + +@pytest.fixture +async def config_entry( + config_entry_data: dict[str, Any], + config_entry_options: dict[str, Any] | None, +) -> MockConfigEntry: """Fixture for MockConfigEntry.""" - return MockConfigEntry(domain=DOMAIN, data=config_entry_data) + return MockConfigEntry( + domain=DOMAIN, data=config_entry_data, options=config_entry_options + ) @pytest.fixture diff --git a/tests/components/rtsp_to_webrtc/test_config_flow.py b/tests/components/rtsp_to_webrtc/test_config_flow.py index a6cd4d6798f..cca6395c317 100644 --- a/tests/components/rtsp_to_webrtc/test_config_flow.py +++ b/tests/components/rtsp_to_webrtc/test_config_flow.py @@ -9,8 +9,11 @@ import rtsp_to_webrtc from homeassistant import config_entries from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.rtsp_to_webrtc import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from .conftest import ComponentSetup + from tests.common import MockConfigEntry @@ -212,3 +215,46 @@ async def test_hassio_discovery_server_failure(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result.get("type") == "abort" assert result.get("reason") == "server_failure" + + +async def test_options_flow( + hass: HomeAssistant, + config_entry: MockConfigEntry, + setup_integration: ComponentSetup, +) -> None: + """Test setting stun server in options flow.""" + with patch( + "homeassistant.components.rtsp_to_webrtc.async_setup_entry", + return_value=True, + ): + await setup_integration() + + assert config_entry.state is ConfigEntryState.LOADED + assert not config_entry.options + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == "form" + assert result["step_id"] == "init" + data_schema = result["data_schema"].schema + assert set(data_schema) == {"stun_server"} + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "stun_server": "example.com:1234", + }, + ) + assert result["type"] == "create_entry" + await hass.async_block_till_done() + assert config_entry.options == {"stun_server": "example.com:1234"} + + # Clear the value + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == "form" + assert result["step_id"] == "init" + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == "create_entry" + await hass.async_block_till_done() + assert config_entry.options == {} diff --git a/tests/components/rtsp_to_webrtc/test_init.py b/tests/components/rtsp_to_webrtc/test_init.py index 759fea7c813..afa365a3044 100644 --- a/tests/components/rtsp_to_webrtc/test_init.py +++ b/tests/components/rtsp_to_webrtc/test_init.py @@ -11,13 +11,14 @@ import aiohttp import pytest import rtsp_to_webrtc -from homeassistant.components.rtsp_to_webrtc import DOMAIN +from homeassistant.components.rtsp_to_webrtc import CONF_STUN_SERVER, DOMAIN from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from .conftest import SERVER_URL, STREAM_SOURCE, ComponentSetup +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker # The webrtc component does not inspect the details of the offer and answer, @@ -154,3 +155,69 @@ async def test_offer_failure( assert response["error"].get("code") == "web_rtc_offer_failed" assert "message" in response["error"] assert "RTSPtoWebRTC server communication failure" in response["error"]["message"] + + +async def test_no_stun_server( + hass: HomeAssistant, + rtsp_to_webrtc_client: Any, + setup_integration: ComponentSetup, + hass_ws_client: Callable[[...], Awaitable[aiohttp.ClientWebSocketResponse]], +) -> None: + """Test successful setup and unload.""" + await setup_integration() + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 2, + "type": "rtsp_to_webrtc/get_settings", + } + ) + response = await client.receive_json() + assert response.get("id") == 2 + assert response.get("type") == TYPE_RESULT + assert "result" in response + assert response["result"].get("stun_server") == "" + + +@pytest.mark.parametrize( + "config_entry_options", [{CONF_STUN_SERVER: "example.com:1234"}] +) +async def test_stun_server( + hass: HomeAssistant, + rtsp_to_webrtc_client: Any, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, + hass_ws_client: Callable[[...], Awaitable[aiohttp.ClientWebSocketResponse]], +) -> None: + """Test successful setup and unload.""" + await setup_integration() + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 3, + "type": "rtsp_to_webrtc/get_settings", + } + ) + response = await client.receive_json() + assert response.get("id") == 3 + assert response.get("type") == TYPE_RESULT + assert "result" in response + assert response["result"].get("stun_server") == "example.com:1234" + + # Simulate an options flow change, clearing the stun server and verify the change is reflected + hass.config_entries.async_update_entry(config_entry, options={}) + await hass.async_block_till_done() + + await client.send_json( + { + "id": 4, + "type": "rtsp_to_webrtc/get_settings", + } + ) + response = await client.receive_json() + assert response.get("id") == 4 + assert response.get("type") == TYPE_RESULT + assert "result" in response + assert response["result"].get("stun_server") == "" From 92ca95ca81f4d62510894cbb209b53eea7ee75f1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 4 Oct 2022 03:13:48 +0200 Subject: [PATCH 143/985] Fix preserving long term statistics when entity_id is changed (#79556) --- homeassistant/components/recorder/statistics.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index ad948b560bb..7ba5c5f8c73 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -29,6 +29,7 @@ from homeassistant.core import Event, HomeAssistant, callback, valid_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.start import async_at_start from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.util import dt as dt_util @@ -299,13 +300,17 @@ def async_setup(hass: HomeAssistant) -> None: return True - if hass.is_running: + @callback + def setup_entity_registry_event_handler(hass: HomeAssistant) -> None: + """Subscribe to event registry events.""" hass.bus.async_listen( entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, _async_entity_id_changed, event_filter=entity_registry_changed_filter, ) + async_at_start(hass, setup_entity_registry_event_handler) + def get_start_time() -> datetime: """Return start time.""" From 2768b2445a67e897e8d00bd9e0448a5f8f94987a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Oct 2022 15:15:09 -1000 Subject: [PATCH 144/985] Remove call to deprecated bleak register_detection_callback (#79558) --- .../components/bluetooth/__init__.py | 6 ++--- homeassistant/components/bluetooth/scanner.py | 24 +++++++++++++------ tests/components/bluetooth/test_init.py | 4 +++- tests/components/bluetooth/test_scanner.py | 14 ++++------- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 2afb638b230..f175b01b798 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -55,7 +55,7 @@ from .models import ( HaBluetoothConnector, ProcessAdvertisementCallback, ) -from .scanner import HaScanner, ScannerStartError, create_bleak_scanner +from .scanner import HaScanner, ScannerStartError from .util import adapter_human_name, adapter_unique_name, async_default_adapter if TYPE_CHECKING: @@ -400,13 +400,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: passive = entry.options.get(CONF_PASSIVE) mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE + scanner = HaScanner(hass, mode, adapter, address) try: - bleak_scanner = create_bleak_scanner(mode, adapter) + scanner.async_setup() except RuntimeError as err: raise ConfigEntryNotReady( f"{adapter_human_name(adapter, address)}: {err}" ) from err - scanner = HaScanner(hass, bleak_scanner, adapter, address) info_callback = async_get_advertisement_callback(hass) entry.async_on_unload(scanner.async_register_callback(info_callback)) try: diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 857a0e4c01c..9bc68059a7f 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -16,7 +16,7 @@ from bleak.assigned_numbers import AdvertisementDataType from bleak.backends.bluezdbus.advertisement_monitor import OrPattern from bleak.backends.bluezdbus.scanner import BlueZScannerArgs from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData +from bleak.backends.scanner import AdvertisementData, AdvertisementDataCallback from bleak_retry_connector import get_device_by_adapter from dbus_fast import InvalidMessageError @@ -86,11 +86,14 @@ class ScannerStartError(HomeAssistantError): def create_bleak_scanner( - scanning_mode: BluetoothScanningMode, adapter: str | None + detection_callback: AdvertisementDataCallback, + scanning_mode: BluetoothScanningMode, + adapter: str | None, ) -> bleak.BleakScanner: """Create a Bleak scanner.""" scanner_kwargs: dict[str, Any] = { - "scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode] + "detection_callback": detection_callback, + "scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode], } if platform.system() == "Linux": # Only Linux supports multiple adapters @@ -117,16 +120,18 @@ class HaScanner(BaseHaScanner): over ethernet, usb over ethernet, etc. """ + scanner: bleak.BleakScanner + def __init__( self, hass: HomeAssistant, - scanner: bleak.BleakScanner, + mode: BluetoothScanningMode, adapter: str, address: str, ) -> None: """Init bluetooth discovery.""" self.hass = hass - self.scanner = scanner + self.mode = mode self.adapter = adapter self._start_stop_lock = asyncio.Lock() self._cancel_watchdog: CALLBACK_TYPE | None = None @@ -141,6 +146,13 @@ class HaScanner(BaseHaScanner): """Return a list of discovered devices.""" return self.scanner.discovered_devices + @hass_callback + def async_setup(self) -> None: + """Set up the scanner.""" + self.scanner = create_bleak_scanner( + self._async_detection_callback, self.mode, self.adapter + ) + async def async_get_device_by_address(self, address: str) -> BLEDevice | None: """Get a device by address.""" if platform.system() == "Linux": @@ -218,8 +230,6 @@ class HaScanner(BaseHaScanner): async def async_start(self) -> None: """Start bluetooth scanner.""" - self.scanner.register_detection_callback(self._async_detection_callback) - async with self._start_stop_lock: await self._async_start() diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 7ee1a9840db..2e311d9d97e 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -2,7 +2,7 @@ import asyncio from datetime import timedelta import time -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import ANY, MagicMock, Mock, patch from bleak import BleakError from bleak.backends.scanner import AdvertisementData, BLEDevice @@ -114,6 +114,7 @@ async def test_setup_and_stop_passive(hass, mock_bleak_scanner_start, one_adapte "adapter": "hci0", "bluez": scanner.PASSIVE_SCANNER_ARGS, "scanning_mode": "passive", + "detection_callback": ANY, } @@ -161,6 +162,7 @@ async def test_setup_and_stop_old_bluez( assert init_kwargs == { "adapter": "hci0", "scanning_mode": "active", + "detection_callback": ANY, } diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index 91e8ab50971..a4666352479 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -174,6 +174,10 @@ async def test_recovery_from_dbus_restart(hass, one_adapter): mock_discovered = [] class MockBleakScanner: + def __init__(self, detection_callback, *args, **kwargs): + nonlocal _callback + _callback = detection_callback + async def start(self, *args, **kwargs): """Mock Start.""" nonlocal called_start @@ -190,23 +194,15 @@ async def test_recovery_from_dbus_restart(hass, one_adapter): nonlocal mock_discovered return mock_discovered - def register_detection_callback(self, callback: AdvertisementDataCallback): - """Mock Register Detection Callback.""" - nonlocal _callback - _callback = callback - - scanner = MockBleakScanner() - with patch( "homeassistant.components.bluetooth.scanner.OriginalBleakScanner", - return_value=scanner, + MockBleakScanner, ): await async_setup_with_one_adapter(hass) assert called_start == 1 start_time_monotonic = time.monotonic() - scanner = _get_manager() mock_discovered = [MagicMock()] # Ensure we don't restart the scanner if we don't need to From eda6f13f8a92127f547f26a8c3ef302f73dd2675 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Mon, 3 Oct 2022 19:17:47 -0600 Subject: [PATCH 145/985] Remove repairs issue per PR review request (#79561) --- .../components/litterrobot/__init__.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index cf14239b22d..3d8f8487b33 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -6,8 +6,6 @@ from pylitterbot import FeederRobot, LitterRobot, LitterRobot3, Robot from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN from .hub import LitterRobotHub @@ -36,21 +34,6 @@ def get_platforms_for_robots(robots: list[Robot]) -> set[Platform]: } -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Litter-Robot integration.""" - async_create_issue( - hass, - DOMAIN, - "migrated_attributes", - breaks_in_ha_version="2022.12.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="migrated_attributes", - ) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Litter-Robot from a config entry.""" hass.data.setdefault(DOMAIN, {}) From e93deaa8aa7b0060e8a838bcbffc5d64835fed34 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 4 Oct 2022 03:50:05 +0200 Subject: [PATCH 146/985] Simplify long term statistics by always supporting unit conversion (#79557) --- homeassistant/components/sensor/recorder.py | 186 +++------ tests/components/sensor/test_recorder.py | 441 +++++++++++--------- 2 files changed, 308 insertions(+), 319 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 144502dd81a..1a72444c758 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -23,22 +23,11 @@ from homeassistant.components.recorder.models import ( StatisticMetaData, StatisticResult, ) -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import entity_sources from homeassistant.util import dt as dt_util -from homeassistant.util.unit_conversion import ( - BaseUnitConverter, - DistanceConverter, - EnergyConverter, - MassConverter, - PowerConverter, - PressureConverter, - SpeedConverter, - TemperatureConverter, - VolumeConverter, -) from . import ( ATTR_LAST_RESET, @@ -48,7 +37,6 @@ from . import ( STATE_CLASS_TOTAL, STATE_CLASS_TOTAL_INCREASING, STATE_CLASSES, - SensorDeviceClass, ) _LOGGER = logging.getLogger(__name__) @@ -59,18 +47,6 @@ DEFAULT_STATISTICS = { STATE_CLASS_TOTAL_INCREASING: {"sum"}, } -UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = { - SensorDeviceClass.DISTANCE: DistanceConverter, - SensorDeviceClass.ENERGY: EnergyConverter, - SensorDeviceClass.GAS: VolumeConverter, - SensorDeviceClass.POWER: PowerConverter, - SensorDeviceClass.PRESSURE: PressureConverter, - SensorDeviceClass.SPEED: SpeedConverter, - SensorDeviceClass.TEMPERATURE: TemperatureConverter, - SensorDeviceClass.VOLUME: VolumeConverter, - SensorDeviceClass.WEIGHT: MassConverter, -} - # Keep track of entities for which a warning about decreasing value has been logged SEEN_DIP = "sensor_seen_total_increasing_dip" WARN_DIP = "sensor_warn_total_increasing_dip" @@ -154,84 +130,84 @@ def _normalize_states( session: Session, old_metadatas: dict[str, tuple[int, StatisticMetaData]], entity_history: Iterable[State], - device_class: str | None, entity_id: str, ) -> tuple[str | None, str | None, list[tuple[float, State]]]: """Normalize units.""" old_metadata = old_metadatas[entity_id][1] if entity_id in old_metadatas else None state_unit: str | None = None - if device_class not in UNIT_CONVERTERS or ( + fstates: list[tuple[float, State]] = [] + for state in entity_history: + try: + fstate = _parse_float(state.state) + except (ValueError, TypeError): # TypeError to guard for NULL state in DB + continue + fstates.append((fstate, state)) + + if not fstates: + return None, None, fstates + + state_unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + if state_unit not in statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER or ( old_metadata and old_metadata["unit_of_measurement"] - not in UNIT_CONVERTERS[device_class].VALID_UNITS + not in statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER ): # We're either not normalizing this device class or this entity is not stored - # in a supported unit, return the states as they are - fstates = [] - for state in entity_history: - try: - fstate = _parse_float(state.state) - except (ValueError, TypeError): # TypeError to guard for NULL state in DB - continue - fstates.append((fstate, state)) + # in a unit which can be converted, return the states as they are - if fstates: - all_units = _get_units(fstates) - if len(all_units) > 1: - if WARN_UNSTABLE_UNIT not in hass.data: - hass.data[WARN_UNSTABLE_UNIT] = set() - if entity_id not in hass.data[WARN_UNSTABLE_UNIT]: - hass.data[WARN_UNSTABLE_UNIT].add(entity_id) - extra = "" - if old_metadata: - extra = ( - " and matches the unit of already compiled statistics " - f"({old_metadata['unit_of_measurement']})" - ) - _LOGGER.warning( - "The unit of %s is changing, got multiple %s, generation of long term " - "statistics will be suppressed unless the unit is stable%s. " - "Go to %s to fix this", - entity_id, - all_units, - extra, - LINK_DEV_STATISTICS, + all_units = _get_units(fstates) + if len(all_units) > 1: + if WARN_UNSTABLE_UNIT not in hass.data: + hass.data[WARN_UNSTABLE_UNIT] = set() + if entity_id not in hass.data[WARN_UNSTABLE_UNIT]: + hass.data[WARN_UNSTABLE_UNIT].add(entity_id) + extra = "" + if old_metadata: + extra = ( + " and matches the unit of already compiled statistics " + f"({old_metadata['unit_of_measurement']})" ) - return None, None, [] - state_unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) + _LOGGER.warning( + "The unit of %s is changing, got multiple %s, generation of long term " + "statistics will be suppressed unless the unit is stable%s. " + "Go to %s to fix this", + entity_id, + all_units, + extra, + LINK_DEV_STATISTICS, + ) + return None, None, [] + state_unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) return state_unit, state_unit, fstates - converter = UNIT_CONVERTERS[device_class] - fstates = [] + converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER[state_unit] + valid_fstates: list[tuple[float, State]] = [] statistics_unit: str | None = None if old_metadata: statistics_unit = old_metadata["unit_of_measurement"] - for state in entity_history: - try: - fstate = _parse_float(state.state) - except ValueError: - continue + for fstate, state in fstates: state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - # Exclude unsupported units from statistics + # Exclude states with unsupported unit from statistics if state_unit not in converter.VALID_UNITS: if WARN_UNSUPPORTED_UNIT not in hass.data: hass.data[WARN_UNSUPPORTED_UNIT] = set() if entity_id not in hass.data[WARN_UNSUPPORTED_UNIT]: hass.data[WARN_UNSUPPORTED_UNIT].add(entity_id) _LOGGER.warning( - "%s has unit %s which is unsupported for device_class %s", + "%s has unit %s which can't be converted to %s", entity_id, state_unit, - device_class, + statistics_unit, ) continue if statistics_unit is None: statistics_unit = state_unit - fstates.append( + valid_fstates.append( ( converter.convert( fstate, from_unit=state_unit, to_unit=statistics_unit @@ -240,7 +216,7 @@ def _normalize_states( ) ) - return statistics_unit, state_unit, fstates + return statistics_unit, state_unit, valid_fstates def _suggest_report_issue(hass: HomeAssistant, entity_id: str) -> str: @@ -427,14 +403,12 @@ def _compile_statistics( # noqa: C901 if entity_id not in history_list: continue - device_class = _state.attributes.get(ATTR_DEVICE_CLASS) entity_history = history_list[entity_id] statistics_unit, state_unit, fstates = _normalize_states( hass, session, old_metadatas, entity_history, - device_class, entity_id, ) @@ -467,11 +441,11 @@ def _compile_statistics( # noqa: C901 if entity_id not in hass.data[WARN_UNSTABLE_UNIT]: hass.data[WARN_UNSTABLE_UNIT].add(entity_id) _LOGGER.warning( - "The %sunit of %s (%s) does not match the unit of already " + "The unit of %s (%s) can not be converted to the unit of previously " "compiled statistics (%s). Generation of long term statistics " - "will be suppressed unless the unit changes back to %s. " + "will be suppressed unless the unit changes back to %s or a " + "compatible unit. " "Go to %s to fix this", - "normalized " if device_class in UNIT_CONVERTERS else "", entity_id, statistics_unit, old_metadata[1]["unit_of_measurement"], @@ -603,7 +577,6 @@ def list_statistic_ids( for state in entities: state_class = state.attributes[ATTR_STATE_CLASS] - device_class = state.attributes.get(ATTR_DEVICE_CLASS) state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) provided_statistics = DEFAULT_STATISTICS[state_class] @@ -620,21 +593,6 @@ def list_statistic_ids( ): continue - if device_class not in UNIT_CONVERTERS: - result[state.entity_id] = { - "has_mean": "mean" in provided_statistics, - "has_sum": "sum" in provided_statistics, - "name": None, - "source": RECORDER_DOMAIN, - "statistic_id": state.entity_id, - "unit_of_measurement": state_unit, - } - continue - - converter = UNIT_CONVERTERS[device_class] - if state_unit not in converter.VALID_UNITS: - continue - result[state.entity_id] = { "has_mean": "mean" in provided_statistics, "has_sum": "sum" in provided_statistics, @@ -643,6 +601,7 @@ def list_statistic_ids( "statistic_id": state.entity_id, "unit_of_measurement": state_unit, } + continue return result @@ -660,7 +619,6 @@ def validate_statistics( for state in sensor_states: entity_id = state.entity_id - device_class = state.attributes.get(ATTR_DEVICE_CLASS) state_class = state.attributes.get(ATTR_STATE_CLASS) state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -684,35 +642,30 @@ def validate_statistics( ) metadata_unit = metadata[1]["unit_of_measurement"] - if device_class not in UNIT_CONVERTERS: + converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER.get(metadata_unit) + if not converter: if state_unit != metadata_unit: - # The unit has changed - issue_type = ( - "units_changed_can_convert" - if statistics.can_convert_units(metadata_unit, state_unit) - else "units_changed" - ) + # The unit has changed, and it's not possible to convert validation_result[entity_id].append( statistics.ValidationIssue( - issue_type, + "units_changed", { "statistic_id": entity_id, "state_unit": state_unit, "metadata_unit": metadata_unit, + "supported_unit": metadata_unit, }, ) ) - elif metadata_unit not in UNIT_CONVERTERS[device_class].VALID_UNITS: - # The unit in metadata is not supported for this device class - valid_units = ", ".join( - sorted(UNIT_CONVERTERS[device_class].VALID_UNITS) - ) + elif state_unit not in converter.VALID_UNITS: + # The state unit can't be converted to the unit in metadata + valid_units = ", ".join(sorted(converter.VALID_UNITS)) validation_result[entity_id].append( statistics.ValidationIssue( - "unsupported_unit_metadata", + "units_changed", { "statistic_id": entity_id, - "device_class": device_class, + "state_unit": state_unit, "metadata_unit": metadata_unit, "supported_unit": valid_units, }, @@ -728,23 +681,6 @@ def validate_statistics( ) ) - if ( - state_class in STATE_CLASSES - and device_class in UNIT_CONVERTERS - and state_unit not in UNIT_CONVERTERS[device_class].VALID_UNITS - ): - # The unit in the state is not supported for this device class - validation_result[entity_id].append( - statistics.ValidationIssue( - "unsupported_unit_state", - { - "statistic_id": entity_id, - "device_class": device_class, - "state_unit": state_unit, - }, - ) - ) - for statistic_id in sensor_statistic_ids - sensor_entity_ids: # There is no sensor matching the statistics_id validation_result[statistic_id].append( diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 99aa3a3bf8e..8d9e34d005f 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -238,8 +238,8 @@ def test_compile_hourly_statistics_purged_state_changes( @pytest.mark.parametrize("attributes", [TEMPERATURE_SENSOR_ATTRIBUTES]) -def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes): - """Test compiling hourly statistics for unsupported sensor.""" +def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes): + """Test compiling hourly statistics for sensor with unit not matching device class.""" zero = dt_util.utcnow() hass = hass_recorder() setup_component(hass, "sensor", {}) @@ -286,6 +286,24 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes "statistics_unit_of_measurement": "°C", "unit_class": "temperature", }, + { + "has_mean": True, + "has_sum": False, + "name": None, + "source": "recorder", + "statistic_id": "sensor.test2", + "statistics_unit_of_measurement": "invalid", + "unit_class": None, + }, + { + "has_mean": True, + "has_sum": False, + "name": None, + "source": "recorder", + "statistic_id": "sensor.test3", + "statistics_unit_of_measurement": None, + "unit_class": None, + }, { "statistic_id": "sensor.test6", "has_mean": True, @@ -320,6 +338,32 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes "sum": None, } ], + "sensor.test2": [ + { + "statistic_id": "sensor.test2", + "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "mean": 13.05084745762712, + "min": -10.0, + "max": 30.0, + "last_reset": None, + "state": None, + "sum": None, + } + ], + "sensor.test3": [ + { + "statistic_id": "sensor.test3", + "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "mean": 13.05084745762712, + "min": -10.0, + "max": 30.0, + "last_reset": None, + "state": None, + "sum": None, + } + ], "sensor.test6": [ { "statistic_id": "sensor.test6", @@ -835,32 +879,44 @@ def test_compile_hourly_sum_statistics_nan_inf_state( @pytest.mark.parametrize( - "entity_id,warning_1,warning_2", + "entity_id, device_class, state_unit, display_unit, statistics_unit, unit_class, offset, warning_1, warning_2", [ ( "sensor.test1", + "energy", + "kWh", + "kWh", + "kWh", + "energy", + 0, "", "bug report at https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue", ), ( "sensor.power_consumption", + "power", + "W", + "W", + "W", + "power", + 15, "from integration demo ", "bug report at https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+demo%22", ), ( "sensor.custom_sensor", + "energy", + "kWh", + "kWh", + "kWh", + "energy", + 0, "from integration test ", "report it to the custom integration author", ), ], ) @pytest.mark.parametrize("state_class", ["total_increasing"]) -@pytest.mark.parametrize( - "device_class, state_unit, display_unit, statistics_unit, unit_class, factor", - [ - ("energy", "kWh", "kWh", "kWh", "energy", 1), - ], -) def test_compile_hourly_sum_statistics_negative_state( hass_recorder, caplog, @@ -873,7 +929,7 @@ def test_compile_hourly_sum_statistics_negative_state( display_unit, statistics_unit, unit_class, - factor, + offset, ): """Test compiling hourly statistics with negative states.""" zero = dt_util.utcnow() @@ -938,8 +994,8 @@ def test_compile_hourly_sum_statistics_negative_state( "mean": None, "min": None, "last_reset": None, - "state": approx(factor * seq[7]), - "sum": approx(factor * 15), # (15 - 10) + (10 - 0) + "state": approx(seq[7]), + "sum": approx(offset + 15), # (20 - 15) + (10 - 0) }, ] assert "Error while processing event StatisticsTask" not in caplog.text @@ -1889,7 +1945,7 @@ def test_compile_hourly_statistics_changing_units_1( do_adhoc_statistics(hass, start=zero) wait_recording_done(hass) - assert "does not match the unit of already compiled" not in caplog.text + assert "can not be converted to the unit of previously" not in caplog.text statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { @@ -1922,8 +1978,8 @@ def test_compile_hourly_statistics_changing_units_1( do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) wait_recording_done(hass) assert ( - "The unit of sensor.test1 (cats) does not match the unit of already compiled " - f"statistics ({display_unit})" in caplog.text + "The unit of sensor.test1 (cats) can not be converted to the unit of " + f"previously compiled statistics ({display_unit})" in caplog.text ) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ @@ -3039,18 +3095,30 @@ def record_states(hass, zero, entity_id, attributes, seq=None): @pytest.mark.parametrize( - "units, attributes, unit", + "units, attributes, unit, unit2, supported_unit", [ - (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), - (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), - (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F"), - (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C"), - (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "psi"), - (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "Pa"), + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "W, kW"), + (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "W, kW"), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F", "K", "K, °C, °F"), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C", "K", "K, °C, °F"), + ( + IMPERIAL_SYSTEM, + PRESSURE_SENSOR_ATTRIBUTES, + "psi", + "bar", + "Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi", + ), + ( + METRIC_SYSTEM, + PRESSURE_SENSOR_ATTRIBUTES, + "Pa", + "bar", + "Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi", + ), ], ) -async def test_validate_statistics_supported_device_class( - hass, hass_ws_client, recorder_mock, units, attributes, unit +async def test_validate_statistics_unit_change_device_class( + hass, hass_ws_client, recorder_mock, units, attributes, unit, unit2, supported_unit ): """Test validate_statistics.""" id = 1 @@ -3078,39 +3146,40 @@ async def test_validate_statistics_supported_device_class( # No statistics, no state - empty response await assert_validation_result(client, {}) - # No statistics, valid state - empty response + # No statistics, unit in state matching device class - empty response hass.states.async_set( "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit}} ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) - # No statistics, invalid state - expect error + # No statistics, unit in state not matching device class - empty response hass.states.async_set( "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}} ) await async_recorder_block_till_done(hass) - expected = { - "sensor.test": [ - { - "data": { - "device_class": attributes["device_class"], - "state_unit": "dogs", - "statistic_id": "sensor.test", - }, - "type": "unsupported_unit_state", - } - ], - } - await assert_validation_result(client, expected) + await assert_validation_result(client, {}) - # Statistics has run, invalid state - expect error + # Statistics has run, incompatible unit - expect error await async_recorder_block_till_done(hass) do_adhoc_statistics(hass, start=now) hass.states.async_set( "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} ) await async_recorder_block_till_done(hass) + expected = { + "sensor.test": [ + { + "data": { + "metadata_unit": unit, + "state_unit": "dogs", + "statistic_id": "sensor.test", + "supported_unit": supported_unit, + }, + "type": "units_changed", + } + ], + } await assert_validation_result(client, expected) # Valid state - empty response @@ -3125,6 +3194,18 @@ async def test_validate_statistics_supported_device_class( await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) + # Valid state in compatible unit - empty response + hass.states.async_set( + "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit2}} + ) + await async_recorder_block_till_done(hass) + await assert_validation_result(client, {}) + + # Valid state, statistic runs again - empty response + do_adhoc_statistics(hass, start=now) + await async_recorder_block_till_done(hass) + await assert_validation_result(client, {}) + # Remove the state - empty response hass.states.async_remove("sensor.test") expected = { @@ -3144,7 +3225,7 @@ async def test_validate_statistics_supported_device_class( (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W, kW"), ], ) -async def test_validate_statistics_supported_device_class_2( +async def test_validate_statistics_unit_change_device_class_2( hass, hass_ws_client, recorder_mock, units, attributes, valid_units ): """Test validate_statistics.""" @@ -3173,56 +3254,144 @@ async def test_validate_statistics_supported_device_class_2( # No statistics, no state - empty response await assert_validation_result(client, {}) - # No statistics, valid state - empty response - initial_attributes = {"state_class": "measurement"} + # No statistics, no device class - empty response + initial_attributes = {"state_class": "measurement", "unit_of_measurement": "dogs"} hass.states.async_set("sensor.test", 10, attributes=initial_attributes) await hass.async_block_till_done() await assert_validation_result(client, {}) - # Statistics has run, device class set - expect error + # Statistics has run, device class set not matching unit - empty response do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", 12, attributes=attributes) - await hass.async_block_till_done() - expected = { - "sensor.test": [ - { - "data": { - "device_class": attributes["device_class"], - "metadata_unit": None, - "statistic_id": "sensor.test", - "supported_unit": valid_units, - }, - "type": "unsupported_unit_metadata", - } - ], - } - await assert_validation_result(client, expected) - - # Invalid state too, expect double errors hass.states.async_set( - "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + ) + await hass.async_block_till_done() + await assert_validation_result(client, {}) + + +@pytest.mark.parametrize( + "units, attributes, unit, unit2, supported_unit", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "W, kW"), + (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "W, kW"), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F", "K", "K, °C, °F"), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C", "K", "K, °C, °F"), + ( + IMPERIAL_SYSTEM, + PRESSURE_SENSOR_ATTRIBUTES, + "psi", + "bar", + "Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi", + ), + ( + METRIC_SYSTEM, + PRESSURE_SENSOR_ATTRIBUTES, + "Pa", + "bar", + "Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi", + ), + ], +) +async def test_validate_statistics_unit_change_no_device_class( + hass, hass_ws_client, recorder_mock, units, attributes, unit, unit2, supported_unit +): + """Test validate_statistics.""" + id = 1 + attributes = dict(attributes) + attributes.pop("device_class") + + def next_id(): + nonlocal id + id += 1 + return id + + async def assert_validation_result(client, expected_result): + await client.send_json( + {"id": next_id(), "type": "recorder/validate_statistics"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected_result + + now = dt_util.utcnow() + + hass.config.units = units + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + client = await hass_ws_client() + + # No statistics, no state - empty response + await assert_validation_result(client, {}) + + # No statistics, unit in state matching device class - empty response + hass.states.async_set( + "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit}} + ) + await async_recorder_block_till_done(hass) + await assert_validation_result(client, {}) + + # No statistics, unit in state not matching device class - empty response + hass.states.async_set( + "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + ) + await async_recorder_block_till_done(hass) + await assert_validation_result(client, {}) + + # Statistics has run, incompatible unit - expect error + await async_recorder_block_till_done(hass) + do_adhoc_statistics(hass, start=now) + hass.states.async_set( + "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} ) await async_recorder_block_till_done(hass) expected = { "sensor.test": [ { "data": { - "device_class": attributes["device_class"], - "metadata_unit": None, - "statistic_id": "sensor.test", - "supported_unit": valid_units, - }, - "type": "unsupported_unit_metadata", - }, - { - "data": { - "device_class": attributes["device_class"], + "metadata_unit": unit, "state_unit": "dogs", "statistic_id": "sensor.test", + "supported_unit": supported_unit, }, - "type": "unsupported_unit_state", - }, + "type": "units_changed", + } + ], + } + await assert_validation_result(client, expected) + + # Valid state - empty response + hass.states.async_set( + "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit}} + ) + await async_recorder_block_till_done(hass) + await assert_validation_result(client, {}) + + # Valid state, statistic runs again - empty response + do_adhoc_statistics(hass, start=now) + await async_recorder_block_till_done(hass) + await assert_validation_result(client, {}) + + # Valid state in compatible unit - empty response + hass.states.async_set( + "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit2}} + ) + await async_recorder_block_till_done(hass) + await assert_validation_result(client, {}) + + # Valid state, statistic runs again - empty response + do_adhoc_statistics(hass, start=now) + await async_recorder_block_till_done(hass) + await assert_validation_result(client, {}) + + # Remove the state - empty response + hass.states.async_remove("sensor.test") + expected = { + "sensor.test": [ + { + "data": {"statistic_id": "sensor.test"}, + "type": "no_state", + } ], } await assert_validation_result(client, expected) @@ -3473,7 +3642,7 @@ async def test_validate_statistics_sensor_removed( "attributes", [BATTERY_SENSOR_ATTRIBUTES, NONE_SENSOR_ATTRIBUTES], ) -async def test_validate_statistics_unsupported_device_class( +async def test_validate_statistics_unit_change_no_conversion( hass, recorder_mock, hass_ws_client, attributes ): """Test validate_statistics.""" @@ -3553,6 +3722,7 @@ async def test_validate_statistics_unsupported_device_class( "metadata_unit": "dogs", "state_unit": attributes.get("unit_of_measurement"), "statistic_id": "sensor.test", + "supported_unit": "dogs", }, "type": "units_changed", } @@ -3573,124 +3743,7 @@ async def test_validate_statistics_unsupported_device_class( await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) - # Remove the state - empty response - hass.states.async_remove("sensor.test") - expected = { - "sensor.test": [ - { - "data": {"statistic_id": "sensor.test"}, - "type": "no_state", - } - ], - } - await assert_validation_result(client, expected) - - -@pytest.mark.parametrize( - "attributes", - [KW_SENSOR_ATTRIBUTES], -) -async def test_validate_statistics_unsupported_device_class_2( - hass, recorder_mock, hass_ws_client, attributes -): - """Test validate_statistics.""" - id = 1 - - def next_id(): - nonlocal id - id += 1 - return id - - async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == expected_result - - async def assert_statistic_ids(expected_result): - with session_scope(hass=hass) as session: - db_states = list(session.query(StatisticsMeta)) - assert len(db_states) == len(expected_result) - for i in range(len(db_states)): - assert db_states[i].statistic_id == expected_result[i]["statistic_id"] - assert ( - db_states[i].unit_of_measurement - == expected_result[i]["unit_of_measurement"] - ) - - now = dt_util.utcnow() - - await async_setup_component(hass, "sensor", {}) - await async_recorder_block_till_done(hass) - client = await hass_ws_client() - - # No statistics, no state - empty response - await assert_validation_result(client, {}) - - # No statistics, original unit - empty response - hass.states.async_set("sensor.test", 10, attributes=attributes) - await assert_validation_result(client, {}) - - # No statistics, changed unit - empty response - hass.states.async_set( - "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "W"}} - ) - await assert_validation_result(client, {}) - - # Run statistics, no statistics will be generated because of conflicting units - await async_recorder_block_till_done(hass) - do_adhoc_statistics(hass, start=now) - await async_recorder_block_till_done(hass) - await assert_statistic_ids([]) - - # No statistics, changed unit - empty response - hass.states.async_set( - "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "W"}} - ) - await assert_validation_result(client, {}) - - # Run statistics one hour later, only the "W" state will be considered - await async_recorder_block_till_done(hass) - do_adhoc_statistics(hass, start=now + timedelta(hours=1)) - await async_recorder_block_till_done(hass) - await assert_statistic_ids( - [{"statistic_id": "sensor.test", "unit_of_measurement": "W"}] - ) - await assert_validation_result(client, {}) - - # Change back to original unit - expect error - hass.states.async_set("sensor.test", 13, attributes=attributes) - await async_recorder_block_till_done(hass) - expected = { - "sensor.test": [ - { - "data": { - "metadata_unit": "W", - "state_unit": "kW", - "statistic_id": "sensor.test", - }, - "type": "units_changed_can_convert", - } - ], - } - await assert_validation_result(client, expected) - - # Changed unit - empty response - hass.states.async_set( - "sensor.test", 14, attributes={**attributes, **{"unit_of_measurement": "W"}} - ) - await async_recorder_block_till_done(hass) - await assert_validation_result(client, {}) - - # Valid state, statistic runs again - empty response - await async_recorder_block_till_done(hass) - do_adhoc_statistics(hass, start=now) - await async_recorder_block_till_done(hass) - await assert_validation_result(client, {}) - - # Remove the state - empty response + # Remove the state - expect error hass.states.async_remove("sensor.test") expected = { "sensor.test": [ From a3989b90fec1d09463f2b26c4622978ed6e86e69 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Oct 2022 16:44:54 -1000 Subject: [PATCH 147/985] Bump dbus-fast to 1.23.0 (#79570) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 1cb01a7da63..3b6f5977157 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.22.0" + "dbus-fast==1.23.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index aeb65b379ba..e3dd8b504a5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.22.0 +dbus-fast==1.23.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index aec36632cb8..7bc44ebacf3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -543,7 +543,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.22.0 +dbus-fast==1.23.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 370db3cef7c..e1692d9380b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.22.0 +dbus-fast==1.23.0 # homeassistant.components.debugpy debugpy==1.6.3 From 8d3e3ee6e9ccc08950ca33ece4ea2b6eaa68844d Mon Sep 17 00:00:00 2001 From: MrAliFu Date: Mon, 3 Oct 2022 23:36:06 -0500 Subject: [PATCH 148/985] Add new Islamic prayer times calculation method (#79278) * Adding new calculation method Adding calculation method Turkey. islamic_prayer_times 0.0.6 already have turkey as a calc_method, bringing that into here. * Update const.py Updated with the feedback * Importing PrayerTimesCalculator * Update const.py --- homeassistant/components/islamic_prayer_times/const.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/islamic_prayer_times/const.py b/homeassistant/components/islamic_prayer_times/const.py index 86f953cc856..e037f486aaa 100644 --- a/homeassistant/components/islamic_prayer_times/const.py +++ b/homeassistant/components/islamic_prayer_times/const.py @@ -1,4 +1,6 @@ """Constants for the Islamic Prayer component.""" +from prayer_times_calculator import PrayerTimesCalculator + DOMAIN = "islamic_prayer_times" NAME = "Islamic Prayer Times" PRAYER_TIMES_ICON = "mdi:calendar-clock" @@ -15,7 +17,7 @@ SENSOR_TYPES = { CONF_CALC_METHOD = "calculation_method" -CALC_METHODS = ["isna", "karachi", "mwl", "makkah", "moonsighting"] +CALC_METHODS: list[str] = list(PrayerTimesCalculator.CALCULATION_METHODS) DEFAULT_CALC_METHOD = "isna" DATA_UPDATED = "Islamic_prayer_data_updated" From 78f64ac3af573451daa9a07ca840544b360aa56f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Oct 2022 09:30:53 +0200 Subject: [PATCH 149/985] Bump actions/cache from 3.0.9 to 3.0.10 (#79574) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1071c23b4e5..0c3bbe21afd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -177,7 +177,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.9 + uses: actions/cache@v3.0.10 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -190,7 +190,7 @@ jobs: pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.9 + uses: actions/cache@v3.0.10 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -216,7 +216,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.9 + uses: actions/cache@v3.0.10 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -227,7 +227,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.9 + uses: actions/cache@v3.0.10 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -265,7 +265,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.9 + uses: actions/cache@v3.0.10 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -276,7 +276,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.9 + uses: actions/cache@v3.0.10 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -317,7 +317,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.9 + uses: actions/cache@v3.0.10 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -328,7 +328,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.9 + uses: actions/cache@v3.0.10 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -358,7 +358,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.9 + uses: actions/cache@v3.0.10 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -369,7 +369,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.9 + uses: actions/cache@v3.0.10 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -485,7 +485,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.9 + uses: actions/cache@v3.0.10 with: path: venv key: >- @@ -493,7 +493,7 @@ jobs: needs.info.outputs.python_cache_key }} - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.9 + uses: actions/cache@v3.0.10 with: path: ${{ env.PIP_CACHE }} key: >- @@ -543,7 +543,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.9 + uses: actions/cache@v3.0.10 with: path: venv key: >- @@ -575,7 +575,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.9 + uses: actions/cache@v3.0.10 with: path: venv key: >- @@ -608,7 +608,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.9 + uses: actions/cache@v3.0.10 with: path: venv key: >- @@ -652,7 +652,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.9 + uses: actions/cache@v3.0.10 with: path: venv key: >- @@ -700,7 +700,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.9 + uses: actions/cache@v3.0.10 with: path: venv key: >- @@ -754,7 +754,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.9 + uses: actions/cache@v3.0.10 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ From c040a7a15254794c45b23f788c034f5b30de2a25 Mon Sep 17 00:00:00 2001 From: kpine Date: Tue, 4 Oct 2022 02:54:13 -0700 Subject: [PATCH 150/985] Set zwave_js climate entity target temp attributes based on current mode (#79575) * Report temperature correctly * DRY * Add test assertions * Don't catch TypeError (revert) --- homeassistant/components/zwave_js/climate.py | 62 +++++++++++++------- tests/components/zwave_js/test_climate.py | 5 ++ 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 07119d365e0..e2bd69a1436 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -201,13 +201,25 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): if self._fan_mode: self._attr_supported_features |= ClimateEntityFeature.FAN_MODE - def _setpoint_value(self, setpoint_type: ThermostatSetpointType) -> ZwaveValue: - """Optionally return a ZwaveValue for a setpoint.""" + def _setpoint_value_or_raise( + self, setpoint_type: ThermostatSetpointType + ) -> ZwaveValue: + """Return a ZwaveValue for a setpoint or raise if not available.""" if (val := self._setpoint_values[setpoint_type]) is None: raise ValueError("Value requested is not available") return val + def _setpoint_temperature( + self, setpoint_type: ThermostatSetpointType + ) -> float | None: + """Optionally return the temperature value of a setpoint.""" + try: + temp = self._setpoint_value_or_raise(setpoint_type) + except (IndexError, ValueError): + return None + return get_value_of_zwave_value(temp) + def _set_modes_and_presets(self) -> None: """Convert Z-Wave Thermostat modes into Home Assistant modes and presets.""" all_modes: dict[HVACMode, int | None] = {} @@ -290,36 +302,44 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" - if self._current_mode and self._current_mode.value is None: + if ( + self._current_mode and self._current_mode.value is None + ) or not self._current_mode_setpoint_enums: # guard missing value return None - try: - temp = self._setpoint_value(self._current_mode_setpoint_enums[0]) - except (IndexError, ValueError): + if len(self._current_mode_setpoint_enums) > 1: + # current mode has a temperature range return None - return get_value_of_zwave_value(temp) + + return self._setpoint_temperature(self._current_mode_setpoint_enums[0]) @property def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" - if self._current_mode and self._current_mode.value is None: + if ( + self._current_mode and self._current_mode.value is None + ) or not self._current_mode_setpoint_enums: # guard missing value return None - try: - temp = self._setpoint_value(self._current_mode_setpoint_enums[1]) - except (IndexError, ValueError): + if len(self._current_mode_setpoint_enums) < 2: + # current mode has a single temperature return None - return get_value_of_zwave_value(temp) + + return self._setpoint_temperature(self._current_mode_setpoint_enums[1]) @property def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" - if self._current_mode and self._current_mode.value is None: + if ( + self._current_mode and self._current_mode.value is None + ) or not self._current_mode_setpoint_enums: # guard missing value return None - if len(self._current_mode_setpoint_enums) > 1: - return self.target_temperature - return None + if len(self._current_mode_setpoint_enums) < 2: + # current mode has a single temperature + return None + + return self._setpoint_temperature(self._current_mode_setpoint_enums[0]) @property def preset_mode(self) -> str | None: @@ -380,7 +400,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): min_temp = DEFAULT_MIN_TEMP base_unit = TEMP_CELSIUS try: - temp = self._setpoint_value(self._current_mode_setpoint_enums[0]) + temp = self._setpoint_value_or_raise(self._current_mode_setpoint_enums[0]) if temp.metadata.min: min_temp = temp.metadata.min base_unit = self.temperature_unit @@ -396,7 +416,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): max_temp = DEFAULT_MAX_TEMP base_unit = TEMP_CELSIUS try: - temp = self._setpoint_value(self._current_mode_setpoint_enums[0]) + temp = self._setpoint_value_or_raise(self._current_mode_setpoint_enums[0]) if temp.metadata.max: max_temp = temp.metadata.max base_unit = self.temperature_unit @@ -431,17 +451,17 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): if hvac_mode is not None: await self.async_set_hvac_mode(hvac_mode) if len(self._current_mode_setpoint_enums) == 1: - setpoint: ZwaveValue = self._setpoint_value( + setpoint: ZwaveValue = self._setpoint_value_or_raise( self._current_mode_setpoint_enums[0] ) target_temp: float | None = kwargs.get(ATTR_TEMPERATURE) if target_temp is not None: await self.info.node.async_set_value(setpoint, target_temp) elif len(self._current_mode_setpoint_enums) == 2: - setpoint_low: ZwaveValue = self._setpoint_value( + setpoint_low: ZwaveValue = self._setpoint_value_or_raise( self._current_mode_setpoint_enums[0] ) - setpoint_high: ZwaveValue = self._setpoint_value( + setpoint_high: ZwaveValue = self._setpoint_value_or_raise( self._current_mode_setpoint_enums[1] ) target_temp_low: float | None = kwargs.get(ATTR_TARGET_TEMP_LOW) diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index 755423e5e43..62dfacc7549 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -65,6 +65,8 @@ async def test_thermostat_v2( assert state.attributes[ATTR_CURRENT_HUMIDITY] == 30 assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.2 assert state.attributes[ATTR_TEMPERATURE] == 22.2 + assert state.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert state.attributes[ATTR_TARGET_TEMP_LOW] is None assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE assert state.attributes[ATTR_FAN_MODE] == "Auto low" assert state.attributes[ATTR_FAN_STATE] == "Idle / off" @@ -159,6 +161,8 @@ async def test_thermostat_v2( state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY) assert state.state == HVACMode.COOL assert state.attributes[ATTR_TEMPERATURE] == 22.8 + assert state.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert state.attributes[ATTR_TARGET_TEMP_LOW] is None # Test heat_cool mode update from value updated event event = Event( @@ -182,6 +186,7 @@ async def test_thermostat_v2( state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY) assert state.state == HVACMode.HEAT_COOL + assert state.attributes[ATTR_TEMPERATURE] is None assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 22.8 assert state.attributes[ATTR_TARGET_TEMP_LOW] == 22.2 From 4a6d1fc7343283d8d2ad004abdf933d498fe4f43 Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Tue, 4 Oct 2022 23:12:54 +1300 Subject: [PATCH 151/985] Fix typo in .strict-typing (#79584) --- .strict-typing | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.strict-typing b/.strict-typing index 303f732018c..2390ab8373d 100644 --- a/.strict-typing +++ b/.strict-typing @@ -2,7 +2,7 @@ # If component is fully covered with type annotations, please add it here # to enable strict mypy checks. -# Stict typing is enabled by default for core files. +# Strict typing is enabled by default for core files. # Add it here to add 'disallow_any_generics'. # --- Only for core file! --- homeassistant.exceptions From 9d2ba7c0080a4a2a498fa8117a46ff78153f3af2 Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Tue, 4 Oct 2022 23:13:40 +1300 Subject: [PATCH 152/985] Use constant in fitbit messages (#79586) Use FITBIT_CONFIG_FILE constant in configurator messages and buttons --- homeassistant/components/fitbit/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 3165843a23d..767d550809e 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -90,7 +90,7 @@ def request_app_setup( if os.path.isfile(config_path): config_file = load_json(config_path) if config_file == DEFAULT_CONFIG: - error_msg = "You didn't correctly modify fitbit.conf, please try again." + error_msg = f"You didn't correctly modify {FITBIT_CONFIG_FILE}, please try again." configurator.notify_errors(hass, _CONFIGURING["fitbit"], error_msg) else: @@ -115,7 +115,7 @@ def request_app_setup( ) return - submit = "I have saved my Client ID and Client Secret into fitbit.conf." + submit = f"I have saved my Client ID and Client Secret into {FITBIT_CONFIG_FILE}." _CONFIGURING["fitbit"] = configurator.request_config( hass, From 1907b8766601ecd0ee60e18a86eae0409b13f1bd Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Wed, 5 Oct 2022 01:28:00 +1300 Subject: [PATCH 153/985] Add unique ID to fitbit (#79587) * Set unique ID for fitbit sensors, including the user ID * Remove fitbit_ from unique ids (see: https://developers.home-assistant.io/docs/entity_registry_index/#unique-id) * change fitbit user_profile type to dict[str, Any] * Fitbit: define a default unique ID, and add battery info if present * No need for trailing _battery in unique ID, since it already contains "devices/battery_" --- homeassistant/components/fitbit/sensor.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 767d550809e..f9dc74fc328 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -195,8 +195,9 @@ def setup_platform( if int(time.time()) - expires_at > 3600: authd_client.client.refresh_token() + user_profile = authd_client.user_profile_get()["user"] if (unit_system := config[CONF_UNIT_SYSTEM]) == "default": - authd_client.system = authd_client.user_profile_get()["user"]["locale"] + authd_client.system = user_profile["locale"] if authd_client.system != "en_GB": if hass.config.units.is_metric: authd_client.system = "metric" @@ -211,6 +212,7 @@ def setup_platform( entities = [ FitbitSensor( authd_client, + user_profile, config_path, description, hass.config.units.is_metric, @@ -224,6 +226,7 @@ def setup_platform( [ FitbitSensor( authd_client, + user_profile, config_path, FITBIT_RESOURCE_BATTERY, hass.config.units.is_metric, @@ -345,6 +348,7 @@ class FitbitSensor(SensorEntity): def __init__( self, client: Fitbit, + user_profile: dict[str, Any], config_path: str, description: FitbitSensorEntityDescription, is_metric: bool, @@ -358,8 +362,12 @@ class FitbitSensor(SensorEntity): self.is_metric = is_metric self.clock_format = clock_format self.extra = extra + + self._attr_unique_id = f"{user_profile['encodedId']}_{description.key}" if self.extra is not None: self._attr_name = f"{self.extra.get('deviceVersion')} Battery" + self._attr_unique_id = f"{self._attr_unique_id}_{self.extra.get('id')}" + if (unit_type := description.unit_type) == "": split_resource = description.key.rsplit("/", maxsplit=1)[-1] try: From d0f1cba4ea04dde04db16f10827f4ce2bc89201a Mon Sep 17 00:00:00 2001 From: Jafar Atili Date: Tue, 4 Oct 2022 15:47:30 +0300 Subject: [PATCH 154/985] Fix Thermostat not showing up in SwitchBee integration (#79592) Fixed Thermostat not showing up in SwitchBee --- homeassistant/components/switchbee/coordinator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/switchbee/coordinator.py b/homeassistant/components/switchbee/coordinator.py index f7101cd5990..3dee30bac0e 100644 --- a/homeassistant/components/switchbee/coordinator.py +++ b/homeassistant/components/switchbee/coordinator.py @@ -64,6 +64,7 @@ class SwitchBeeCoordinator(DataUpdateCoordinator[Mapping[int, SwitchBeeBaseDevic DeviceType.Dimmer, DeviceType.Shutter, DeviceType.Somfy, + DeviceType.Thermostat, ] ) except SwitchBeeError as exp: From 2fd62b571d03d27dede97a95dc27374e10b1d315 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 4 Oct 2022 14:47:57 +0200 Subject: [PATCH 155/985] Add docstring to US volume constants (#79582) * Add docstring to US volume constants * A blank line separation --- homeassistant/const.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3c0b2a4051c..ad81eb8c692 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -559,7 +559,10 @@ VOLUME_CUBIC_METERS: Final = "m³" VOLUME_CUBIC_FEET: Final = "ft³" VOLUME_GALLONS: Final = "gal" +"""US gallon (British gallon is not yet supported)""" + VOLUME_FLUID_OUNCE: Final = "fl. oz." +"""US fluid ounce (British fluid ounce is not yet supported)""" # Volume Flow Rate units VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR: Final = "m³/h" From 74a8472eed41fe8ff7277dccf0857defa05e5f8a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 4 Oct 2022 15:24:55 +0200 Subject: [PATCH 156/985] Collect all brands (#79579) --- homeassistant/brands/amazon.json | 5 + homeassistant/brands/apple.json | 7 +- homeassistant/brands/aruba.json | 5 + homeassistant/brands/asterisk.json | 5 + homeassistant/brands/august.json | 5 + homeassistant/brands/cisco.json | 5 + homeassistant/brands/clicksend.json | 5 + homeassistant/brands/devolo.json | 5 + homeassistant/brands/dlna.json | 5 + homeassistant/brands/elgato.json | 5 + homeassistant/brands/emoncms.json | 5 + homeassistant/brands/epson.json | 5 + homeassistant/brands/eq3.json | 5 + homeassistant/brands/ffmpeg.json | 5 + homeassistant/brands/geonet.json | 5 + homeassistant/brands/globalcache.json | 5 + homeassistant/brands/hikvision.json | 5 + homeassistant/brands/homematic.json | 5 + homeassistant/brands/honeywell.json | 5 + homeassistant/brands/ibm.json | 5 + homeassistant/brands/lg.json | 5 + homeassistant/brands/logitech.json | 5 + homeassistant/brands/lutron.json | 5 + homeassistant/brands/melnor.json | 5 + homeassistant/brands/microsoft.json | 16 + homeassistant/brands/mqtt.json | 12 + homeassistant/brands/netgear.json | 5 + homeassistant/brands/openwrt.json | 5 + homeassistant/brands/panasonic.json | 5 + homeassistant/brands/philips.json | 5 + homeassistant/brands/qnap.json | 5 + homeassistant/brands/raspberry.json | 5 + homeassistant/brands/russound.json | 5 + homeassistant/brands/samsung.json | 5 + homeassistant/brands/solaredge.json | 5 + homeassistant/brands/sony.json | 5 + homeassistant/brands/synology.json | 5 + homeassistant/brands/telegram.json | 5 + homeassistant/brands/telldus.json | 5 + homeassistant/brands/tesla.json | 5 + homeassistant/brands/trafikverket.json | 9 + homeassistant/brands/twilio.json | 5 + homeassistant/brands/vlc.json | 5 + homeassistant/brands/xiaomi.json | 11 + homeassistant/brands/yale.json | 5 + homeassistant/brands/yandex.json | 5 + homeassistant/brands/yeelight.json | 5 + homeassistant/generated/integrations.json | 1366 ++++++++++++--------- 48 files changed, 1065 insertions(+), 566 deletions(-) create mode 100644 homeassistant/brands/amazon.json create mode 100644 homeassistant/brands/aruba.json create mode 100644 homeassistant/brands/asterisk.json create mode 100644 homeassistant/brands/august.json create mode 100644 homeassistant/brands/cisco.json create mode 100644 homeassistant/brands/clicksend.json create mode 100644 homeassistant/brands/devolo.json create mode 100644 homeassistant/brands/dlna.json create mode 100644 homeassistant/brands/elgato.json create mode 100644 homeassistant/brands/emoncms.json create mode 100644 homeassistant/brands/epson.json create mode 100644 homeassistant/brands/eq3.json create mode 100644 homeassistant/brands/ffmpeg.json create mode 100644 homeassistant/brands/geonet.json create mode 100644 homeassistant/brands/globalcache.json create mode 100644 homeassistant/brands/hikvision.json create mode 100644 homeassistant/brands/homematic.json create mode 100644 homeassistant/brands/honeywell.json create mode 100644 homeassistant/brands/ibm.json create mode 100644 homeassistant/brands/lg.json create mode 100644 homeassistant/brands/logitech.json create mode 100644 homeassistant/brands/lutron.json create mode 100644 homeassistant/brands/melnor.json create mode 100644 homeassistant/brands/microsoft.json create mode 100644 homeassistant/brands/mqtt.json create mode 100644 homeassistant/brands/netgear.json create mode 100644 homeassistant/brands/openwrt.json create mode 100644 homeassistant/brands/panasonic.json create mode 100644 homeassistant/brands/philips.json create mode 100644 homeassistant/brands/qnap.json create mode 100644 homeassistant/brands/raspberry.json create mode 100644 homeassistant/brands/russound.json create mode 100644 homeassistant/brands/samsung.json create mode 100644 homeassistant/brands/solaredge.json create mode 100644 homeassistant/brands/sony.json create mode 100644 homeassistant/brands/synology.json create mode 100644 homeassistant/brands/telegram.json create mode 100644 homeassistant/brands/telldus.json create mode 100644 homeassistant/brands/tesla.json create mode 100644 homeassistant/brands/trafikverket.json create mode 100644 homeassistant/brands/twilio.json create mode 100644 homeassistant/brands/vlc.json create mode 100644 homeassistant/brands/xiaomi.json create mode 100644 homeassistant/brands/yale.json create mode 100644 homeassistant/brands/yandex.json create mode 100644 homeassistant/brands/yeelight.json diff --git a/homeassistant/brands/amazon.json b/homeassistant/brands/amazon.json new file mode 100644 index 00000000000..e31bb410457 --- /dev/null +++ b/homeassistant/brands/amazon.json @@ -0,0 +1,5 @@ +{ + "domain": "amazon", + "name": "Amazon", + "integrations": ["alexa", "amazon_polly", "aws", "route53"] +} diff --git a/homeassistant/brands/apple.json b/homeassistant/brands/apple.json index 1a782b50900..00f646e435e 100644 --- a/homeassistant/brands/apple.json +++ b/homeassistant/brands/apple.json @@ -2,10 +2,11 @@ "domain": "apple", "name": "Apple", "integrations": [ - "icloud", - "ibeacon", "apple_tv", + "homekit_controller", "homekit", - "homekit_controller" + "ibeacon", + "icloud", + "itunes" ] } diff --git a/homeassistant/brands/aruba.json b/homeassistant/brands/aruba.json new file mode 100644 index 00000000000..512192813e4 --- /dev/null +++ b/homeassistant/brands/aruba.json @@ -0,0 +1,5 @@ +{ + "domain": "aruba", + "name": "Aruba", + "integrations": ["aruba", "cppm_tracker"] +} diff --git a/homeassistant/brands/asterisk.json b/homeassistant/brands/asterisk.json new file mode 100644 index 00000000000..1df3e660afe --- /dev/null +++ b/homeassistant/brands/asterisk.json @@ -0,0 +1,5 @@ +{ + "domain": "asterisk", + "name": "Asterisk", + "integrations": ["asterisk_cdr", "asterisk_mbox"] +} diff --git a/homeassistant/brands/august.json b/homeassistant/brands/august.json new file mode 100644 index 00000000000..ce2f18dc759 --- /dev/null +++ b/homeassistant/brands/august.json @@ -0,0 +1,5 @@ +{ + "domain": "august", + "name": "August Home", + "integrations": ["august", "yalexs_ble"] +} diff --git a/homeassistant/brands/cisco.json b/homeassistant/brands/cisco.json new file mode 100644 index 00000000000..a1885b1af5e --- /dev/null +++ b/homeassistant/brands/cisco.json @@ -0,0 +1,5 @@ +{ + "domain": "cisco", + "name": "Cisco", + "integrations": ["cisco_ios", "cisco_mobility_express", "cisco_webex_teams"] +} diff --git a/homeassistant/brands/clicksend.json b/homeassistant/brands/clicksend.json new file mode 100644 index 00000000000..07de60a99e3 --- /dev/null +++ b/homeassistant/brands/clicksend.json @@ -0,0 +1,5 @@ +{ + "domain": "clicksend", + "name": "ClickSend", + "integrations": ["clicksend", "clicksend_tts"] +} diff --git a/homeassistant/brands/devolo.json b/homeassistant/brands/devolo.json new file mode 100644 index 00000000000..86dc7a3b100 --- /dev/null +++ b/homeassistant/brands/devolo.json @@ -0,0 +1,5 @@ +{ + "domain": "devolo", + "name": "devolo", + "integrations": ["devolo_home_control", "devolo_home_network"] +} diff --git a/homeassistant/brands/dlna.json b/homeassistant/brands/dlna.json new file mode 100644 index 00000000000..f6a648d6895 --- /dev/null +++ b/homeassistant/brands/dlna.json @@ -0,0 +1,5 @@ +{ + "domain": "dlna", + "name": "DLNA", + "integrations": ["dlna_dmr", "dlna_dms"] +} diff --git a/homeassistant/brands/elgato.json b/homeassistant/brands/elgato.json new file mode 100644 index 00000000000..3ca7e07c1bb --- /dev/null +++ b/homeassistant/brands/elgato.json @@ -0,0 +1,5 @@ +{ + "domain": "elgato", + "name": "Elgato", + "integrations": ["avea", "elgato"] +} diff --git a/homeassistant/brands/emoncms.json b/homeassistant/brands/emoncms.json new file mode 100644 index 00000000000..866c7ff18f3 --- /dev/null +++ b/homeassistant/brands/emoncms.json @@ -0,0 +1,5 @@ +{ + "domain": "emoncms", + "name": "emoncms", + "integrations": ["emoncms", "emoncms_history"] +} diff --git a/homeassistant/brands/epson.json b/homeassistant/brands/epson.json new file mode 100644 index 00000000000..80d5db942a2 --- /dev/null +++ b/homeassistant/brands/epson.json @@ -0,0 +1,5 @@ +{ + "domain": "epson", + "name": "Epson", + "integrations": ["epson", "epsonworkforce"] +} diff --git a/homeassistant/brands/eq3.json b/homeassistant/brands/eq3.json new file mode 100644 index 00000000000..4052afac277 --- /dev/null +++ b/homeassistant/brands/eq3.json @@ -0,0 +1,5 @@ +{ + "domain": "eq3", + "name": "eQ-3", + "integrations": ["eq3btsmart", "maxcube"] +} diff --git a/homeassistant/brands/ffmpeg.json b/homeassistant/brands/ffmpeg.json new file mode 100644 index 00000000000..2ec1de4ec03 --- /dev/null +++ b/homeassistant/brands/ffmpeg.json @@ -0,0 +1,5 @@ +{ + "domain": "ffmpeg", + "name": "FFmpeg", + "integrations": ["ffmpeg", "ffmpeg_motion", "ffmpeg_noise"] +} diff --git a/homeassistant/brands/geonet.json b/homeassistant/brands/geonet.json new file mode 100644 index 00000000000..4f09d607f80 --- /dev/null +++ b/homeassistant/brands/geonet.json @@ -0,0 +1,5 @@ +{ + "domain": "geonet", + "name": "GeoNet", + "integrations": ["geonetnz_quakes", "geonetnz_volcano"] +} diff --git a/homeassistant/brands/globalcache.json b/homeassistant/brands/globalcache.json new file mode 100644 index 00000000000..0cba9d65d0d --- /dev/null +++ b/homeassistant/brands/globalcache.json @@ -0,0 +1,5 @@ +{ + "domain": "globalcache", + "name": "Global Caché", + "integrations": ["gc100", "itach"] +} diff --git a/homeassistant/brands/hikvision.json b/homeassistant/brands/hikvision.json new file mode 100644 index 00000000000..b09770bccc5 --- /dev/null +++ b/homeassistant/brands/hikvision.json @@ -0,0 +1,5 @@ +{ + "domain": "hikvision", + "name": "Hikvision", + "integrations": ["hikvision", "hikvisioncam"] +} diff --git a/homeassistant/brands/homematic.json b/homeassistant/brands/homematic.json new file mode 100644 index 00000000000..e7f29c19d67 --- /dev/null +++ b/homeassistant/brands/homematic.json @@ -0,0 +1,5 @@ +{ + "domain": "homematic", + "name": "Homematic", + "integrations": ["homematic", "homematicip_cloud"] +} diff --git a/homeassistant/brands/honeywell.json b/homeassistant/brands/honeywell.json new file mode 100644 index 00000000000..37cd6d8ce73 --- /dev/null +++ b/homeassistant/brands/honeywell.json @@ -0,0 +1,5 @@ +{ + "domain": "honeywell", + "name": "Honeywell", + "integrations": ["lyric", "evohome", "honeywell"] +} diff --git a/homeassistant/brands/ibm.json b/homeassistant/brands/ibm.json new file mode 100644 index 00000000000..42367e899e7 --- /dev/null +++ b/homeassistant/brands/ibm.json @@ -0,0 +1,5 @@ +{ + "domain": "ibm", + "name": "IBM", + "integrations": ["watson_iot", "watson_tts"] +} diff --git a/homeassistant/brands/lg.json b/homeassistant/brands/lg.json new file mode 100644 index 00000000000..350db80b5f3 --- /dev/null +++ b/homeassistant/brands/lg.json @@ -0,0 +1,5 @@ +{ + "domain": "lg", + "name": "LG", + "integrations": ["lg_netcast", "lg_soundbar", "webostv"] +} diff --git a/homeassistant/brands/logitech.json b/homeassistant/brands/logitech.json new file mode 100644 index 00000000000..d4a0dd1bb87 --- /dev/null +++ b/homeassistant/brands/logitech.json @@ -0,0 +1,5 @@ +{ + "domain": "logitech", + "name": "Logitech", + "integrations": ["harmony", "ue_smart_radio", "squeezebox"] +} diff --git a/homeassistant/brands/lutron.json b/homeassistant/brands/lutron.json new file mode 100644 index 00000000000..b891065d819 --- /dev/null +++ b/homeassistant/brands/lutron.json @@ -0,0 +1,5 @@ +{ + "domain": "lutron", + "name": "Lutron", + "integrations": ["lutron", "lutron_caseta", "homeworks"] +} diff --git a/homeassistant/brands/melnor.json b/homeassistant/brands/melnor.json new file mode 100644 index 00000000000..c04db5c4e7c --- /dev/null +++ b/homeassistant/brands/melnor.json @@ -0,0 +1,5 @@ +{ + "domain": "melnor", + "name": "Melnor", + "integrations": ["melnor", "raincloud"] +} diff --git a/homeassistant/brands/microsoft.json b/homeassistant/brands/microsoft.json new file mode 100644 index 00000000000..d28932082a6 --- /dev/null +++ b/homeassistant/brands/microsoft.json @@ -0,0 +1,16 @@ +{ + "domain": "microsoft", + "name": "Microsoft", + "integrations": [ + "azure_devops", + "azure_event_hub", + "azure_service_bus", + "microsoft_face_detect", + "microsoft_face_identify", + "microsoft_face", + "microsoft", + "msteams", + "xbox", + "xbox_live" + ] +} diff --git a/homeassistant/brands/mqtt.json b/homeassistant/brands/mqtt.json new file mode 100644 index 00000000000..c1d58521a7c --- /dev/null +++ b/homeassistant/brands/mqtt.json @@ -0,0 +1,12 @@ +{ + "domain": "mqtt", + "name": "MQTT", + "integrations": [ + "manual_mqtt", + "mqtt", + "mqtt_eventstream", + "mqtt_json", + "mqtt_room", + "mqtt_statestream" + ] +} diff --git a/homeassistant/brands/netgear.json b/homeassistant/brands/netgear.json new file mode 100644 index 00000000000..9a6b6e51da0 --- /dev/null +++ b/homeassistant/brands/netgear.json @@ -0,0 +1,5 @@ +{ + "domain": "netgear", + "name": "NETGEAR", + "integrations": ["netgear", "netgear_lte"] +} diff --git a/homeassistant/brands/openwrt.json b/homeassistant/brands/openwrt.json new file mode 100644 index 00000000000..ff9cd4ca250 --- /dev/null +++ b/homeassistant/brands/openwrt.json @@ -0,0 +1,5 @@ +{ + "domain": "openwrt", + "name": "OpenWrt", + "integrations": ["luci", "ubus"] +} diff --git a/homeassistant/brands/panasonic.json b/homeassistant/brands/panasonic.json new file mode 100644 index 00000000000..2d8f29a3968 --- /dev/null +++ b/homeassistant/brands/panasonic.json @@ -0,0 +1,5 @@ +{ + "domain": "panasonic", + "name": "Panasonic", + "integrations": ["panasonic_bluray", "panasonic_viera"] +} diff --git a/homeassistant/brands/philips.json b/homeassistant/brands/philips.json new file mode 100644 index 00000000000..bfd290eb945 --- /dev/null +++ b/homeassistant/brands/philips.json @@ -0,0 +1,5 @@ +{ + "domain": "philips", + "name": "Philips", + "integrations": ["dynalite", "hue", "philips_js"] +} diff --git a/homeassistant/brands/qnap.json b/homeassistant/brands/qnap.json new file mode 100644 index 00000000000..6464a0ec877 --- /dev/null +++ b/homeassistant/brands/qnap.json @@ -0,0 +1,5 @@ +{ + "domain": "qnap", + "name": "QNAP", + "integrations": ["qnap", "qnap_qsw"] +} diff --git a/homeassistant/brands/raspberry.json b/homeassistant/brands/raspberry.json new file mode 100644 index 00000000000..a0ec6f12699 --- /dev/null +++ b/homeassistant/brands/raspberry.json @@ -0,0 +1,5 @@ +{ + "domain": "raspberry_pi", + "name": "Raspberry Pi", + "integrations": ["rpi_camera", "rpi_power", "remote_rpi_gpio"] +} diff --git a/homeassistant/brands/russound.json b/homeassistant/brands/russound.json new file mode 100644 index 00000000000..70b3de109ca --- /dev/null +++ b/homeassistant/brands/russound.json @@ -0,0 +1,5 @@ +{ + "domain": "russound", + "name": "Russound", + "integrations": ["russound_rio", "russound_rnet"] +} diff --git a/homeassistant/brands/samsung.json b/homeassistant/brands/samsung.json new file mode 100644 index 00000000000..1d5f2522e9e --- /dev/null +++ b/homeassistant/brands/samsung.json @@ -0,0 +1,5 @@ +{ + "domain": "samsung", + "name": "Samsung", + "integrations": ["familyhub", "samsungtv", "syncthru"] +} diff --git a/homeassistant/brands/solaredge.json b/homeassistant/brands/solaredge.json new file mode 100644 index 00000000000..90190f9c786 --- /dev/null +++ b/homeassistant/brands/solaredge.json @@ -0,0 +1,5 @@ +{ + "domain": "solaredge", + "name": "SolarEdge", + "integrations": ["solaredge", "solaredge_local"] +} diff --git a/homeassistant/brands/sony.json b/homeassistant/brands/sony.json new file mode 100644 index 00000000000..e35d5f4723c --- /dev/null +++ b/homeassistant/brands/sony.json @@ -0,0 +1,5 @@ +{ + "domain": "sony", + "name": "Sony", + "integrations": ["braviatv", "ps4", "sony_projector", "songpal"] +} diff --git a/homeassistant/brands/synology.json b/homeassistant/brands/synology.json new file mode 100644 index 00000000000..0387fabffaf --- /dev/null +++ b/homeassistant/brands/synology.json @@ -0,0 +1,5 @@ +{ + "domain": "synology", + "name": "Synology", + "integrations": ["synology_chat", "synology_dsm", "synology_srm"] +} diff --git a/homeassistant/brands/telegram.json b/homeassistant/brands/telegram.json new file mode 100644 index 00000000000..8cb5e202190 --- /dev/null +++ b/homeassistant/brands/telegram.json @@ -0,0 +1,5 @@ +{ + "domain": "telegram", + "name": "Telegram", + "integrations": ["telegram", "telegram_bot"] +} diff --git a/homeassistant/brands/telldus.json b/homeassistant/brands/telldus.json new file mode 100644 index 00000000000..c280832f68e --- /dev/null +++ b/homeassistant/brands/telldus.json @@ -0,0 +1,5 @@ +{ + "domain": "telldus", + "name": "Telldus", + "integrations": ["tellduslive", "tellstick"] +} diff --git a/homeassistant/brands/tesla.json b/homeassistant/brands/tesla.json new file mode 100644 index 00000000000..aeec7982579 --- /dev/null +++ b/homeassistant/brands/tesla.json @@ -0,0 +1,5 @@ +{ + "domain": "tesla", + "name": "Tesla", + "integrations": ["powerwall", "tesla_wall_connector"] +} diff --git a/homeassistant/brands/trafikverket.json b/homeassistant/brands/trafikverket.json new file mode 100644 index 00000000000..df444cbeb60 --- /dev/null +++ b/homeassistant/brands/trafikverket.json @@ -0,0 +1,9 @@ +{ + "domain": "trafikverket", + "name": "Trafikverket", + "integrations": [ + "trafikverket_ferry", + "trafikverket_train", + "trafikverket_weatherstation" + ] +} diff --git a/homeassistant/brands/twilio.json b/homeassistant/brands/twilio.json new file mode 100644 index 00000000000..7ae9162059e --- /dev/null +++ b/homeassistant/brands/twilio.json @@ -0,0 +1,5 @@ +{ + "domain": "twilio", + "name": "Twilio", + "integrations": ["twilio", "twilio_call", "twilio_sms"] +} diff --git a/homeassistant/brands/vlc.json b/homeassistant/brands/vlc.json new file mode 100644 index 00000000000..66c004470d6 --- /dev/null +++ b/homeassistant/brands/vlc.json @@ -0,0 +1,5 @@ +{ + "domain": "vlc", + "name": "VideoLAN", + "integrations": ["vlc", "vlc_telnet"] +} diff --git a/homeassistant/brands/xiaomi.json b/homeassistant/brands/xiaomi.json new file mode 100644 index 00000000000..ebdc99d8c38 --- /dev/null +++ b/homeassistant/brands/xiaomi.json @@ -0,0 +1,11 @@ +{ + "domain": "xiaomi", + "name": "Xiaomi", + "integrations": [ + "xiaomi_aqara", + "xiaomi_ble", + "xiaomi_miio", + "xiaomi_tv", + "xiaomi" + ] +} diff --git a/homeassistant/brands/yale.json b/homeassistant/brands/yale.json new file mode 100644 index 00000000000..87c119fdd40 --- /dev/null +++ b/homeassistant/brands/yale.json @@ -0,0 +1,5 @@ +{ + "domain": "yale", + "name": "Yale", + "integrations": ["august", "yale_smart_alarm", "yalexs_ble"] +} diff --git a/homeassistant/brands/yandex.json b/homeassistant/brands/yandex.json new file mode 100644 index 00000000000..c4a55be8b5e --- /dev/null +++ b/homeassistant/brands/yandex.json @@ -0,0 +1,5 @@ +{ + "domain": "yandex", + "name": "Yandex", + "integrations": ["yandex_transport", "yandextts"] +} diff --git a/homeassistant/brands/yeelight.json b/homeassistant/brands/yeelight.json new file mode 100644 index 00000000000..1ce04a99214 --- /dev/null +++ b/homeassistant/brands/yeelight.json @@ -0,0 +1,5 @@ +{ + "domain": "yeelight", + "name": "Yeelight", + "integrations": ["yeelight", "yeelightsunflower"] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index eb5c1c8fefb..fe58a793c54 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -119,11 +119,6 @@ "iot_class": "local_push", "name": "Alert" }, - "alexa": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Amazon Alexa" - }, "almond": { "config_flow": true, "iot_class": "local_polling", @@ -134,10 +129,30 @@ "iot_class": "cloud_polling", "name": "Alpha Vantage" }, - "amazon_polly": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Amazon Polly" + "amazon": { + "name": "Amazon", + "integrations": { + "alexa": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Amazon Alexa" + }, + "amazon_polly": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Amazon Polly" + }, + "aws": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Amazon Web Services (AWS)" + }, + "route53": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "AWS Route53" + } + } }, "amberelectric": { "config_flow": true, @@ -202,29 +217,34 @@ "apple": { "name": "Apple", "integrations": { - "icloud": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Apple iCloud" - }, - "ibeacon": { - "config_flow": true, - "iot_class": "local_push", - "name": "iBeacon Tracker" - }, "apple_tv": { "config_flow": true, "iot_class": "local_push", "name": "Apple TV" }, + "homekit_controller": { + "config_flow": true, + "iot_class": "local_push" + }, "homekit": { "config_flow": true, "iot_class": "local_push", "name": "HomeKit" }, - "homekit_controller": { + "ibeacon": { "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "name": "iBeacon Tracker" + }, + "icloud": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Apple iCloud" + }, + "itunes": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Apple iTunes" } } }, @@ -268,9 +288,19 @@ "name": "Arris TG2492LG" }, "aruba": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Aruba" + "name": "Aruba", + "integrations": { + "aruba": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Aruba" + }, + "cppm_tracker": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Aruba ClearPass" + } + } }, "arwn": { "config_flow": false, @@ -282,15 +312,20 @@ "iot_class": "cloud_polling", "name": "Aseko Pool Live" }, - "asterisk_cdr": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Asterisk Call Detail Records" - }, - "asterisk_mbox": { - "config_flow": false, - "iot_class": "local_push", - "name": "Asterisk Voicemail" + "asterisk": { + "name": "Asterisk", + "integrations": { + "asterisk_cdr": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Asterisk Call Detail Records" + }, + "asterisk_mbox": { + "config_flow": false, + "iot_class": "local_push", + "name": "Asterisk Voicemail" + } + } }, "asuswrt": { "config_flow": true, @@ -313,9 +348,19 @@ "name": "Atome Linky" }, "august": { - "config_flow": true, - "iot_class": "cloud_push", - "name": "August" + "name": "August Home", + "integrations": { + "august": { + "config_flow": true, + "iot_class": "cloud_push", + "name": "August" + }, + "yalexs_ble": { + "config_flow": true, + "iot_class": "local_push", + "name": "Yale Access Bluetooth" + } + } }, "aurora": { "config_flow": true, @@ -340,11 +385,6 @@ "config_flow": false, "iot_class": null }, - "avea": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Elgato Avea" - }, "avion": { "config_flow": false, "iot_class": "assumed_state", @@ -355,31 +395,11 @@ "iot_class": "local_polling", "name": "Awair" }, - "aws": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Amazon Web Services (AWS)" - }, "axis": { "config_flow": true, "iot_class": "local_push", "name": "Axis" }, - "azure_devops": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Azure DevOps" - }, - "azure_event_hub": { - "config_flow": true, - "iot_class": "cloud_push", - "name": "Azure Event Hub" - }, - "azure_service_bus": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Azure Service Bus" - }, "backup": { "config_flow": false, "iot_class": "calculated", @@ -504,11 +524,6 @@ "iot_class": "local_push", "name": "Bosch SHC" }, - "braviatv": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Sony Bravia TV" - }, "broadlink": { "config_flow": true, "iot_class": "local_polling", @@ -595,20 +610,25 @@ "iot_class": "cloud_push", "name": "Unify Circuit" }, - "cisco_ios": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Cisco IOS" - }, - "cisco_mobility_express": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Cisco Mobility Express" - }, - "cisco_webex_teams": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Cisco Webex Teams" + "cisco": { + "name": "Cisco", + "integrations": { + "cisco_ios": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Cisco IOS" + }, + "cisco_mobility_express": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Cisco Mobility Express" + }, + "cisco_webex_teams": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Cisco Webex Teams" + } + } }, "citybikes": { "config_flow": false, @@ -626,14 +646,19 @@ "name": "Clickatell" }, "clicksend": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "ClickSend SMS" - }, - "clicksend_tts": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "ClickSend TTS" + "name": "ClickSend", + "integrations": { + "clicksend": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "ClickSend SMS" + }, + "clicksend_tts": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "ClickSend TTS" + } + } }, "climate": { "config_flow": false, @@ -726,11 +751,6 @@ "config_flow": false, "iot_class": null }, - "cppm_tracker": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Aruba ClearPass" - }, "cpuspeed": { "config_flow": true, "iot_class": "local_push" @@ -853,15 +873,20 @@ "config_flow": false, "iot_class": null }, - "devolo_home_control": { - "config_flow": true, - "iot_class": "local_push", - "name": "devolo Home Control" - }, - "devolo_home_network": { - "config_flow": true, - "iot_class": "local_polling", - "name": "devolo Home Network" + "devolo": { + "name": "devolo", + "integrations": { + "devolo_home_control": { + "config_flow": true, + "iot_class": "local_push", + "name": "devolo Home Control" + }, + "devolo_home_network": { + "config_flow": true, + "iot_class": "local_polling", + "name": "devolo Home Network" + } + } }, "dexcom": { "config_flow": true, @@ -917,15 +942,20 @@ "iot_class": "local_polling", "name": "D-Link Wi-Fi Smart Plugs" }, - "dlna_dmr": { - "config_flow": true, - "iot_class": "local_push", - "name": "DLNA Digital Media Renderer" - }, - "dlna_dms": { - "config_flow": true, - "iot_class": "local_polling", - "name": "DLNA Digital Media Server" + "dlna": { + "name": "DLNA", + "integrations": { + "dlna_dmr": { + "config_flow": true, + "iot_class": "local_push", + "name": "DLNA Digital Media Renderer" + }, + "dlna_dms": { + "config_flow": true, + "iot_class": "local_polling", + "name": "DLNA Digital Media Server" + } + } }, "dnsip": { "config_flow": true, @@ -997,11 +1027,6 @@ "iot_class": "cloud_polling", "name": "dweet.io" }, - "dynalite": { - "config_flow": true, - "iot_class": "local_push", - "name": "Philips Dynalite" - }, "eafm": { "config_flow": true, "iot_class": "cloud_polling", @@ -1073,9 +1098,19 @@ "name": "Eight Sleep" }, "elgato": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Elgato Light" + "name": "Elgato", + "integrations": { + "avea": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Elgato Avea" + }, + "elgato": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Elgato Light" + } + } }, "eliqonline": { "config_flow": false, @@ -1103,14 +1138,19 @@ "name": "Emby" }, "emoncms": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Emoncms" - }, - "emoncms_history": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Emoncms History" + "name": "emoncms", + "integrations": { + "emoncms": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Emoncms" + }, + "emoncms_history": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Emoncms History" + } + } }, "emonitor": { "config_flow": true, @@ -1171,19 +1211,34 @@ "name": "EPH Controls" }, "epson": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Epson" + "name": "Epson", + "integrations": { + "epson": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Epson" + }, + "epsonworkforce": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Epson Workforce" + } + } }, - "epsonworkforce": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Epson Workforce" - }, - "eq3btsmart": { - "config_flow": false, - "iot_class": "local_polling", - "name": "eQ-3 Bluetooth Smart Thermostats" + "eq3": { + "name": "eQ-3", + "integrations": { + "eq3btsmart": { + "config_flow": false, + "iot_class": "local_polling", + "name": "eQ-3 Bluetooth Smart Thermostats" + }, + "maxcube": { + "config_flow": false, + "iot_class": "local_polling", + "name": "eQ-3 MAX!" + } + } }, "escea": { "config_flow": true, @@ -1215,11 +1270,6 @@ "iot_class": "local_polling", "name": "Evil Genius Labs" }, - "evohome": { - "config_flow": false, - "iot_class": "cloud_polling", - "name": "Honeywell Total Connect Comfort (Europe)" - }, "ezviz": { "config_flow": true, "iot_class": "cloud_polling", @@ -1245,11 +1295,6 @@ "iot_class": "local_polling", "name": "Fail2Ban" }, - "familyhub": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Samsung Family Hub" - }, "fan": { "config_flow": false, "iot_class": null @@ -1265,19 +1310,24 @@ "name": "Feedreader" }, "ffmpeg": { - "config_flow": false, - "iot_class": null, - "name": "FFmpeg" - }, - "ffmpeg_motion": { - "config_flow": false, - "iot_class": "calculated", - "name": "FFmpeg Motion" - }, - "ffmpeg_noise": { - "config_flow": false, - "iot_class": "calculated", - "name": "FFmpeg Noise" + "name": "FFmpeg", + "integrations": { + "ffmpeg": { + "config_flow": false, + "iot_class": null, + "name": "FFmpeg" + }, + "ffmpeg_motion": { + "config_flow": false, + "iot_class": "calculated", + "name": "FFmpeg Motion" + }, + "ffmpeg_noise": { + "config_flow": false, + "iot_class": "calculated", + "name": "FFmpeg Noise" + } + } }, "fibaro": { "config_flow": true, @@ -1507,11 +1557,6 @@ "config_flow": true, "iot_class": "cloud_polling" }, - "gc100": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Global Cach\u00e9 GC-100" - }, "gdacs": { "config_flow": true, "iot_class": "cloud_polling", @@ -1562,15 +1607,20 @@ "iot_class": "cloud_push", "name": "Geofency" }, - "geonetnz_quakes": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "GeoNet NZ Quakes" - }, - "geonetnz_volcano": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "GeoNet NZ Volcano" + "geonet": { + "name": "GeoNet", + "integrations": { + "geonetnz_quakes": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "GeoNet NZ Quakes" + }, + "geonetnz_volcano": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "GeoNet NZ Volcano" + } + } }, "gios": { "config_flow": true, @@ -1597,6 +1647,21 @@ "iot_class": "local_polling", "name": "Glances" }, + "globalcache": { + "name": "Global Cach\u00e9", + "integrations": { + "gc100": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Global Cach\u00e9 GC-100" + }, + "itach": { + "config_flow": false, + "iot_class": "assumed_state", + "name": "Global Cach\u00e9 iTach TCP/IP to IR" + } + } + }, "goalfeed": { "config_flow": false, "iot_class": "cloud_push", @@ -1760,11 +1825,6 @@ "iot_class": "local_polling", "name": "Harman Kardon AVR" }, - "harmony": { - "config_flow": true, - "iot_class": "local_push", - "name": "Logitech Harmony Hub" - }, "hassio": { "config_flow": false, "iot_class": "local_polling", @@ -1796,14 +1856,19 @@ "name": "HERE Travel Time" }, "hikvision": { - "config_flow": false, - "iot_class": "local_push", - "name": "Hikvision" - }, - "hikvisioncam": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Hikvision" + "name": "Hikvision", + "integrations": { + "hikvision": { + "config_flow": false, + "iot_class": "local_push", + "name": "Hikvision" + }, + "hikvisioncam": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Hikvision" + } + } }, "hisense_aehw4a1": { "config_flow": true, @@ -1856,29 +1921,44 @@ "name": "Home Assistant Alerts" }, "homematic": { - "config_flow": false, - "iot_class": "local_push", - "name": "Homematic" - }, - "homematicip_cloud": { - "config_flow": true, - "iot_class": "cloud_push", - "name": "HomematicIP Cloud" + "name": "Homematic", + "integrations": { + "homematic": { + "config_flow": false, + "iot_class": "local_push", + "name": "Homematic" + }, + "homematicip_cloud": { + "config_flow": true, + "iot_class": "cloud_push", + "name": "HomematicIP Cloud" + } + } }, "homewizard": { "config_flow": true, "iot_class": "local_polling", "name": "HomeWizard Energy" }, - "homeworks": { - "config_flow": false, - "iot_class": "local_push", - "name": "Lutron Homeworks" - }, "honeywell": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Honeywell Total Connect Comfort (US)" + "name": "Honeywell", + "integrations": { + "lyric": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Honeywell Lyric" + }, + "evohome": { + "config_flow": false, + "iot_class": "cloud_polling", + "name": "Honeywell Total Connect Comfort (Europe)" + }, + "honeywell": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Honeywell Total Connect Comfort (US)" + } + } }, "horizon": { "config_flow": false, @@ -1905,11 +1985,6 @@ "iot_class": "local_polling", "name": "Huawei LTE" }, - "hue": { - "config_flow": true, - "iot_class": "local_push", - "name": "Philips Hue" - }, "huisbaasje": { "config_flow": true, "iot_class": "cloud_polling", @@ -1954,6 +2029,21 @@ "iot_class": "cloud_polling", "name": "Jandy iAqualink" }, + "ibm": { + "name": "IBM", + "integrations": { + "watson_iot": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "IBM Watson IoT Platform" + }, + "watson_tts": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "IBM Watson TTS" + } + } + }, "idteck_prox": { "config_flow": false, "iot_class": "local_push", @@ -2087,16 +2177,6 @@ "iot_class": "local_push", "name": "Universal Devices ISY994" }, - "itach": { - "config_flow": false, - "iot_class": "assumed_state", - "name": "Global Cach\u00e9 iTach TCP/IP to IR" - }, - "itunes": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Apple iTunes" - }, "izone": { "config_flow": true, "iot_class": "local_polling", @@ -2283,15 +2363,25 @@ "zwave" ] }, - "lg_netcast": { - "config_flow": false, - "iot_class": "local_polling", - "name": "LG Netcast" - }, - "lg_soundbar": { - "config_flow": true, - "iot_class": "local_polling", - "name": "LG Soundbars" + "lg": { + "name": "LG", + "integrations": { + "lg_netcast": { + "config_flow": false, + "iot_class": "local_polling", + "name": "LG Netcast" + }, + "lg_soundbar": { + "config_flow": true, + "iot_class": "local_polling", + "name": "LG Soundbars" + }, + "webostv": { + "config_flow": true, + "iot_class": "local_push", + "name": "LG webOS Smart TV" + } + } }, "lidarr": { "config_flow": true, @@ -2400,6 +2490,26 @@ "iot_class": "cloud_polling", "name": "Logi Circle" }, + "logitech": { + "name": "Logitech", + "integrations": { + "harmony": { + "config_flow": true, + "iot_class": "local_push", + "name": "Logitech Harmony Hub" + }, + "ue_smart_radio": { + "config_flow": false, + "iot_class": "cloud_polling", + "name": "Logitech UE Smart Radio" + }, + "squeezebox": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Squeezebox (Logitech Media Server)" + } + } + }, "london_air": { "config_flow": false, "iot_class": "cloud_polling", @@ -2420,11 +2530,6 @@ "iot_class": null, "name": "Dashboards" }, - "luci": { - "config_flow": false, - "iot_class": "local_polling", - "name": "OpenWrt (luci)" - }, "luftdaten": { "config_flow": true, "iot_class": "cloud_polling", @@ -2436,25 +2541,30 @@ "name": "Lupus Electronics LUPUSEC" }, "lutron": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Lutron" - }, - "lutron_caseta": { - "config_flow": true, - "iot_class": "local_push", - "name": "Lutron Cas\u00e9ta" + "name": "Lutron", + "integrations": { + "lutron": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Lutron" + }, + "lutron_caseta": { + "config_flow": true, + "iot_class": "local_push", + "name": "Lutron Cas\u00e9ta" + }, + "homeworks": { + "config_flow": false, + "iot_class": "local_push", + "name": "Lutron Homeworks" + } + } }, "lw12wifi": { "config_flow": false, "iot_class": "local_polling", "name": "LAGUTE LW-12" }, - "lyric": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Honeywell Lyric" - }, "magicseaweed": { "config_flow": false, "iot_class": "cloud_polling", @@ -2474,11 +2584,6 @@ "iot_class": "calculated", "name": "Manual Alarm Control Panel" }, - "manual_mqtt": { - "config_flow": false, - "iot_class": "local_push", - "name": "Manual MQTT Alarm Control Panel" - }, "map": { "config_flow": false, "iot_class": null, @@ -2499,11 +2604,6 @@ "iot_class": "cloud_push", "name": "Matrix" }, - "maxcube": { - "config_flow": false, - "iot_class": "local_polling", - "name": "eQ-3 MAX!" - }, "mazda": { "config_flow": true, "iot_class": "cloud_polling", @@ -2544,9 +2644,19 @@ "name": "Melissa" }, "melnor": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Melnor Bluetooth" + "name": "Melnor", + "integrations": { + "melnor": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Melnor Bluetooth" + }, + "raincloud": { + "config_flow": false, + "iot_class": "cloud_polling", + "name": "Melnor RainCloud" + } + } }, "meraki": { "config_flow": false, @@ -2594,24 +2704,59 @@ "name": "Ubiquiti mFi mPort" }, "microsoft": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Microsoft Text-to-Speech (TTS)" - }, - "microsoft_face": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Microsoft Face" - }, - "microsoft_face_detect": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Microsoft Face Detect" - }, - "microsoft_face_identify": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Microsoft Face Identify" + "name": "Microsoft", + "integrations": { + "azure_devops": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Azure DevOps" + }, + "azure_event_hub": { + "config_flow": true, + "iot_class": "cloud_push", + "name": "Azure Event Hub" + }, + "azure_service_bus": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Azure Service Bus" + }, + "microsoft_face_detect": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Microsoft Face Detect" + }, + "microsoft_face_identify": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Microsoft Face Identify" + }, + "microsoft_face": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Microsoft Face" + }, + "microsoft": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Microsoft Text-to-Speech (TTS)" + }, + "msteams": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Microsoft Teams" + }, + "xbox": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Xbox" + }, + "xbox_live": { + "config_flow": false, + "iot_class": "cloud_polling", + "name": "Xbox Live" + } + } }, "miflora": { "config_flow": false, @@ -2711,34 +2856,39 @@ "name": "Music Player Daemon (MPD)" }, "mqtt": { - "config_flow": true, - "iot_class": "local_push", - "name": "MQTT" - }, - "mqtt_eventstream": { - "config_flow": false, - "iot_class": "local_polling", - "name": "MQTT Eventstream" - }, - "mqtt_json": { - "config_flow": false, - "iot_class": "local_push", - "name": "MQTT JSON" - }, - "mqtt_room": { - "config_flow": false, - "iot_class": "local_push", - "name": "MQTT Room Presence" - }, - "mqtt_statestream": { - "config_flow": false, - "iot_class": "local_push", - "name": "MQTT Statestream" - }, - "msteams": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Microsoft Teams" + "name": "MQTT", + "integrations": { + "manual_mqtt": { + "config_flow": false, + "iot_class": "local_push", + "name": "Manual MQTT Alarm Control Panel" + }, + "mqtt": { + "config_flow": true, + "iot_class": "local_push", + "name": "MQTT" + }, + "mqtt_eventstream": { + "config_flow": false, + "iot_class": "local_polling", + "name": "MQTT Eventstream" + }, + "mqtt_json": { + "config_flow": false, + "iot_class": "local_push", + "name": "MQTT JSON" + }, + "mqtt_room": { + "config_flow": false, + "iot_class": "local_push", + "name": "MQTT Room Presence" + }, + "mqtt_statestream": { + "config_flow": false, + "iot_class": "local_push", + "name": "MQTT Statestream" + } + } }, "mullvad": { "config_flow": true, @@ -2831,14 +2981,19 @@ "name": "Netdata" }, "netgear": { - "config_flow": true, - "iot_class": "local_polling", - "name": "NETGEAR" - }, - "netgear_lte": { - "config_flow": false, - "iot_class": "local_polling", - "name": "NETGEAR LTE" + "name": "NETGEAR", + "integrations": { + "netgear": { + "config_flow": true, + "iot_class": "local_polling", + "name": "NETGEAR" + }, + "netgear_lte": { + "config_flow": false, + "iot_class": "local_polling", + "name": "NETGEAR LTE" + } + } }, "netio": { "config_flow": false, @@ -3142,6 +3297,21 @@ "iot_class": "cloud_polling", "name": "OpenWeatherMap" }, + "openwrt": { + "name": "OpenWrt", + "integrations": { + "luci": { + "config_flow": false, + "iot_class": "local_polling", + "name": "OpenWrt (luci)" + }, + "ubus": { + "config_flow": false, + "iot_class": "local_polling", + "name": "OpenWrt (ubus)" + } + } + }, "opnsense": { "config_flow": false, "iot_class": "local_polling", @@ -3192,15 +3362,20 @@ "iot_class": "local_polling", "name": "P1 Monitor" }, - "panasonic_bluray": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Panasonic Blu-Ray Player" - }, - "panasonic_viera": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Panasonic Viera" + "panasonic": { + "name": "Panasonic", + "integrations": { + "panasonic_bluray": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Panasonic Blu-Ray Player" + }, + "panasonic_viera": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Panasonic Viera" + } + } }, "pandora": { "config_flow": false, @@ -3236,10 +3411,25 @@ "config_flow": false, "iot_class": "calculated" }, - "philips_js": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Philips TV" + "philips": { + "name": "Philips", + "integrations": { + "dynalite": { + "config_flow": true, + "iot_class": "local_push", + "name": "Philips Dynalite" + }, + "hue": { + "config_flow": true, + "iot_class": "local_push", + "name": "Philips Hue" + }, + "philips_js": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Philips TV" + } + } }, "pi_hole": { "config_flow": true, @@ -3315,11 +3505,6 @@ "iot_class": "cloud_polling", "name": "PoolSense" }, - "powerwall": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Tesla Powerwall" - }, "profiler": { "config_flow": true, "iot_class": null, @@ -3369,11 +3554,6 @@ "iot_class": "local_polling", "name": "PrusaLink" }, - "ps4": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Sony PlayStation 4" - }, "pulseaudio_loopback": { "config_flow": false, "iot_class": "local_polling", @@ -3440,14 +3620,19 @@ "name": "Queensland Bushfire Alert" }, "qnap": { - "config_flow": false, - "iot_class": "local_polling", - "name": "QNAP" - }, - "qnap_qsw": { - "config_flow": true, - "iot_class": "local_polling", - "name": "QNAP QSW" + "name": "QNAP", + "integrations": { + "qnap": { + "config_flow": false, + "iot_class": "local_polling", + "name": "QNAP" + }, + "qnap_qsw": { + "config_flow": true, + "iot_class": "local_polling", + "name": "QNAP QSW" + } + } }, "qrcode": { "config_flow": false, @@ -3494,11 +3679,6 @@ "iot_class": "local_polling", "name": "Rain Bird" }, - "raincloud": { - "config_flow": false, - "iot_class": "cloud_polling", - "name": "Melnor RainCloud" - }, "rainforest_eagle": { "config_flow": true, "iot_class": "local_polling", @@ -3514,6 +3694,25 @@ "iot_class": "local_polling", "name": "Random" }, + "raspberry": { + "name": "Raspberry Pi", + "integrations": { + "rpi_camera": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Raspberry Pi Camera" + }, + "rpi_power": { + "config_flow": true, + "iot_class": "local_polling" + }, + "remote_rpi_gpio": { + "config_flow": false, + "iot_class": "local_push", + "name": "remote_rpi_gpio" + } + } + }, "raspyrfm": { "config_flow": false, "iot_class": "assumed_state", @@ -3558,11 +3757,6 @@ "config_flow": false, "iot_class": null }, - "remote_rpi_gpio": { - "config_flow": false, - "iot_class": "local_push", - "name": "remote_rpi_gpio" - }, "renault": { "config_flow": true, "iot_class": "cloud_polling", @@ -3653,25 +3847,11 @@ "iot_class": "local_push", "name": "RoonLabs music player" }, - "route53": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "AWS Route53" - }, "rova": { "config_flow": false, "iot_class": "cloud_polling", "name": "ROVA" }, - "rpi_camera": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Raspberry Pi Camera" - }, - "rpi_power": { - "config_flow": true, - "iot_class": "local_polling" - }, "rss_feed_template": { "config_flow": false, "iot_class": "local_push", @@ -3692,15 +3872,20 @@ "iot_class": "local_polling", "name": "Ruckus Unleashed" }, - "russound_rio": { - "config_flow": false, - "iot_class": "local_push", - "name": "Russound RIO" - }, - "russound_rnet": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Russound RNET" + "russound": { + "name": "Russound", + "integrations": { + "russound_rio": { + "config_flow": false, + "iot_class": "local_push", + "name": "Russound RIO" + }, + "russound_rnet": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Russound RNET" + } + } }, "sabnzbd": { "config_flow": true, @@ -3717,10 +3902,25 @@ "iot_class": "local_polling", "name": "SAJ Solar Inverter" }, - "samsungtv": { - "config_flow": true, - "iot_class": "local_push", - "name": "Samsung Smart TV" + "samsung": { + "name": "Samsung", + "integrations": { + "familyhub": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Samsung Family Hub" + }, + "samsungtv": { + "config_flow": true, + "iot_class": "local_push", + "name": "Samsung Smart TV" + }, + "syncthru": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Samsung SyncThru Printer" + } + } }, "satel_integra": { "config_flow": false, @@ -4012,14 +4212,19 @@ "name": "SNMP" }, "solaredge": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "SolarEdge" - }, - "solaredge_local": { - "config_flow": false, - "iot_class": "local_polling", - "name": "SolarEdge Local" + "name": "SolarEdge", + "integrations": { + "solaredge": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "SolarEdge" + }, + "solaredge_local": { + "config_flow": false, + "iot_class": "local_polling", + "name": "SolarEdge Local" + } + } }, "solarlog": { "config_flow": true, @@ -4046,20 +4251,35 @@ "iot_class": "local_polling", "name": "Sonarr" }, - "songpal": { - "config_flow": true, - "iot_class": "local_push", - "name": "Sony Songpal" - }, "sonos": { "config_flow": true, "iot_class": "local_push", "name": "Sonos" }, - "sony_projector": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Sony Projector" + "sony": { + "name": "Sony", + "integrations": { + "braviatv": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Sony Bravia TV" + }, + "ps4": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Sony PlayStation 4" + }, + "sony_projector": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Sony Projector" + }, + "songpal": { + "config_flow": true, + "iot_class": "local_push", + "name": "Sony Songpal" + } + } }, "soundtouch": { "config_flow": true, @@ -4101,11 +4321,6 @@ "iot_class": "local_polling", "name": "SQL" }, - "squeezebox": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Squeezebox (Logitech Media Server)" - }, "srp_energy": { "config_flow": true, "iot_class": "cloud_polling", @@ -4249,25 +4464,25 @@ "iot_class": "local_polling", "name": "Syncthing" }, - "syncthru": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Samsung SyncThru Printer" - }, - "synology_chat": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Synology Chat" - }, - "synology_dsm": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Synology DSM" - }, - "synology_srm": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Synology SRM" + "synology": { + "name": "Synology", + "integrations": { + "synology_chat": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Synology Chat" + }, + "synology_dsm": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Synology DSM" + }, + "synology_srm": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Synology SRM" + } + } }, "syslog": { "config_flow": false, @@ -4343,24 +4558,34 @@ "name": "The Energy Detective TED5000" }, "telegram": { - "config_flow": false, - "iot_class": "cloud_polling", - "name": "Telegram" + "name": "Telegram", + "integrations": { + "telegram": { + "config_flow": false, + "iot_class": "cloud_polling", + "name": "Telegram" + }, + "telegram_bot": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Telegram bot" + } + } }, - "telegram_bot": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Telegram bot" - }, - "tellduslive": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Telldus Live" - }, - "tellstick": { - "config_flow": false, - "iot_class": "assumed_state", - "name": "TellStick" + "telldus": { + "name": "Telldus", + "integrations": { + "tellduslive": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Telldus Live" + }, + "tellstick": { + "config_flow": false, + "iot_class": "assumed_state", + "name": "TellStick" + } + } }, "telnet": { "config_flow": false, @@ -4382,10 +4607,20 @@ "iot_class": "local_polling", "name": "TensorFlow" }, - "tesla_wall_connector": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Tesla Wall Connector" + "tesla": { + "name": "Tesla", + "integrations": { + "powerwall": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Tesla Powerwall" + }, + "tesla_wall_connector": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Tesla Wall Connector" + } + } }, "tfiac": { "config_flow": false, @@ -4533,20 +4768,25 @@ "iot_class": "local_polling", "name": "IKEA TR\u00c5DFRI" }, - "trafikverket_ferry": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Trafikverket Ferry" - }, - "trafikverket_train": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Trafikverket Train" - }, - "trafikverket_weatherstation": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Trafikverket Weather Station" + "trafikverket": { + "name": "Trafikverket", + "integrations": { + "trafikverket_ferry": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Trafikverket Ferry" + }, + "trafikverket_train": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Trafikverket Train" + }, + "trafikverket_weatherstation": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Trafikverket Weather Station" + } + } }, "transmission": { "config_flow": true, @@ -4584,19 +4824,24 @@ "name": "Twente Milieu" }, "twilio": { - "config_flow": true, - "iot_class": "cloud_push", - "name": "Twilio" - }, - "twilio_call": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Twilio Call" - }, - "twilio_sms": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Twilio SMS" + "name": "Twilio", + "integrations": { + "twilio": { + "config_flow": true, + "iot_class": "cloud_push", + "name": "Twilio" + }, + "twilio_call": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Twilio Call" + }, + "twilio_sms": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Twilio SMS" + } + } }, "twinkly": { "config_flow": true, @@ -4638,16 +4883,6 @@ } } }, - "ubus": { - "config_flow": false, - "iot_class": "local_polling", - "name": "OpenWrt (ubus)" - }, - "ue_smart_radio": { - "config_flow": false, - "iot_class": "cloud_polling", - "name": "Logitech UE Smart Radio" - }, "uk_transport": { "config_flow": false, "iot_class": "cloud_polling", @@ -4791,14 +5026,19 @@ "name": "VIZIO SmartCast" }, "vlc": { - "config_flow": false, - "iot_class": "local_polling", - "name": "VLC media player" - }, - "vlc_telnet": { - "config_flow": true, - "iot_class": "local_polling", - "name": "VLC media player via Telnet" + "name": "VideoLAN", + "integrations": { + "vlc": { + "config_flow": false, + "iot_class": "local_polling", + "name": "VLC media player" + }, + "vlc_telnet": { + "config_flow": true, + "iot_class": "local_polling", + "name": "VLC media player via Telnet" + } + } }, "voicerss": { "config_flow": false, @@ -4860,16 +5100,6 @@ "iot_class": "cloud_polling", "name": "WaterFurnace" }, - "watson_iot": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "IBM Watson IoT Platform" - }, - "watson_tts": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "IBM Watson TTS" - }, "watttime": { "config_flow": true, "iot_class": "cloud_polling", @@ -4889,11 +5119,6 @@ "iot_class": null, "name": "Webhook" }, - "webostv": { - "config_flow": true, - "iot_class": "local_push", - "name": "LG webOS Smart TV" - }, "websocket_api": { "config_flow": false, "iot_class": null, @@ -4984,45 +5209,40 @@ "iot_class": "local_polling", "name": "Heyu X10" }, - "xbox": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Xbox" - }, - "xbox_live": { - "config_flow": false, - "iot_class": "cloud_polling", - "name": "Xbox Live" - }, "xeoma": { "config_flow": false, "iot_class": "local_polling", "name": "Xeoma" }, "xiaomi": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Xiaomi" - }, - "xiaomi_aqara": { - "config_flow": true, - "iot_class": "local_push", - "name": "Xiaomi Gateway (Aqara)" - }, - "xiaomi_ble": { - "config_flow": true, - "iot_class": "local_push", - "name": "Xiaomi BLE" - }, - "xiaomi_miio": { - "config_flow": true, - "iot_class": "local_polling", - "name": "Xiaomi Miio" - }, - "xiaomi_tv": { - "config_flow": false, - "iot_class": "assumed_state", - "name": "Xiaomi TV" + "name": "Xiaomi", + "integrations": { + "xiaomi_aqara": { + "config_flow": true, + "iot_class": "local_push", + "name": "Xiaomi Gateway (Aqara)" + }, + "xiaomi_ble": { + "config_flow": true, + "iot_class": "local_push", + "name": "Xiaomi BLE" + }, + "xiaomi_miio": { + "config_flow": true, + "iot_class": "local_polling", + "name": "Xiaomi Miio" + }, + "xiaomi_tv": { + "config_flow": false, + "iot_class": "assumed_state", + "name": "Xiaomi TV" + }, + "xiaomi": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Xiaomi" + } + } }, "xmpp": { "config_flow": false, @@ -5034,15 +5254,25 @@ "iot_class": "local_polling", "name": "EZcontrol XS1" }, - "yale_smart_alarm": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Yale Smart Living" - }, - "yalexs_ble": { - "config_flow": true, - "iot_class": "local_push", - "name": "Yale Access Bluetooth" + "yale": { + "name": "Yale", + "integrations": { + "august": { + "config_flow": true, + "iot_class": "cloud_push", + "name": "August" + }, + "yale_smart_alarm": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Yale Smart Living" + }, + "yalexs_ble": { + "config_flow": true, + "iot_class": "local_push", + "name": "Yale Access Bluetooth" + } + } }, "yamaha": { "config_flow": false, @@ -5054,25 +5284,35 @@ "iot_class": "local_push", "name": "MusicCast" }, - "yandex_transport": { - "config_flow": false, - "iot_class": "cloud_polling", - "name": "Yandex Transport" - }, - "yandextts": { - "config_flow": false, - "iot_class": "cloud_push", - "name": "Yandex TTS" + "yandex": { + "name": "Yandex", + "integrations": { + "yandex_transport": { + "config_flow": false, + "iot_class": "cloud_polling", + "name": "Yandex Transport" + }, + "yandextts": { + "config_flow": false, + "iot_class": "cloud_push", + "name": "Yandex TTS" + } + } }, "yeelight": { - "config_flow": true, - "iot_class": "local_push", - "name": "Yeelight" - }, - "yeelightsunflower": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Yeelight Sunflower" + "name": "Yeelight", + "integrations": { + "yeelight": { + "config_flow": true, + "iot_class": "local_push", + "name": "Yeelight" + }, + "yeelightsunflower": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Yeelight Sunflower" + } + } }, "yi": { "config_flow": false, From 2b27cfdabb0a3d0737cfe2fcb06dd591449ea4de Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 4 Oct 2022 16:36:42 +0200 Subject: [PATCH 157/985] Set system & entity integration types (#79593) --- .../alarm_control_panel/manifest.json | 3 +- homeassistant/components/api/manifest.json | 3 +- .../application_credentials/manifest.json | 3 +- homeassistant/components/auth/manifest.json | 3 +- .../components/automation/manifest.json | 3 +- homeassistant/components/backup/manifest.json | 3 +- .../components/binary_sensor/manifest.json | 3 +- .../components/blueprint/manifest.json | 3 +- homeassistant/components/button/manifest.json | 3 +- .../components/calendar/manifest.json | 3 +- homeassistant/components/camera/manifest.json | 3 +- .../components/climate/manifest.json | 3 +- homeassistant/components/config/manifest.json | 3 +- .../components/configurator/manifest.json | 3 +- .../components/conversation/manifest.json | 3 +- homeassistant/components/cover/manifest.json | 3 +- .../components/default_config/manifest.json | 3 +- .../device_automation/manifest.json | 3 +- .../components/device_tracker/manifest.json | 3 +- homeassistant/components/dhcp/manifest.json | 3 +- .../components/diagnostics/manifest.json | 3 +- .../components/discovery/manifest.json | 3 +- homeassistant/components/energy/manifest.json | 3 +- homeassistant/components/fan/manifest.json | 3 +- .../components/file_upload/manifest.json | 3 +- .../components/frontend/manifest.json | 3 +- .../components/geo_location/manifest.json | 3 +- .../components/hardware/manifest.json | 3 +- homeassistant/components/hassio/manifest.json | 3 +- .../components/history/manifest.json | 3 +- .../components/homeassistant/manifest.json | 3 +- homeassistant/components/http/manifest.json | 3 +- .../components/humidifier/manifest.json | 3 +- homeassistant/components/image/manifest.json | 3 +- .../components/image_processing/manifest.json | 3 +- homeassistant/components/intent/manifest.json | 3 +- homeassistant/components/light/manifest.json | 3 +- homeassistant/components/lock/manifest.json | 3 +- .../components/logbook/manifest.json | 3 +- homeassistant/components/logger/manifest.json | 3 +- .../components/lovelace/manifest.json | 3 +- .../components/mailbox/manifest.json | 3 +- .../components/media_player/manifest.json | 3 +- .../components/media_source/manifest.json | 3 +- homeassistant/components/my/manifest.json | 3 +- .../components/network/manifest.json | 3 +- homeassistant/components/notify/manifest.json | 3 +- homeassistant/components/number/manifest.json | 3 +- .../components/onboarding/manifest.json | 3 +- homeassistant/components/person/manifest.json | 3 +- .../components/recorder/manifest.json | 3 +- homeassistant/components/remote/manifest.json | 3 +- .../components/repairs/manifest.json | 3 +- .../components/safe_mode/manifest.json | 3 +- homeassistant/components/scene/manifest.json | 3 +- homeassistant/components/script/manifest.json | 3 +- homeassistant/components/search/manifest.json | 3 +- homeassistant/components/select/manifest.json | 3 +- homeassistant/components/sensor/manifest.json | 3 +- homeassistant/components/siren/manifest.json | 3 +- homeassistant/components/ssdp/manifest.json | 3 +- homeassistant/components/stt/manifest.json | 3 +- homeassistant/components/switch/manifest.json | 3 +- .../components/system_health/manifest.json | 3 +- homeassistant/components/trace/manifest.json | 3 +- homeassistant/components/tts/manifest.json | 3 +- homeassistant/components/update/manifest.json | 3 +- homeassistant/components/usb/manifest.json | 3 +- homeassistant/components/vacuum/manifest.json | 3 +- .../components/water_heater/manifest.json | 3 +- .../components/weather/manifest.json | 3 +- .../components/websocket_api/manifest.json | 3 +- .../components/zeroconf/manifest.json | 3 +- homeassistant/generated/integrations.json | 365 ------------------ 74 files changed, 146 insertions(+), 438 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/manifest.json b/homeassistant/components/alarm_control_panel/manifest.json index 461094e8ce6..426e1e15afb 100644 --- a/homeassistant/components/alarm_control_panel/manifest.json +++ b/homeassistant/components/alarm_control_panel/manifest.json @@ -3,5 +3,6 @@ "name": "Alarm Control Panel", "documentation": "https://www.home-assistant.io/integrations/alarm_control_panel", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/api/manifest.json b/homeassistant/components/api/manifest.json index 1f400470943..dadfc95c3b9 100644 --- a/homeassistant/components/api/manifest.json +++ b/homeassistant/components/api/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/api", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/application_credentials/manifest.json b/homeassistant/components/application_credentials/manifest.json index 9a8abc16c36..fa45f1a6309 100644 --- a/homeassistant/components/application_credentials/manifest.json +++ b/homeassistant/components/application_credentials/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/application_credentials", "dependencies": ["auth", "websocket_api"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/auth/manifest.json b/homeassistant/components/auth/manifest.json index 2674bdfb032..200e41713d6 100644 --- a/homeassistant/components/auth/manifest.json +++ b/homeassistant/components/auth/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/auth", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index 9dd0130ee2f..3bfb192759c 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["blueprint", "trace"], "after_dependencies": ["device_automation", "webhook"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/backup/manifest.json b/homeassistant/components/backup/manifest.json index eaf6a9fd979..3eefa68fcc4 100644 --- a/homeassistant/components/backup/manifest.json +++ b/homeassistant/components/backup/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@home-assistant/core"], "requirements": ["securetar==2022.2.0"], "iot_class": "calculated", - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/binary_sensor/manifest.json b/homeassistant/components/binary_sensor/manifest.json index d1c631ee94b..e10478889f3 100644 --- a/homeassistant/components/binary_sensor/manifest.json +++ b/homeassistant/components/binary_sensor/manifest.json @@ -3,5 +3,6 @@ "name": "Binary Sensor", "documentation": "https://www.home-assistant.io/integrations/binary_sensor", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/blueprint/manifest.json b/homeassistant/components/blueprint/manifest.json index c00b92b1e3c..4ed299438bb 100644 --- a/homeassistant/components/blueprint/manifest.json +++ b/homeassistant/components/blueprint/manifest.json @@ -3,5 +3,6 @@ "name": "Blueprint", "documentation": "https://www.home-assistant.io/integrations/blueprint", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/button/manifest.json b/homeassistant/components/button/manifest.json index beeaca487a6..02945d979ff 100644 --- a/homeassistant/components/button/manifest.json +++ b/homeassistant/components/button/manifest.json @@ -3,5 +3,6 @@ "name": "Button", "documentation": "https://www.home-assistant.io/integrations/button", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/calendar/manifest.json b/homeassistant/components/calendar/manifest.json index 2fb4df84414..cc4f09cfa64 100644 --- a/homeassistant/components/calendar/manifest.json +++ b/homeassistant/components/calendar/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/calendar", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index b1ab479f3a5..92bed21c1b8 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -6,5 +6,6 @@ "requirements": ["PyTurboJPEG==1.6.7"], "after_dependencies": ["media_player"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/climate/manifest.json b/homeassistant/components/climate/manifest.json index 8b54d3a91ad..7c23705181a 100644 --- a/homeassistant/components/climate/manifest.json +++ b/homeassistant/components/climate/manifest.json @@ -3,5 +3,6 @@ "name": "Climate", "documentation": "https://www.home-assistant.io/integrations/climate", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/config/manifest.json b/homeassistant/components/config/manifest.json index 57dfd0d360a..3be667f6cd2 100644 --- a/homeassistant/components/config/manifest.json +++ b/homeassistant/components/config/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/config", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/configurator/manifest.json b/homeassistant/components/configurator/manifest.json index acd0fa80423..716fe26910b 100644 --- a/homeassistant/components/configurator/manifest.json +++ b/homeassistant/components/configurator/manifest.json @@ -3,5 +3,6 @@ "name": "Configurator", "documentation": "https://www.home-assistant.io/integrations/configurator", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 1d2e0893065..54265bfcb83 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "integration_type": "system" } diff --git a/homeassistant/components/cover/manifest.json b/homeassistant/components/cover/manifest.json index 3da130fd799..66347b77eea 100644 --- a/homeassistant/components/cover/manifest.json +++ b/homeassistant/components/cover/manifest.json @@ -3,5 +3,6 @@ "name": "Cover", "documentation": "https://www.home-assistant.io/integrations/cover", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 6701e62c71f..d33aee6e030 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -41,5 +41,6 @@ "zone" ], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/device_automation/manifest.json b/homeassistant/components/device_automation/manifest.json index 033a54312be..e897cb5a29f 100644 --- a/homeassistant/components/device_automation/manifest.json +++ b/homeassistant/components/device_automation/manifest.json @@ -3,5 +3,6 @@ "name": "Device Automation", "documentation": "https://www.home-assistant.io/integrations/device_automation", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/device_tracker/manifest.json b/homeassistant/components/device_tracker/manifest.json index 7abd68b03e2..1ce4349e537 100644 --- a/homeassistant/components/device_tracker/manifest.json +++ b/homeassistant/components/device_tracker/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["zone"], "after_dependencies": [], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index f3f44f6dc9b..2ebb0fd63e0 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push", - "loggers": ["aiodiscover", "dnspython", "pyroute2", "scapy"] + "loggers": ["aiodiscover", "dnspython", "pyroute2", "scapy"], + "integration_type": "system" } diff --git a/homeassistant/components/diagnostics/manifest.json b/homeassistant/components/diagnostics/manifest.json index ad6edf110b0..383ebebd947 100644 --- a/homeassistant/components/diagnostics/manifest.json +++ b/homeassistant/components/diagnostics/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/diagnostics", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json index 6f97993c788..c98cdfa60a6 100644 --- a/homeassistant/components/discovery/manifest.json +++ b/homeassistant/components/discovery/manifest.json @@ -6,5 +6,6 @@ "after_dependencies": ["zeroconf"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", - "loggers": ["netdisco"] + "loggers": ["netdisco"], + "integration_type": "system" } diff --git a/homeassistant/components/energy/manifest.json b/homeassistant/components/energy/manifest.json index 5ddc6457a61..39a3f66d65c 100644 --- a/homeassistant/components/energy/manifest.json +++ b/homeassistant/components/energy/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@home-assistant/core"], "iot_class": "calculated", "dependencies": ["websocket_api", "history", "recorder"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/fan/manifest.json b/homeassistant/components/fan/manifest.json index bb968240f0b..f25162d9959 100644 --- a/homeassistant/components/fan/manifest.json +++ b/homeassistant/components/fan/manifest.json @@ -3,5 +3,6 @@ "name": "Fan", "documentation": "https://www.home-assistant.io/integrations/fan", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/file_upload/manifest.json b/homeassistant/components/file_upload/manifest.json index 6e190ba3712..d2b4f88a279 100644 --- a/homeassistant/components/file_upload/manifest.json +++ b/homeassistant/components/file_upload/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/file_upload", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index fde637657dd..62bed3777a8 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -19,5 +19,6 @@ "websocket_api" ], "codeowners": ["@home-assistant/frontend"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/geo_location/manifest.json b/homeassistant/components/geo_location/manifest.json index 2e0d7061099..3a0cb7eae91 100644 --- a/homeassistant/components/geo_location/manifest.json +++ b/homeassistant/components/geo_location/manifest.json @@ -3,5 +3,6 @@ "name": "Geolocation", "documentation": "https://www.home-assistant.io/integrations/geo_location", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/hardware/manifest.json b/homeassistant/components/hardware/manifest.json index 710726d9869..8f7e27e6911 100644 --- a/homeassistant/components/hardware/manifest.json +++ b/homeassistant/components/hardware/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/hardware", "codeowners": ["@home-assistant/core"], "quality_scale": "internal", - "requirements": ["psutil-home-assistant==0.0.1"] + "requirements": ["psutil-home-assistant==0.0.1"], + "integration_type": "system" } diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index 5de80fdbd19..b087eb25807 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -6,5 +6,6 @@ "after_dependencies": ["panel_custom"], "codeowners": ["@home-assistant/supervisor"], "iot_class": "local_polling", - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/history/manifest.json b/homeassistant/components/history/manifest.json index 7185a8b63c4..4ebf64dd603 100644 --- a/homeassistant/components/history/manifest.json +++ b/homeassistant/components/history/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/history", "dependencies": ["http", "recorder"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/homeassistant/manifest.json b/homeassistant/components/homeassistant/manifest.json index 027d1b9376d..179e8deb233 100644 --- a/homeassistant/components/homeassistant/manifest.json +++ b/homeassistant/components/homeassistant/manifest.json @@ -3,5 +3,6 @@ "name": "Home Assistant Core Integration", "documentation": "https://www.home-assistant.io/integrations/homeassistant", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/http/manifest.json b/homeassistant/components/http/manifest.json index 4391fd1acaf..26bf3dc31ce 100644 --- a/homeassistant/components/http/manifest.json +++ b/homeassistant/components/http/manifest.json @@ -5,5 +5,6 @@ "requirements": ["aiohttp_cors==0.7.0"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "integration_type": "system" } diff --git a/homeassistant/components/humidifier/manifest.json b/homeassistant/components/humidifier/manifest.json index b64065a2583..0cb84e08f0e 100644 --- a/homeassistant/components/humidifier/manifest.json +++ b/homeassistant/components/humidifier/manifest.json @@ -3,5 +3,6 @@ "name": "Humidifier", "documentation": "https://www.home-assistant.io/integrations/humidifier", "codeowners": ["@home-assistant/core", "@Shulyaka"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json index 4f967dbcc89..ed500c89011 100644 --- a/homeassistant/components/image/manifest.json +++ b/homeassistant/components/image/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pillow==9.2.0"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index 0315a69b82a..43a52268881 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/image_processing", "dependencies": ["camera"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/intent/manifest.json b/homeassistant/components/intent/manifest.json index e5c87461022..771482d76a4 100644 --- a/homeassistant/components/intent/manifest.json +++ b/homeassistant/components/intent/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/intent", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/light/manifest.json b/homeassistant/components/light/manifest.json index c7cf2abc7c8..e49701794d4 100644 --- a/homeassistant/components/light/manifest.json +++ b/homeassistant/components/light/manifest.json @@ -3,5 +3,6 @@ "name": "Light", "documentation": "https://www.home-assistant.io/integrations/light", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/lock/manifest.json b/homeassistant/components/lock/manifest.json index f93d2962ea3..0a786c05865 100644 --- a/homeassistant/components/lock/manifest.json +++ b/homeassistant/components/lock/manifest.json @@ -3,5 +3,6 @@ "name": "Lock", "documentation": "https://www.home-assistant.io/integrations/lock", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/logbook/manifest.json b/homeassistant/components/logbook/manifest.json index 66c0348a2ac..5b8a8d4c2a3 100644 --- a/homeassistant/components/logbook/manifest.json +++ b/homeassistant/components/logbook/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/logbook", "dependencies": ["frontend", "http", "recorder"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/logger/manifest.json b/homeassistant/components/logger/manifest.json index 2cb04538260..ef0a6fa2e65 100644 --- a/homeassistant/components/logger/manifest.json +++ b/homeassistant/components/logger/manifest.json @@ -3,5 +3,6 @@ "name": "Logger", "documentation": "https://www.home-assistant.io/integrations/logger", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/lovelace/manifest.json b/homeassistant/components/lovelace/manifest.json index 8cccf65f37c..7d9561f9755 100644 --- a/homeassistant/components/lovelace/manifest.json +++ b/homeassistant/components/lovelace/manifest.json @@ -3,5 +3,6 @@ "name": "Dashboards", "documentation": "https://www.home-assistant.io/integrations/lovelace", "codeowners": ["@home-assistant/frontend"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/mailbox/manifest.json b/homeassistant/components/mailbox/manifest.json index 9d8a1403332..8d080888985 100644 --- a/homeassistant/components/mailbox/manifest.json +++ b/homeassistant/components/mailbox/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/mailbox", "dependencies": ["http"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/media_player/manifest.json b/homeassistant/components/media_player/manifest.json index 118d05036cc..4b8b9013b98 100644 --- a/homeassistant/components/media_player/manifest.json +++ b/homeassistant/components/media_player/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/media_player", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/media_source/manifest.json b/homeassistant/components/media_source/manifest.json index 3b00df4300b..ae65137c113 100644 --- a/homeassistant/components/media_source/manifest.json +++ b/homeassistant/components/media_source/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/media_source", "dependencies": ["http"], "codeowners": ["@hunterjm"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/my/manifest.json b/homeassistant/components/my/manifest.json index 8c88b092e1c..23d1b3d21e2 100644 --- a/homeassistant/components/my/manifest.json +++ b/homeassistant/components/my/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/my", "dependencies": ["frontend"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/network/manifest.json b/homeassistant/components/network/manifest.json index 9f2fa7849f0..40712a40faf 100644 --- a/homeassistant/components/network/manifest.json +++ b/homeassistant/components/network/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@home-assistant/core"], "dependencies": ["websocket_api"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "integration_type": "system" } diff --git a/homeassistant/components/notify/manifest.json b/homeassistant/components/notify/manifest.json index b32295a10a6..3e930b32ba6 100644 --- a/homeassistant/components/notify/manifest.json +++ b/homeassistant/components/notify/manifest.json @@ -3,5 +3,6 @@ "name": "Notifications", "documentation": "https://www.home-assistant.io/integrations/notify", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/number/manifest.json b/homeassistant/components/number/manifest.json index 549494fa3f5..4cb16c8e0c3 100644 --- a/homeassistant/components/number/manifest.json +++ b/homeassistant/components/number/manifest.json @@ -3,5 +3,6 @@ "name": "Number", "documentation": "https://www.home-assistant.io/integrations/number", "codeowners": ["@home-assistant/core", "@Shulyaka"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json index fe65d82f626..4e200d22502 100644 --- a/homeassistant/components/onboarding/manifest.json +++ b/homeassistant/components/onboarding/manifest.json @@ -5,5 +5,6 @@ "after_dependencies": ["hassio"], "dependencies": ["analytics", "auth", "http", "person"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/person/manifest.json b/homeassistant/components/person/manifest.json index 09b74bf34eb..dc9c76ca103 100644 --- a/homeassistant/components/person/manifest.json +++ b/homeassistant/components/person/manifest.json @@ -6,5 +6,6 @@ "after_dependencies": ["device_tracker"], "codeowners": [], "quality_scale": "internal", - "iot_class": "calculated" + "iot_class": "calculated", + "integration_type": "system" } diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 51fd4a6dbe3..19a22b2a1e3 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -5,5 +5,6 @@ "requirements": ["sqlalchemy==1.4.41", "fnvhash==0.1.0"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "integration_type": "system" } diff --git a/homeassistant/components/remote/manifest.json b/homeassistant/components/remote/manifest.json index d08cb624c16..dac51e27749 100644 --- a/homeassistant/components/remote/manifest.json +++ b/homeassistant/components/remote/manifest.json @@ -3,5 +3,6 @@ "name": "Remote", "documentation": "https://www.home-assistant.io/integrations/remote", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/repairs/manifest.json b/homeassistant/components/repairs/manifest.json index c87d9d559e0..c63c3ec2946 100644 --- a/homeassistant/components/repairs/manifest.json +++ b/homeassistant/components/repairs/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/repairs", "codeowners": ["@home-assistant/core"], "dependencies": ["http"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/safe_mode/manifest.json b/homeassistant/components/safe_mode/manifest.json index 5ce7c3abf7b..f2627693f33 100644 --- a/homeassistant/components/safe_mode/manifest.json +++ b/homeassistant/components/safe_mode/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/safe_mode", "dependencies": ["frontend", "persistent_notification", "cloud"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/scene/manifest.json b/homeassistant/components/scene/manifest.json index 3134a310042..d653c8076e6 100644 --- a/homeassistant/components/scene/manifest.json +++ b/homeassistant/components/scene/manifest.json @@ -3,5 +3,6 @@ "name": "Scenes", "documentation": "https://www.home-assistant.io/integrations/scene", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/script/manifest.json b/homeassistant/components/script/manifest.json index da7d249ce12..a31861cba87 100644 --- a/homeassistant/components/script/manifest.json +++ b/homeassistant/components/script/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/script", "dependencies": ["blueprint", "trace"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/search/manifest.json b/homeassistant/components/search/manifest.json index b9ce2115112..8feba7c08e2 100644 --- a/homeassistant/components/search/manifest.json +++ b/homeassistant/components/search/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["websocket_api"], "after_dependencies": ["scene", "group", "automation", "script"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/select/manifest.json b/homeassistant/components/select/manifest.json index 86e8b917199..8427a72321e 100644 --- a/homeassistant/components/select/manifest.json +++ b/homeassistant/components/select/manifest.json @@ -3,5 +3,6 @@ "name": "Select", "documentation": "https://www.home-assistant.io/integrations/select", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/sensor/manifest.json b/homeassistant/components/sensor/manifest.json index 4726ac790a7..f2057cf3012 100644 --- a/homeassistant/components/sensor/manifest.json +++ b/homeassistant/components/sensor/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/sensor", "codeowners": ["@home-assistant/core"], "quality_scale": "internal", - "after_dependencies": ["recorder"] + "after_dependencies": ["recorder"], + "integration_type": "entity" } diff --git a/homeassistant/components/siren/manifest.json b/homeassistant/components/siren/manifest.json index a3f3989e3f1..58b16ed6880 100644 --- a/homeassistant/components/siren/manifest.json +++ b/homeassistant/components/siren/manifest.json @@ -3,5 +3,6 @@ "name": "Siren", "documentation": "https://www.home-assistant.io/integrations/siren", "codeowners": ["@home-assistant/core", "@raman325"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 88e3d0f4286..e403867226a 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -8,5 +8,6 @@ "codeowners": [], "quality_scale": "internal", "iot_class": "local_push", - "loggers": ["async_upnp_client"] + "loggers": ["async_upnp_client"], + "integration_type": "system" } diff --git a/homeassistant/components/stt/manifest.json b/homeassistant/components/stt/manifest.json index 43c5c8684a3..2d9da38af89 100644 --- a/homeassistant/components/stt/manifest.json +++ b/homeassistant/components/stt/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/stt", "dependencies": ["http"], "codeowners": ["@pvizeli"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/switch/manifest.json b/homeassistant/components/switch/manifest.json index 929c617e46a..d2f64327285 100644 --- a/homeassistant/components/switch/manifest.json +++ b/homeassistant/components/switch/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/switch", "after_dependencies": ["switch_as_x"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/system_health/manifest.json b/homeassistant/components/system_health/manifest.json index 4109855d466..9fb033abc21 100644 --- a/homeassistant/components/system_health/manifest.json +++ b/homeassistant/components/system_health/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/system_health", "dependencies": ["http"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/trace/manifest.json b/homeassistant/components/trace/manifest.json index 572dff17b03..79164268c73 100644 --- a/homeassistant/components/trace/manifest.json +++ b/homeassistant/components/trace/manifest.json @@ -3,5 +3,6 @@ "name": "Trace", "documentation": "https://www.home-assistant.io/integrations/automation", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/tts/manifest.json b/homeassistant/components/tts/manifest.json index f81d112e825..f3b16cafac5 100644 --- a/homeassistant/components/tts/manifest.json +++ b/homeassistant/components/tts/manifest.json @@ -7,5 +7,6 @@ "after_dependencies": ["media_player"], "codeowners": ["@pvizeli"], "quality_scale": "internal", - "loggers": ["mutagen"] + "loggers": ["mutagen"], + "integration_type": "entity" } diff --git a/homeassistant/components/update/manifest.json b/homeassistant/components/update/manifest.json index f5fe74c9d02..44535a5d998 100644 --- a/homeassistant/components/update/manifest.json +++ b/homeassistant/components/update/manifest.json @@ -3,5 +3,6 @@ "name": "Update", "documentation": "https://www.home-assistant.io/integrations/update", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/usb/manifest.json b/homeassistant/components/usb/manifest.json index 22dca558379..792be3dcb59 100644 --- a/homeassistant/components/usb/manifest.json +++ b/homeassistant/components/usb/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@bdraco"], "dependencies": ["websocket_api"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "integration_type": "system" } diff --git a/homeassistant/components/vacuum/manifest.json b/homeassistant/components/vacuum/manifest.json index ee4fa6a471e..28737a59750 100644 --- a/homeassistant/components/vacuum/manifest.json +++ b/homeassistant/components/vacuum/manifest.json @@ -3,5 +3,6 @@ "name": "Vacuum", "documentation": "https://www.home-assistant.io/integrations/vacuum", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/water_heater/manifest.json b/homeassistant/components/water_heater/manifest.json index ac00bc64210..63f9f513847 100644 --- a/homeassistant/components/water_heater/manifest.json +++ b/homeassistant/components/water_heater/manifest.json @@ -3,5 +3,6 @@ "name": "Water Heater", "documentation": "https://www.home-assistant.io/integrations/water_heater", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/weather/manifest.json b/homeassistant/components/weather/manifest.json index cbf04af989d..6d1d1665124 100644 --- a/homeassistant/components/weather/manifest.json +++ b/homeassistant/components/weather/manifest.json @@ -3,5 +3,6 @@ "name": "Weather", "documentation": "https://www.home-assistant.io/integrations/weather", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/websocket_api/manifest.json b/homeassistant/components/websocket_api/manifest.json index 66dd76af769..f40d2940561 100644 --- a/homeassistant/components/websocket_api/manifest.json +++ b/homeassistant/components/websocket_api/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/websocket_api", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index ec670558e66..5fcb514ea51 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -7,5 +7,6 @@ "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push", - "loggers": ["zeroconf"] + "loggers": ["zeroconf"], + "integration_type": "system" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index fe58a793c54..b9989233b30 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -105,10 +105,6 @@ "iot_class": "cloud_polling", "name": "Aladdin Connect" }, - "alarm_control_panel": { - "config_flow": false, - "iot_class": null - }, "alarmdecoder": { "config_flow": true, "iot_class": "local_push", @@ -209,11 +205,6 @@ "iot_class": "local_polling", "name": "APC UPS Daemon" }, - "api": { - "config_flow": false, - "iot_class": null, - "name": "Home Assistant API" - }, "apple": { "name": "Apple", "integrations": { @@ -248,10 +239,6 @@ } } }, - "application_credentials": { - "config_flow": false, - "iot_class": null - }, "apprise": { "config_flow": false, "iot_class": "cloud_push", @@ -376,15 +363,6 @@ "iot_class": "cloud_polling", "name": "Aussie Broadband" }, - "auth": { - "config_flow": false, - "iot_class": null, - "name": "Auth" - }, - "automation": { - "config_flow": false, - "iot_class": null - }, "avion": { "config_flow": false, "iot_class": "assumed_state", @@ -400,11 +378,6 @@ "iot_class": "local_push", "name": "Axis" }, - "backup": { - "config_flow": false, - "iot_class": "calculated", - "name": "Backup" - }, "baf": { "config_flow": true, "iot_class": "local_push", @@ -435,10 +408,6 @@ "iot_class": "local_polling", "name": "BeeWi SmartClim BLE sensor" }, - "binary_sensor": { - "config_flow": false, - "iot_class": null - }, "bitcoin": { "config_flow": false, "iot_class": "cloud_polling", @@ -484,11 +453,6 @@ "iot_class": "local_push", "name": "BlueMaestro" }, - "blueprint": { - "config_flow": false, - "iot_class": null, - "name": "Blueprint" - }, "bluesound": { "config_flow": false, "iot_class": "local_polling", @@ -574,23 +538,11 @@ "iot_class": "cloud_polling", "name": "Buienradar" }, - "button": { - "config_flow": false, - "iot_class": null - }, "caldav": { "config_flow": false, "iot_class": "cloud_polling", "name": "CalDAV" }, - "calendar": { - "config_flow": false, - "iot_class": null - }, - "camera": { - "config_flow": false, - "iot_class": null - }, "canary": { "config_flow": true, "iot_class": "cloud_polling", @@ -660,10 +612,6 @@ } } }, - "climate": { - "config_flow": false, - "iot_class": null - }, "cloud": { "config_flow": false, "iot_class": "cloud_push", @@ -719,24 +667,11 @@ "iot_class": "local_polling", "name": "Concord232" }, - "config": { - "config_flow": false, - "iot_class": null, - "name": "Configuration" - }, - "configurator": { - "config_flow": false, - "iot_class": null - }, "control4": { "config_flow": true, "iot_class": "local_polling", "name": "Control4" }, - "conversation": { - "config_flow": false, - "iot_class": "local_push" - }, "coolmaster": { "config_flow": true, "iot_class": "local_polling", @@ -747,10 +682,6 @@ "iot_class": "cloud_polling", "name": "Coronavirus (COVID-19)" }, - "cover": { - "config_flow": false, - "iot_class": null - }, "cpuspeed": { "config_flow": true, "iot_class": "local_push" @@ -815,11 +746,6 @@ "iot_class": "cloud_polling", "name": "Leviton Decora Wi-Fi" }, - "default_config": { - "config_flow": false, - "iot_class": null, - "name": "Default Config" - }, "delijn": { "config_flow": false, "iot_class": "cloud_polling", @@ -859,20 +785,11 @@ "iot_class": "cloud_polling", "name": "Deutsche Bahn" }, - "device_automation": { - "config_flow": false, - "iot_class": null, - "name": "Device Automation" - }, "device_sun_light_trigger": { "config_flow": false, "iot_class": "calculated", "name": "Presence-based Lights" }, - "device_tracker": { - "config_flow": false, - "iot_class": null - }, "devolo": { "name": "devolo", "integrations": { @@ -893,15 +810,6 @@ "iot_class": "cloud_polling", "name": "Dexcom" }, - "dhcp": { - "config_flow": false, - "iot_class": "local_push", - "name": "DHCP Discovery" - }, - "diagnostics": { - "config_flow": false, - "iot_class": null - }, "digital_ocean": { "config_flow": false, "iot_class": "local_polling", @@ -922,11 +830,6 @@ "iot_class": "cloud_push", "name": "Discord" }, - "discovery": { - "config_flow": false, - "iot_class": null, - "name": "Discovery" - }, "dlib_face_detect": { "config_flow": false, "iot_class": "local_push", @@ -1171,10 +1074,6 @@ "config_flow": true, "iot_class": "local_push" }, - "energy": { - "config_flow": false, - "iot_class": "calculated" - }, "enigma2": { "config_flow": false, "iot_class": "local_polling", @@ -1295,10 +1194,6 @@ "iot_class": "local_polling", "name": "Fail2Ban" }, - "fan": { - "config_flow": false, - "iot_class": null - }, "fastdotcom": { "config_flow": false, "iot_class": "cloud_polling", @@ -1344,11 +1239,6 @@ "iot_class": "local_polling", "name": "File" }, - "file_upload": { - "config_flow": false, - "iot_class": null, - "name": "File Upload" - }, "filesize": { "config_flow": true, "iot_class": "local_polling" @@ -1528,11 +1418,6 @@ "iot_class": "local_polling", "name": "Fronius" }, - "frontend": { - "config_flow": false, - "iot_class": null, - "name": "Home Assistant Frontend" - }, "frontier_silicon": { "config_flow": false, "iot_class": "local_polling", @@ -1587,11 +1472,6 @@ "iot_class": "cloud_polling", "name": "GeoJSON" }, - "geo_location": { - "config_flow": false, - "iot_class": null, - "name": "Geolocation" - }, "geo_rss_events": { "config_flow": false, "iot_class": "cloud_polling", @@ -1815,21 +1695,11 @@ "iot_class": "cloud_polling", "name": "Habitica" }, - "hardware": { - "config_flow": false, - "iot_class": null, - "name": "Hardware" - }, "harman_kardon_avr": { "config_flow": false, "iot_class": "local_polling", "name": "Harman Kardon AVR" }, - "hassio": { - "config_flow": false, - "iot_class": "local_polling", - "name": "Home Assistant Supervisor" - }, "haveibeenpwned": { "config_flow": false, "iot_class": "cloud_polling", @@ -1875,11 +1745,6 @@ "iot_class": "local_polling", "name": "Hisense AEH-W4A1" }, - "history": { - "config_flow": false, - "iot_class": null, - "name": "History" - }, "history_stats": { "config_flow": false, "iot_class": "local_polling", @@ -1910,11 +1775,6 @@ "iot_class": "cloud_polling", "name": "Legrand Home+ Control" }, - "homeassistant": { - "config_flow": false, - "iot_class": null, - "name": "Home Assistant Core Integration" - }, "homeassistant_alerts": { "config_flow": false, "iot_class": null, @@ -1975,11 +1835,6 @@ "iot_class": "cloud_push", "name": "HTML5 Push Notifications" }, - "http": { - "config_flow": false, - "iot_class": "local_push", - "name": "HTTP" - }, "huawei_lte": { "config_flow": true, "iot_class": "local_polling", @@ -1990,10 +1845,6 @@ "iot_class": "cloud_polling", "name": "Huisbaasje" }, - "humidifier": { - "config_flow": false, - "iot_class": null - }, "hunterdouglas_powerview": { "config_flow": true, "iot_class": "local_polling", @@ -2069,15 +1920,6 @@ "iot_class": "local_push", "name": "IHC Controller" }, - "image": { - "config_flow": false, - "iot_class": null, - "name": "Image" - }, - "image_processing": { - "config_flow": false, - "iot_class": null - }, "imap": { "config_flow": false, "iot_class": "cloud_push", @@ -2113,11 +1955,6 @@ "iot_class": "local_polling", "name": "IntelliFire" }, - "intent": { - "config_flow": false, - "iot_class": null, - "name": "Intent" - }, "intent_script": { "config_flow": false, "iot_class": null, @@ -2403,10 +2240,6 @@ "iot_class": "cloud_push", "name": "LIFX Cloud" }, - "light": { - "config_flow": false, - "iot_class": null - }, "lightwave": { "config_flow": false, "iot_class": "assumed_state", @@ -2466,25 +2299,11 @@ "iot_class": "local_push", "name": "Locative" }, - "lock": { - "config_flow": false, - "iot_class": null - }, - "logbook": { - "config_flow": false, - "iot_class": null, - "name": "Logbook" - }, "logentries": { "config_flow": false, "iot_class": "cloud_push", "name": "Logentries" }, - "logger": { - "config_flow": false, - "iot_class": null, - "name": "Logger" - }, "logi_circle": { "config_flow": true, "iot_class": "cloud_polling", @@ -2525,11 +2344,6 @@ "iot_class": "local_push", "name": "LOOKin" }, - "lovelace": { - "config_flow": false, - "iot_class": null, - "name": "Dashboards" - }, "luftdaten": { "config_flow": true, "iot_class": "cloud_polling", @@ -2570,10 +2384,6 @@ "iot_class": "cloud_polling", "name": "Magicseaweed" }, - "mailbox": { - "config_flow": false, - "iot_class": null - }, "mailgun": { "config_flow": true, "iot_class": "cloud_push", @@ -2619,15 +2429,6 @@ "iot_class": "calculated", "name": "Media Extractor" }, - "media_player": { - "config_flow": false, - "iot_class": null - }, - "media_source": { - "config_flow": false, - "iot_class": null, - "name": "Media Source" - }, "mediaroom": { "config_flow": false, "iot_class": "local_polling", @@ -2905,11 +2706,6 @@ "iot_class": "cloud_polling", "name": "MVG" }, - "my": { - "config_flow": false, - "iot_class": null, - "name": "My Home Assistant" - }, "mycroft": { "config_flow": false, "iot_class": "local_push", @@ -3000,11 +2796,6 @@ "iot_class": "local_polling", "name": "Netio" }, - "network": { - "config_flow": false, - "iot_class": "local_push", - "name": "Network Configuration" - }, "neurio_energy": { "config_flow": false, "iot_class": "cloud_polling", @@ -3094,10 +2885,6 @@ "iot_class": "cloud_polling", "name": "Om Luftkvalitet i Norge (Norway Air)" }, - "notify": { - "config_flow": false, - "iot_class": null - }, "notify_events": { "config_flow": false, "iot_class": "cloud_push", @@ -3133,10 +2920,6 @@ "iot_class": "local_push", "name": "Numato USB GPIO Expander" }, - "number": { - "config_flow": false, - "iot_class": null - }, "nut": { "config_flow": true, "iot_class": "local_polling", @@ -3192,11 +2975,6 @@ "iot_class": "cloud_polling", "name": "Hayward Omnilogic" }, - "onboarding": { - "config_flow": false, - "iot_class": null, - "name": "Home Assistant Onboarding" - }, "oncue": { "config_flow": true, "iot_class": "cloud_polling", @@ -3407,10 +3185,6 @@ "iot_class": "local_push", "name": "Persistent Notification" }, - "person": { - "config_flow": false, - "iot_class": "calculated" - }, "philips": { "name": "Philips", "integrations": { @@ -3728,11 +3502,6 @@ "iot_class": "cloud_polling", "name": "ReCollect Waste" }, - "recorder": { - "config_flow": false, - "iot_class": "local_push", - "name": "Recorder" - }, "recswitch": { "config_flow": false, "iot_class": "local_polling", @@ -3753,20 +3522,11 @@ "iot_class": "cloud_push", "name": "Remember The Milk" }, - "remote": { - "config_flow": false, - "iot_class": null - }, "renault": { "config_flow": true, "iot_class": "cloud_polling", "name": "Renault" }, - "repairs": { - "config_flow": false, - "iot_class": null, - "name": "Repairs" - }, "repetier": { "config_flow": false, "iot_class": "local_polling", @@ -3892,11 +3652,6 @@ "iot_class": "local_polling", "name": "SABnzbd" }, - "safe_mode": { - "config_flow": false, - "iot_class": null, - "name": "Safe Mode" - }, "saj": { "config_flow": false, "iot_class": "local_polling", @@ -3927,10 +3682,6 @@ "iot_class": "local_push", "name": "Satel Integra" }, - "scene": { - "config_flow": false, - "iot_class": null - }, "schluter": { "config_flow": false, "iot_class": "cloud_polling", @@ -3946,29 +3697,16 @@ "iot_class": "local_polling", "name": "Pentair ScreenLogic" }, - "script": { - "config_flow": false, - "iot_class": null - }, "scsgate": { "config_flow": false, "iot_class": "local_polling", "name": "SCSGate" }, - "search": { - "config_flow": false, - "iot_class": null, - "name": "Search" - }, "season": { "config_flow": true, "iot_class": "local_polling", "name": "Season" }, - "select": { - "config_flow": false, - "iot_class": null - }, "sendgrid": { "config_flow": false, "iot_class": "cloud_push", @@ -3989,10 +3727,6 @@ "iot_class": "cloud_polling", "name": "Sensibo" }, - "sensor": { - "config_flow": false, - "iot_class": null - }, "sensorpro": { "config_flow": true, "iot_class": "local_push", @@ -4107,10 +3841,6 @@ "iot_class": "cloud_push", "name": "Sinch SMS" }, - "siren": { - "config_flow": false, - "iot_class": null - }, "sisyphus": { "config_flow": false, "iot_class": "local_push", @@ -4326,11 +4056,6 @@ "iot_class": "cloud_polling", "name": "SRP Energy" }, - "ssdp": { - "config_flow": false, - "iot_class": "local_push", - "name": "Simple Service Discovery Protocol (SSDP)" - }, "starline": { "config_flow": true, "iot_class": "cloud_polling", @@ -4386,11 +4111,6 @@ "iot_class": "cloud_polling", "name": "StreamLabs" }, - "stt": { - "config_flow": false, - "iot_class": null, - "name": "Speech-to-Text (STT)" - }, "subaru": { "config_flow": true, "iot_class": "cloud_polling", @@ -4435,10 +4155,6 @@ "iot_class": "local_polling", "name": "Swisscom Internet-Box" }, - "switch": { - "config_flow": false, - "iot_class": null - }, "switchbee": { "config_flow": true, "iot_class": "local_polling", @@ -4494,10 +4210,6 @@ "iot_class": "local_push", "name": "System Bridge" }, - "system_health": { - "config_flow": false, - "iot_class": null - }, "system_log": { "config_flow": false, "iot_class": null, @@ -4753,11 +4465,6 @@ "iot_class": "local_polling", "name": "Traccar" }, - "trace": { - "config_flow": false, - "iot_class": null, - "name": "Trace" - }, "tractive": { "config_flow": true, "iot_class": "cloud_push", @@ -4808,11 +4515,6 @@ "iot_class": "local_push", "name": "Trend" }, - "tts": { - "config_flow": false, - "iot_class": null, - "name": "Text-to-Speech (TTS)" - }, "tuya": { "config_flow": true, "iot_class": "cloud_push", @@ -4913,10 +4615,6 @@ "iot_class": "cloud_polling", "name": "UpCloud" }, - "update": { - "config_flow": false, - "iot_class": null - }, "upnp": { "config_flow": true, "iot_class": "local_polling", @@ -4931,11 +4629,6 @@ "iot_class": "cloud_polling", "name": "UptimeRobot" }, - "usb": { - "config_flow": false, - "iot_class": "local_push", - "name": "USB Discovery" - }, "usgs_earthquakes_feed": { "config_flow": false, "iot_class": "cloud_polling", @@ -4946,10 +4639,6 @@ "iot_class": "local_polling", "name": "Ubiquiti UniFi Video" }, - "vacuum": { - "config_flow": false, - "iot_class": null - }, "vallox": { "config_flow": true, "iot_class": "local_polling", @@ -5090,11 +4779,6 @@ "iot_class": "cloud_polling", "name": "World Air Quality Index (WAQI)" }, - "water_heater": { - "config_flow": false, - "iot_class": null, - "name": "Water Heater" - }, "waterfurnace": { "config_flow": false, "iot_class": "cloud_polling", @@ -5109,21 +4793,11 @@ "config_flow": true, "iot_class": "cloud_polling" }, - "weather": { - "config_flow": false, - "iot_class": null, - "name": "Weather" - }, "webhook": { "config_flow": false, "iot_class": null, "name": "Webhook" }, - "websocket_api": { - "config_flow": false, - "iot_class": null, - "name": "Home Assistant WebSocket API" - }, "wemo": { "config_flow": true, "iot_class": "local_push", @@ -5344,11 +5018,6 @@ "iot_class": "local_polling", "name": "Zengge" }, - "zeroconf": { - "config_flow": false, - "iot_class": "local_push", - "name": "Zero-configuration networking (zeroconf)" - }, "zerproc": { "config_flow": true, "iot_class": "local_polling", @@ -5496,35 +5165,18 @@ } }, "translated_name": [ - "alarm_control_panel", - "application_credentials", "aurora", - "automation", - "binary_sensor", - "button", - "calendar", - "camera", "cert_expiry", - "climate", - "configurator", - "conversation", - "cover", "cpuspeed", "demo", "derivative", - "device_tracker", - "diagnostics", "emulated_roku", - "energy", - "fan", "filesize", "garages_amsterdam", "google_travel_time", "group", "growatt_server", "homekit_controller", - "humidifier", - "image_processing", "input_boolean", "input_datetime", "input_number", @@ -5532,41 +5184,24 @@ "input_text", "integration", "islamic_prayer_times", - "light", "local_ip", - "lock", - "mailbox", - "media_player", "min_max", "mobile_app", "moehlenhoff_alpha2", "moon", "nmap_tracker", - "notify", - "number", - "person", "plant", "proximity", - "remote", "rpi_power", - "scene", "schedule", - "script", - "select", - "sensor", "shopping_list", - "siren", "sun", - "switch", "switch_as_x", - "system_health", "tag", "threshold", "tod", - "update", "uptime", "utility_meter", - "vacuum", "waze_travel_time" ] } From 27413cee19cb587ac7993f356e8338666c13ecc5 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 4 Oct 2022 10:40:49 -0400 Subject: [PATCH 158/985] Bump zwave_js lib to 0.43.0 and fix multi-file firmware updates (#79342) --- homeassistant/components/zwave_js/api.py | 31 +-- .../components/zwave_js/manifest.json | 2 +- homeassistant/components/zwave_js/update.py | 77 +++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_api.py | 47 +++-- tests/components/zwave_js/test_update.py | 182 +++--------------- 7 files changed, 111 insertions(+), 232 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 7ceca062ee4..4a5b233a2f0 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable import dataclasses from functools import partial, wraps -from typing import Any, Literal, cast +from typing import Any, Literal from aiohttp import web, web_exceptions, web_request import voluptuous as vol @@ -27,7 +27,7 @@ from zwave_js_server.exceptions import ( NotFoundError, SetValueFailed, ) -from zwave_js_server.firmware import begin_firmware_update +from zwave_js_server.firmware import update_firmware from zwave_js_server.model.controller import ( ControllerStatistics, InclusionGrant, @@ -36,8 +36,9 @@ from zwave_js_server.model.controller import ( ) from zwave_js_server.model.driver import Driver from zwave_js_server.model.firmware import ( - FirmwareUpdateFinished, + FirmwareUpdateData, FirmwareUpdateProgress, + FirmwareUpdateResult, ) from zwave_js_server.model.log_config import LogConfig from zwave_js_server.model.log_message import LogMessage @@ -1897,11 +1898,14 @@ async def websocket_is_node_firmware_update_in_progress( def _get_firmware_update_progress_dict( progress: FirmwareUpdateProgress, -) -> dict[str, int]: +) -> dict[str, int | float]: """Get a dictionary of firmware update progress.""" return { + "current_file": progress.current_file, + "total_files": progress.total_files, "sent_fragments": progress.sent_fragments, "total_fragments": progress.total_fragments, + "progress": progress.progress, } @@ -1943,14 +1947,16 @@ async def websocket_subscribe_firmware_update_status( @callback def forward_finished(event: dict) -> None: - finished: FirmwareUpdateFinished = event["firmware_update_finished"] + finished: FirmwareUpdateResult = event["firmware_update_finished"] connection.send_message( websocket_api.event_message( msg[ID], { "event": event["event"], "status": finished.status, + "success": finished.success, "wait_time": finished.wait_time, + "reinterview": finished.reinterview, }, ) ) @@ -2052,21 +2058,20 @@ class FirmwareUploadView(HomeAssistantView): if "file" not in data or not isinstance(data["file"], web_request.FileField): raise web_exceptions.HTTPBadRequest - target = None - if "target" in data: - target = int(cast(str, data["target"])) - uploaded_file: web_request.FileField = data["file"] try: - await begin_firmware_update( + await update_firmware( node.client.ws_server_url, node, - uploaded_file.filename, - await hass.async_add_executor_job(uploaded_file.file.read), + [ + FirmwareUpdateData( + uploaded_file.filename, + await hass.async_add_executor_job(uploaded_file.file.read), + ) + ], async_get_clientsession(hass), additional_user_agent_components=USER_AGENT, - target=target, ) except BaseZwaveJSServerError as err: raise web_exceptions.HTTPBadRequest(reason=str(err)) from err diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 8f0c93f6c3e..5b085ab0bb3 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.42.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.43.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 52c7e0d46e1..0c458d6e1a8 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio from collections.abc import Callable from datetime import datetime, timedelta -from math import floor from typing import Any from awesomeversion import AwesomeVersion @@ -13,10 +12,9 @@ from zwave_js_server.const import NodeStatus from zwave_js_server.exceptions import BaseZwaveJSServerError, FailedZWaveCommand from zwave_js_server.model.driver import Driver from zwave_js_server.model.firmware import ( - FirmwareUpdateFinished, FirmwareUpdateInfo, FirmwareUpdateProgress, - FirmwareUpdateStatus, + FirmwareUpdateResult, ) from zwave_js_server.model.node import Node as ZwaveNode @@ -91,9 +89,8 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._poll_unsub: Callable[[], None] | None = None self._progress_unsub: Callable[[], None] | None = None self._finished_unsub: Callable[[], None] | None = None - self._num_files_installed: int = 0 self._finished_event = asyncio.Event() - self._finished_status: FirmwareUpdateStatus | None = None + self._result: FirmwareUpdateResult | None = None # Entity class attributes self._attr_name = "Firmware" @@ -115,25 +112,14 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): progress: FirmwareUpdateProgress = event["firmware_update_progress"] if not self._latest_version_firmware: return - # We will assume that each file in the firmware update represents an equal - # percentage of the overall progress. This is likely not true because each file - # may be a different size, but it's the best we can do since we don't know the - # total number of fragments across all files. - self._attr_in_progress = floor( - 100 - * ( - self._num_files_installed - + (progress.sent_fragments / progress.total_fragments) - ) - / len(self._latest_version_firmware.files) - ) + self._attr_in_progress = int(progress.progress) self.async_write_ha_state() @callback def _update_finished(self, event: dict[str, Any]) -> None: """Update install progress on event.""" - finished: FirmwareUpdateFinished = event["firmware_update_finished"] - self._finished_status = finished.status + result: FirmwareUpdateResult = event["firmware_update_finished"] + self._result = result self._finished_event.set() @callback @@ -149,10 +135,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._finished_unsub() self._finished_unsub = None - self._finished_status = None + self._result = None self._finished_event.clear() - self._num_files_installed = 0 - self._attr_in_progress = 0 + self._attr_in_progress = False if write_state: self.async_write_ha_state() @@ -235,41 +220,23 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): "firmware update finished", self._update_finished ) - for file in firmware.files: - try: - await self.driver.controller.async_begin_ota_firmware_update( - self.node, file - ) - except BaseZwaveJSServerError as err: - self._unsub_firmware_events_and_reset_progress() - raise HomeAssistantError(err) from err - - # We need to block until we receive the `firmware update finished` event - await self._finished_event.wait() - # Clear the event so that a second firmware update blocks again - self._finished_event.clear() - assert self._finished_status is not None - - # If status is not OK, we should throw an error to let the user know - if self._finished_status not in ( - FirmwareUpdateStatus.OK_NO_RESTART, - FirmwareUpdateStatus.OK_RESTART_PENDING, - FirmwareUpdateStatus.OK_WAITING_FOR_ACTIVATION, - ): - status = self._finished_status - self._unsub_firmware_events_and_reset_progress() - raise HomeAssistantError(status.name.replace("_", " ").title()) - - # If we get here, the firmware installation was successful and we need to - # update progress accordingly - self._num_files_installed += 1 - self._attr_in_progress = floor( - 100 * self._num_files_installed / len(firmware.files) + try: + await self.driver.controller.async_firmware_update_ota( + self.node, firmware.files ) + except BaseZwaveJSServerError as err: + self._unsub_firmware_events_and_reset_progress() + raise HomeAssistantError(err) from err - # Clear the status so we can get a new one - self._finished_status = None - self.async_write_ha_state() + # We need to block until we receive the `firmware update finished` event + await self._finished_event.wait() + assert self._result is not None + + # If the update was not successful, we should throw an error to let the user know + if not self._result.success: + error_msg = self._result.status.name.replace("_", " ").title() + self._unsub_firmware_events_and_reset_progress() + raise HomeAssistantError(error_msg) # If we get here, all files were installed successfully self._attr_installed_version = self._attr_latest_version = firmware.version diff --git a/requirements_all.txt b/requirements_all.txt index 7bc44ebacf3..08537c10e56 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2622,7 +2622,7 @@ zigpy==0.51.1 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.42.0 +zwave-js-server-python==0.43.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e1692d9380b..271a40151fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1814,7 +1814,7 @@ zigpy-znp==0.9.0 zigpy==0.51.1 # homeassistant.components.zwave_js -zwave-js-server-python==0.42.0 +zwave-js-server-python==0.43.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.6 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index b55f4941a49..caea283e25c 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -28,6 +28,7 @@ from zwave_js_server.model.controller import ( ProvisioningEntry, QRProvisioningInformation, ) +from zwave_js_server.model.firmware import FirmwareUpdateData from zwave_js_server.model.node import Node from homeassistant.components.websocket_api import ERR_INVALID_FORMAT, ERR_NOT_FOUND @@ -2815,18 +2816,20 @@ async def test_firmware_upload_view( client = await hass_client() device = get_device(hass, multisensor_6) with patch( - "homeassistant.components.zwave_js.api.begin_firmware_update", + "homeassistant.components.zwave_js.api.update_firmware", ) as mock_cmd, patch.dict( "homeassistant.components.zwave_js.api.USER_AGENT", {"HomeAssistant": "0.0.0"}, ): resp = await client.post( f"/api/zwave_js/firmware/upload/{device.id}", - data={"file": firmware_file, "target": "15"}, + data={"file": firmware_file}, + ) + assert mock_cmd.call_args[0][1:3] == ( + multisensor_6, + [FirmwareUpdateData("file", bytes(10))], ) - assert mock_cmd.call_args[0][1:4] == (multisensor_6, "file", bytes(10)) assert mock_cmd.call_args[1] == { - "target": 15, "additional_user_agent_components": {"HomeAssistant": "0.0.0"}, } assert json.loads(await resp.text()) is None @@ -2839,7 +2842,7 @@ async def test_firmware_upload_view_failed_command( client = await hass_client() device = get_device(hass, multisensor_6) with patch( - "homeassistant.components.zwave_js.api.begin_firmware_update", + "homeassistant.components.zwave_js.api.update_firmware", side_effect=FailedCommand("test", "test"), ): resp = await client.post( @@ -3502,8 +3505,13 @@ async def test_subscribe_firmware_update_status( "source": "node", "event": "firmware update progress", "nodeId": multisensor_6.node_id, - "sentFragments": 1, - "totalFragments": 10, + "progress": { + "currentFile": 1, + "totalFiles": 1, + "sentFragments": 1, + "totalFragments": 10, + "progress": 10.0, + }, }, ) multisensor_6.receive_event(event) @@ -3511,8 +3519,11 @@ async def test_subscribe_firmware_update_status( msg = await ws_client.receive_json() assert msg["event"] == { "event": "firmware update progress", + "current_file": 1, + "total_files": 1, "sent_fragments": 1, "total_fragments": 10, + "progress": 10.0, } event = Event( @@ -3521,8 +3532,12 @@ async def test_subscribe_firmware_update_status( "source": "node", "event": "firmware update finished", "nodeId": multisensor_6.node_id, - "status": 255, - "waitTime": 10, + "result": { + "status": 255, + "success": True, + "waitTime": 10, + "reInterview": False, + }, }, ) multisensor_6.receive_event(event) @@ -3531,7 +3546,9 @@ async def test_subscribe_firmware_update_status( assert msg["event"] == { "event": "firmware update finished", "status": 255, + "success": True, "wait_time": 10, + "reinterview": False, } @@ -3551,8 +3568,13 @@ async def test_subscribe_firmware_update_status_initial_value( "source": "node", "event": "firmware update progress", "nodeId": multisensor_6.node_id, - "sentFragments": 1, - "totalFragments": 10, + "progress": { + "currentFile": 1, + "totalFiles": 1, + "sentFragments": 1, + "totalFragments": 10, + "progress": 10.0, + }, }, ) multisensor_6.receive_event(event) @@ -3574,8 +3596,11 @@ async def test_subscribe_firmware_update_status_initial_value( msg = await ws_client.receive_json() assert msg["event"] == { "event": "firmware update progress", + "current_file": 1, + "total_files": 1, "sent_fragments": 1, "total_fragments": 10, + "progress": 10.0, } diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index b2517c3dd34..4c00c1c9a3a 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -324,7 +324,7 @@ async def test_update_entity_progress( assert attrs[ATTR_LATEST_VERSION] == "11.2.4" client.async_send_command.reset_mock() - client.async_send_command.return_value = None + client.async_send_command.return_value = {"success": False} # Test successful install call without a version install_task = hass.async_create_task( @@ -352,8 +352,13 @@ async def test_update_entity_progress( "source": "node", "event": "firmware update progress", "nodeId": node.node_id, - "sentFragments": 1, - "totalFragments": 20, + "progress": { + "currentFile": 1, + "totalFiles": 1, + "sentFragments": 1, + "totalFragments": 20, + "progress": 5.0, + }, }, ) node.receive_event(event) @@ -370,7 +375,11 @@ async def test_update_entity_progress( "source": "node", "event": "firmware update finished", "nodeId": node.node_id, - "status": FirmwareUpdateStatus.OK_NO_RESTART, + "result": { + "status": FirmwareUpdateStatus.OK_NO_RESTART, + "success": True, + "reInterview": False, + }, }, ) @@ -381,142 +390,7 @@ async def test_update_entity_progress( state = hass.states.get(UPDATE_ENTITY) assert state attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] == 0 - assert attrs[ATTR_INSTALLED_VERSION] == "11.2.4" - assert attrs[ATTR_LATEST_VERSION] == "11.2.4" - assert state.state == STATE_OFF - - await install_task - - -async def test_update_entity_progress_multiple( - hass, - client, - climate_radio_thermostat_ct100_plus_different_endpoints, - integration, -): - """Test update entity progress with multiple files.""" - node = climate_radio_thermostat_ct100_plus_different_endpoints - client.async_send_command.return_value = FIRMWARE_UPDATE_MULTIPLE_FILES - - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) - await hass.async_block_till_done() - - state = hass.states.get(UPDATE_ENTITY) - assert state - assert state.state == STATE_ON - attrs = state.attributes - assert attrs[ATTR_INSTALLED_VERSION] == "10.7" - assert attrs[ATTR_LATEST_VERSION] == "11.2.4" - - client.async_send_command.reset_mock() - client.async_send_command.return_value = None - - # Test successful install call without a version - install_task = hass.async_create_task( - hass.services.async_call( - UPDATE_DOMAIN, - SERVICE_INSTALL, - { - ATTR_ENTITY_ID: UPDATE_ENTITY, - }, - blocking=True, - ) - ) - - # Sleep so that task starts - await asyncio.sleep(0.1) - - state = hass.states.get(UPDATE_ENTITY) - assert state - attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] is True - - node.receive_event( - Event( - type="firmware update progress", - data={ - "source": "node", - "event": "firmware update progress", - "nodeId": node.node_id, - "sentFragments": 1, - "totalFragments": 20, - }, - ) - ) - - # Block so HA can do its thing - await asyncio.sleep(0) - - # Validate that the progress is updated (two files means progress is 50% of 5) - state = hass.states.get(UPDATE_ENTITY) - assert state - attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] == 2 - - node.receive_event( - Event( - type="firmware update finished", - data={ - "source": "node", - "event": "firmware update finished", - "nodeId": node.node_id, - "status": FirmwareUpdateStatus.OK_NO_RESTART, - }, - ) - ) - - # Block so HA can do its thing - await asyncio.sleep(0) - - # One file done, progress should be 50% - state = hass.states.get(UPDATE_ENTITY) - assert state - attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] == 50 - - node.receive_event( - Event( - type="firmware update progress", - data={ - "source": "node", - "event": "firmware update progress", - "nodeId": node.node_id, - "sentFragments": 1, - "totalFragments": 20, - }, - ) - ) - - # Block so HA can do its thing - await asyncio.sleep(0) - - # Validate that the progress is updated (50% + 50% of 5) - state = hass.states.get(UPDATE_ENTITY) - assert state - attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] == 52 - - node.receive_event( - Event( - type="firmware update finished", - data={ - "source": "node", - "event": "firmware update finished", - "nodeId": node.node_id, - "status": FirmwareUpdateStatus.OK_NO_RESTART, - }, - ) - ) - - # Block so HA can do its thing - await asyncio.sleep(0) - - # Validate that progress is reset and entity reflects new version - state = hass.states.get(UPDATE_ENTITY) - assert state - attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] == 0 + assert attrs[ATTR_IN_PROGRESS] is False assert attrs[ATTR_INSTALLED_VERSION] == "11.2.4" assert attrs[ATTR_LATEST_VERSION] == "11.2.4" assert state.state == STATE_OFF @@ -546,10 +420,11 @@ async def test_update_entity_install_failed( assert attrs[ATTR_LATEST_VERSION] == "11.2.4" client.async_send_command.reset_mock() - client.async_send_command.return_value = None + client.async_send_command.return_value = {"success": False} - async def call_install(): - await hass.services.async_call( + # Test install call - we expect it to finish fail + install_task = hass.async_create_task( + hass.services.async_call( UPDATE_DOMAIN, SERVICE_INSTALL, { @@ -557,9 +432,7 @@ async def test_update_entity_install_failed( }, blocking=True, ) - - # Test install call - we expect it to raise - install_task = hass.async_create_task(call_install()) + ) # Sleep so that task starts await asyncio.sleep(0.1) @@ -570,8 +443,13 @@ async def test_update_entity_install_failed( "source": "node", "event": "firmware update progress", "nodeId": node.node_id, - "sentFragments": 1, - "totalFragments": 20, + "progress": { + "currentFile": 1, + "totalFiles": 1, + "sentFragments": 1, + "totalFragments": 20, + "progress": 5.0, + }, }, ) node.receive_event(event) @@ -588,7 +466,11 @@ async def test_update_entity_install_failed( "source": "node", "event": "firmware update finished", "nodeId": node.node_id, - "status": FirmwareUpdateStatus.ERROR_TIMEOUT, + "result": { + "status": FirmwareUpdateStatus.ERROR_TIMEOUT, + "success": False, + "reInterview": False, + }, }, ) @@ -599,7 +481,7 @@ async def test_update_entity_install_failed( state = hass.states.get(UPDATE_ENTITY) assert state attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] == 0 + assert attrs[ATTR_IN_PROGRESS] is False assert attrs[ATTR_INSTALLED_VERSION] == "10.7" assert attrs[ATTR_LATEST_VERSION] == "11.2.4" assert state.state == STATE_ON From 56dd0a6867bf16aeb1f4bf4dd5ddadd73955bc2d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 4 Oct 2022 16:41:11 +0200 Subject: [PATCH 159/985] Run hassfest in pre-commit when brands changed (#79589) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 088099bf4e4..1635a7dcf12 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -113,7 +113,7 @@ repos: pass_filenames: false language: script types: [text] - files: ^(homeassistant/.+/(manifest|strings)\.json|\.coveragerc|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py)$ + files: ^(homeassistant/.+/(manifest|strings)\.json|homeassistant/brands/.*\.json|\.coveragerc|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py)$ - id: hassfest-metadata name: hassfest-metadata entry: script/run-in-env.sh python3 -m script.hassfest -p metadata From dd1463da287f591652e47b00eee0c5b77f5f5b7c Mon Sep 17 00:00:00 2001 From: HarvsG <11440490+HarvsG@users.noreply.github.com> Date: Tue, 4 Oct 2022 16:16:39 +0100 Subject: [PATCH 160/985] Refactor bayesian observations using dataclass (#79590) * refactor * remove some changes * remove typehint * improve codestyle * move docstring to comment * < 88 chars * avoid short var names * more readable * fix rename * Update homeassistant/components/bayesian/helpers.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/bayesian/binary_sensor.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/bayesian/binary_sensor.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * no intermediate * comment why set before list Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/bayesian/binary_sensor.py | 199 ++++++++++-------- homeassistant/components/bayesian/const.py | 17 ++ homeassistant/components/bayesian/helpers.py | 69 ++++++ homeassistant/components/bayesian/repairs.py | 15 +- 4 files changed, 195 insertions(+), 105 deletions(-) create mode 100644 homeassistant/components/bayesian/const.py create mode 100644 homeassistant/components/bayesian/helpers.py diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 706c7ecdfd7..190fb889553 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -35,24 +35,24 @@ from homeassistant.helpers.template import result_as_boolean from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DOMAIN, PLATFORMS +from .const import ( + ATTR_OBSERVATIONS, + ATTR_OCCURRED_OBSERVATION_ENTITIES, + ATTR_PROBABILITY, + ATTR_PROBABILITY_THRESHOLD, + CONF_OBSERVATIONS, + CONF_P_GIVEN_F, + CONF_P_GIVEN_T, + CONF_PRIOR, + CONF_PROBABILITY_THRESHOLD, + CONF_TEMPLATE, + CONF_TO_STATE, + DEFAULT_NAME, + DEFAULT_PROBABILITY_THRESHOLD, +) +from .helpers import Observation from .repairs import raise_mirrored_entries, raise_no_prob_given_false -ATTR_OBSERVATIONS = "observations" -ATTR_OCCURRED_OBSERVATION_ENTITIES = "occurred_observation_entities" -ATTR_PROBABILITY = "probability" -ATTR_PROBABILITY_THRESHOLD = "probability_threshold" - -CONF_OBSERVATIONS = "observations" -CONF_PRIOR = "prior" -CONF_TEMPLATE = "template" -CONF_PROBABILITY_THRESHOLD = "probability_threshold" -CONF_P_GIVEN_F = "prob_given_false" -CONF_P_GIVEN_T = "prob_given_true" -CONF_TO_STATE = "to_state" - -DEFAULT_NAME = "Bayesian Binary Sensor" -DEFAULT_PROBABILITY_THRESHOLD = 0.5 - _LOGGER = logging.getLogger(__name__) @@ -156,7 +156,20 @@ class BayesianBinarySensor(BinarySensorEntity): def __init__(self, name, prior, observations, probability_threshold, device_class): """Initialize the Bayesian sensor.""" self._attr_name = name - self._observations = observations + self._observations = [ + Observation( + entity_id=observation.get(CONF_ENTITY_ID), + platform=observation[CONF_PLATFORM], + prob_given_false=observation[CONF_P_GIVEN_F], + prob_given_true=observation[CONF_P_GIVEN_T], + observed=None, + to_state=observation.get(CONF_TO_STATE), + above=observation.get(CONF_ABOVE), + below=observation.get(CONF_BELOW), + value_template=observation.get(CONF_VALUE_TEMPLATE), + ) + for observation in observations + ] self._probability_threshold = probability_threshold self._attr_device_class = device_class self._attr_is_on = False @@ -230,13 +243,18 @@ class BayesianBinarySensor(BinarySensorEntity): self.entity_id, ) - observation = None + observed = None else: - observation = result_as_boolean(result) + observed = result_as_boolean(result) - for obs in self.observations_by_template[template]: - obs_entry = {"entity_id": entity, "observation": observation, **obs} - self.current_observations[obs["id"]] = obs_entry + for observation in self.observations_by_template[template]: + observation.observed = observed + + # in some cases a template may update because of the absence of an entity + if entity is not None: + observation.entity_id = str(entity) + + self.current_observations[observation.id] = observation if event: self.async_set_context(event.context) @@ -270,7 +288,7 @@ class BayesianBinarySensor(BinarySensorEntity): raise_mirrored_entries( self.hass, all_template_observations, - text=f"{self._attr_name}/{all_template_observations[0]['value_template']}", + text=f"{self._attr_name}/{all_template_observations[0].value_template}", ) @callback @@ -289,42 +307,38 @@ class BayesianBinarySensor(BinarySensorEntity): def _record_entity_observations(self, entity): local_observations = OrderedDict({}) - for entity_obs in self.observations_by_entity[entity]: - platform = entity_obs["platform"] + for observation in self.observations_by_entity[entity]: + platform = observation.platform - observation = self.observation_handlers[platform](entity_obs) + observed = self.observation_handlers[platform](observation) + observation.observed = observed - obs_entry = { - "entity_id": entity, - "observation": observation, - **entity_obs, - } - local_observations[entity_obs["id"]] = obs_entry + local_observations[observation.id] = observation return local_observations def _calculate_new_probability(self): prior = self.prior - for obs in self.current_observations.values(): - if obs is not None: - if obs["observation"] is True: + for observation in self.current_observations.values(): + if observation is not None: + if observation.observed is True: prior = update_probability( prior, - obs["prob_given_true"], - obs["prob_given_false"], + observation.prob_given_true, + observation.prob_given_false, ) - elif obs["observation"] is False: + elif observation.observed is False: prior = update_probability( prior, - 1 - obs["prob_given_true"], - 1 - obs["prob_given_false"], + 1 - observation.prob_given_true, + 1 - observation.prob_given_false, ) - elif obs["observation"] is None: - if obs["entity_id"] is not None: + elif observation.observed is None: + if observation.entity_id is not None: _LOGGER.debug( "Observation for entity '%s' returned None, it will not be used for Bayesian updating", - obs["entity_id"], + observation.entity_id, ) else: _LOGGER.debug( @@ -338,8 +352,8 @@ class BayesianBinarySensor(BinarySensorEntity): Build and return data structure of the form below. { - "sensor.sensor1": [{"id": 0, ...}, {"id": 1, ...}], - "sensor.sensor2": [{"id": 2, ...}], + "sensor.sensor1": [Observation, Observation], + "sensor.sensor2": [Observation], ... } @@ -347,21 +361,20 @@ class BayesianBinarySensor(BinarySensorEntity): for all relevant observations to be looked up via their `entity_id`. """ - observations_by_entity: dict[str, list[OrderedDict]] = {} - for i, obs in enumerate(self._observations): - obs["id"] = i + observations_by_entity: dict[str, list[Observation]] = {} + for observation in self._observations: - if "entity_id" not in obs: + if (key := observation.entity_id) is None: continue - observations_by_entity.setdefault(obs["entity_id"], []).append(obs) + observations_by_entity.setdefault(key, []).append(observation) - for li_of_dicts in observations_by_entity.values(): - if len(li_of_dicts) == 1: + for entity_observations in observations_by_entity.values(): + if len(entity_observations) == 1: continue - for ord_dict in li_of_dicts: - if ord_dict["platform"] != "state": + for observation in entity_observations: + if observation.platform != "state": continue - ord_dict["platform"] = "multi_state" + observation.platform = "multi_state" return observations_by_entity @@ -370,8 +383,8 @@ class BayesianBinarySensor(BinarySensorEntity): Build and return data structure of the form below. { - "template": [{"id": 0, ...}, {"id": 1, ...}], - "template2": [{"id": 2, ...}], + "template": [Observation, Observation], + "template2": [Observation], ... } @@ -380,20 +393,18 @@ class BayesianBinarySensor(BinarySensorEntity): """ observations_by_template = {} - for ind, obs in enumerate(self._observations): - obs["id"] = ind - - if "value_template" not in obs: + for observation in self._observations: + if observation.value_template is None: continue - template = obs.get(CONF_VALUE_TEMPLATE) - observations_by_template.setdefault(template, []).append(obs) + template = observation.value_template + observations_by_template.setdefault(template, []).append(observation) return observations_by_template def _process_numeric_state(self, entity_observation): """Return True if numeric condition is met, return False if not, return None otherwise.""" - entity = entity_observation["entity_id"] + entity = entity_observation.entity_id try: if condition.state(self.hass, entity, [STATE_UNKNOWN, STATE_UNAVAILABLE]): @@ -401,61 +412,67 @@ class BayesianBinarySensor(BinarySensorEntity): return condition.async_numeric_state( self.hass, entity, - entity_observation.get("below"), - entity_observation.get("above"), + entity_observation.below, + entity_observation.above, None, - entity_observation, + entity_observation.to_dict(), ) except ConditionError: return None def _process_state(self, entity_observation): - """Return True if state conditions are met.""" - entity = entity_observation["entity_id"] + """Return True if state conditions are met, return False if they are not. + + Returns None if the state is unavailable. + """ + + entity = entity_observation.entity_id try: if condition.state(self.hass, entity, [STATE_UNKNOWN, STATE_UNAVAILABLE]): return None - return condition.state( - self.hass, entity, entity_observation.get("to_state") - ) + return condition.state(self.hass, entity, entity_observation.to_state) except ConditionError: return None def _process_multi_state(self, entity_observation): - """Return True if state conditions are met.""" - entity = entity_observation["entity_id"] + """Return True if state conditions are met, otherwise return None. + + Never return False as all other states should have their own probabilities configured. + """ + + entity = entity_observation.entity_id try: - if condition.state(self.hass, entity, entity_observation.get("to_state")): + if condition.state(self.hass, entity, entity_observation.to_state): return True except ConditionError: return None + return None @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" - attr_observations_list = [ - obs.copy() for obs in self.current_observations.values() if obs is not None - ] - - for item in attr_observations_list: - item.pop("value_template", None) return { - ATTR_OBSERVATIONS: attr_observations_list, - ATTR_OCCURRED_OBSERVATION_ENTITIES: list( - { - obs.get("entity_id") - for obs in self.current_observations.values() - if obs is not None - and obs.get("entity_id") is not None - and obs.get("observation") is not None - } - ), ATTR_PROBABILITY: round(self.probability, 2), ATTR_PROBABILITY_THRESHOLD: self._probability_threshold, + # An entity can be in more than one observation so set then list to deduplicate + ATTR_OCCURRED_OBSERVATION_ENTITIES: list( + { + observation.entity_id + for observation in self.current_observations.values() + if observation is not None + and observation.entity_id is not None + and observation.observed is not None + } + ), + ATTR_OBSERVATIONS: [ + observation.to_dict() + for observation in self.current_observations.values() + if observation is not None + ], } async def async_update(self) -> None: diff --git a/homeassistant/components/bayesian/const.py b/homeassistant/components/bayesian/const.py new file mode 100644 index 00000000000..5d3f978cedc --- /dev/null +++ b/homeassistant/components/bayesian/const.py @@ -0,0 +1,17 @@ +"""Consts for using in modules.""" + +ATTR_OBSERVATIONS = "observations" +ATTR_OCCURRED_OBSERVATION_ENTITIES = "occurred_observation_entities" +ATTR_PROBABILITY = "probability" +ATTR_PROBABILITY_THRESHOLD = "probability_threshold" + +CONF_OBSERVATIONS = "observations" +CONF_PRIOR = "prior" +CONF_TEMPLATE = "template" +CONF_PROBABILITY_THRESHOLD = "probability_threshold" +CONF_P_GIVEN_F = "prob_given_false" +CONF_P_GIVEN_T = "prob_given_true" +CONF_TO_STATE = "to_state" + +DEFAULT_NAME = "Bayesian Binary Sensor" +DEFAULT_PROBABILITY_THRESHOLD = 0.5 diff --git a/homeassistant/components/bayesian/helpers.py b/homeassistant/components/bayesian/helpers.py new file mode 100644 index 00000000000..22c5d518b46 --- /dev/null +++ b/homeassistant/components/bayesian/helpers.py @@ -0,0 +1,69 @@ +"""Helpers to deal with bayesian observations.""" +from __future__ import annotations + +from dataclasses import dataclass, field +import uuid + +from homeassistant.const import ( + CONF_ABOVE, + CONF_BELOW, + CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_VALUE_TEMPLATE, +) +from homeassistant.helpers.template import Template + +from .const import CONF_P_GIVEN_F, CONF_P_GIVEN_T, CONF_TO_STATE + + +@dataclass +class Observation: + """Representation of a sensor or template observation.""" + + entity_id: str | None + platform: str + prob_given_true: float + prob_given_false: float + to_state: str | None + above: float | None + below: float | None + value_template: Template | None + observed: bool | None = None + id: str = field(default_factory=lambda: str(uuid.uuid4())) + + def to_dict(self) -> dict[str, str | float | bool | None]: + """Represent Class as a Dict for easier serialization.""" + + # Needed because dataclasses asdict() can't serialize Templates and ignores Properties. + dic = { + CONF_PLATFORM: self.platform, + CONF_ENTITY_ID: self.entity_id, + CONF_VALUE_TEMPLATE: self.template, + CONF_TO_STATE: self.to_state, + CONF_ABOVE: self.above, + CONF_BELOW: self.below, + CONF_P_GIVEN_T: self.prob_given_true, + CONF_P_GIVEN_F: self.prob_given_false, + "observed": self.observed, + } + + for key, value in dic.copy().items(): + if value is None: + del dic[key] + + return dic + + def is_mirror(self, other: Observation) -> bool: + """Dectects whether given observation is a mirror of this one.""" + return ( + self.platform == other.platform + and round(self.prob_given_true + other.prob_given_true, 1) == 1 + and round(self.prob_given_false + other.prob_given_false, 1) == 1 + ) + + @property + def template(self) -> str | None: + """Not all observations have templates and we want to get template strings.""" + if self.value_template is not None: + return self.value_template.template + return None diff --git a/homeassistant/components/bayesian/repairs.py b/homeassistant/components/bayesian/repairs.py index a1d4f142527..2b04a6a6605 100644 --- a/homeassistant/components/bayesian/repairs.py +++ b/homeassistant/components/bayesian/repairs.py @@ -11,20 +11,7 @@ def raise_mirrored_entries(hass: HomeAssistant, observations, text: str = "") -> """If there are mirrored entries, the user is probably using a workaround for a patched bug.""" if len(observations) != 2: return - true_sums_1: bool = ( - round( - observations[0]["prob_given_true"] + observations[1]["prob_given_true"], 1 - ) - == 1.0 - ) - false_sums_1: bool = ( - round( - observations[0]["prob_given_false"] + observations[1]["prob_given_false"], 1 - ) - == 1.0 - ) - same_states: bool = observations[0]["platform"] == observations[1]["platform"] - if true_sums_1 & false_sums_1 & same_states: + if observations[0].is_mirror(observations[1]): issue_registry.async_create_issue( hass, DOMAIN, From abc80d8245e8905535bb2321c043f305f76e8534 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Oct 2022 11:45:40 -0400 Subject: [PATCH 161/985] Add a couple more brands (#79600) --- homeassistant/brands/inovelli.json | 5 +++++ homeassistant/brands/jasco.json | 5 +++++ homeassistant/brands/u_tec.json | 5 +++++ homeassistant/brands/zooz.json | 5 +++++ homeassistant/generated/integrations.json | 25 +++++++++++++++++++++++ 5 files changed, 45 insertions(+) create mode 100644 homeassistant/brands/inovelli.json create mode 100644 homeassistant/brands/jasco.json create mode 100644 homeassistant/brands/u_tec.json create mode 100644 homeassistant/brands/zooz.json diff --git a/homeassistant/brands/inovelli.json b/homeassistant/brands/inovelli.json new file mode 100644 index 00000000000..3667a6519c6 --- /dev/null +++ b/homeassistant/brands/inovelli.json @@ -0,0 +1,5 @@ +{ + "domain": "inovelli", + "name": "Inovelli", + "iot_standards": ["zigbee", "zwave"] +} diff --git a/homeassistant/brands/jasco.json b/homeassistant/brands/jasco.json new file mode 100644 index 00000000000..e293b81f994 --- /dev/null +++ b/homeassistant/brands/jasco.json @@ -0,0 +1,5 @@ +{ + "domain": "jasco", + "name": "Jasco", + "iot_standards": ["zwave"] +} diff --git a/homeassistant/brands/u_tec.json b/homeassistant/brands/u_tec.json new file mode 100644 index 00000000000..2ce4be9a7d9 --- /dev/null +++ b/homeassistant/brands/u_tec.json @@ -0,0 +1,5 @@ +{ + "domain": "u_tec", + "name": "U-tec", + "iot_standards": ["zwave"] +} diff --git a/homeassistant/brands/zooz.json b/homeassistant/brands/zooz.json new file mode 100644 index 00000000000..f3032e58653 --- /dev/null +++ b/homeassistant/brands/zooz.json @@ -0,0 +1,5 @@ +{ + "domain": "zooz", + "name": "Zooz", + "iot_standards": ["zwave"] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index b9989233b30..540d5c043c5 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1945,6 +1945,13 @@ "iot_class": "local_push", "name": "INKBIRD" }, + "inovelli": { + "name": "Inovelli", + "iot_standards": [ + "zigbee", + "zwave" + ] + }, "insteon": { "config_flow": true, "iot_class": "local_push", @@ -2019,6 +2026,12 @@ "iot_class": "local_polling", "name": "iZone" }, + "jasco": { + "name": "Jasco", + "iot_standards": [ + "zwave" + ] + }, "jellyfin": { "config_flow": true, "iot_class": "local_polling", @@ -4560,6 +4573,12 @@ "iot_class": "cloud_push", "name": "Twitter" }, + "u_tec": { + "name": "U-tec", + "iot_standards": [ + "zwave" + ] + }, "ubiquiti": { "name": "Ubiquiti", "integrations": { @@ -5058,6 +5077,12 @@ "iot_class": "local_polling", "name": "ZoneMinder" }, + "zooz": { + "name": "Zooz", + "iot_standards": [ + "zwave" + ] + }, "zwave_js": { "config_flow": true, "iot_class": "local_push", From 9c97ebbcfe97599bea244df51bf52c73f5ff970f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 4 Oct 2022 17:51:12 +0200 Subject: [PATCH 162/985] Update frontend to 20221004.0 (#79602) --- 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 62bed3777a8..61fc1629793 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==20221003.0"], + "requirements": ["home-assistant-frontend==20221004.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e3dd8b504a5..099207c3a7a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.23.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20221003.0 +home-assistant-frontend==20221004.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 08537c10e56..dcad93ace25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,7 +868,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221003.0 +home-assistant-frontend==20221004.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 271a40151fa..ff5585753e8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,7 +648,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221003.0 +home-assistant-frontend==20221004.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 051374d73e0f8ad8d7c70bf96bbb86451c30858f Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Tue, 4 Oct 2022 14:43:57 -0400 Subject: [PATCH 163/985] Handle state is None in InfluxDB (#79609) --- homeassistant/components/influxdb/__init__.py | 2 +- tests/components/influxdb/test_init.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index 72871c75fc4..4fd6eb58fdd 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -218,7 +218,7 @@ def _generate_event_to_json(conf: dict) -> Callable[[Event], dict[str, Any] | No state: State | None = event.data.get(EVENT_NEW_STATE) if ( state is None - or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE) + or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE, None) or not entity_filter(state.entity_id) ): return None diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index 27b9ac82ade..78648852803 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -557,7 +557,7 @@ async def test_event_listener_states( """Test the event listener against ignored states.""" handler_method = await _setup(hass, mock_client, config_ext, get_write_api) - for state_state in (1, "unknown", "", "unavailable"): + for state_state in (1, "unknown", "", "unavailable", None): state = MagicMock( state=state_state, domain="fake", From 89c4bf6536322cb1ce1daec530351bab945fd73f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Oct 2022 08:55:28 -1000 Subject: [PATCH 164/985] Bump dbus-fast to 1.24.0 (#79608) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 3b6f5977157..f81e1324da4 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.23.0" + "dbus-fast==1.24.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 099207c3a7a..afda4684b34 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.23.0 +dbus-fast==1.24.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index dcad93ace25..eab1726fb46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -543,7 +543,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.23.0 +dbus-fast==1.24.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff5585753e8..2bbdc6eb53f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.23.0 +dbus-fast==1.24.0 # homeassistant.components.debugpy debugpy==1.6.3 From 8faecae34d9b45914eb7413383a8134c73234461 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 4 Oct 2022 22:29:07 +0300 Subject: [PATCH 165/985] Shelly - move coordinators to coordinator.py (#79616) --- .coveragerc | 1 + homeassistant/components/shelly/__init__.py | 537 +----------------- .../components/shelly/coordinator.py | 533 +++++++++++++++++ 3 files changed, 543 insertions(+), 528 deletions(-) create mode 100644 homeassistant/components/shelly/coordinator.py diff --git a/.coveragerc b/.coveragerc index 298bc9020ef..79ecbb3ece5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1106,6 +1106,7 @@ omit = homeassistant/components/shelly/__init__.py homeassistant/components/shelly/binary_sensor.py homeassistant/components/shelly/climate.py + homeassistant/components/shelly/coordinator.py homeassistant/components/shelly/entity.py homeassistant/components/shelly/light.py homeassistant/components/shelly/number.py diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index ba03cf40f4f..e46d5a81c0e 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -2,8 +2,6 @@ from __future__ import annotations import asyncio -from collections.abc import Coroutine -from datetime import timedelta from http import HTTPStatus from typing import Any, Final, cast @@ -16,29 +14,15 @@ import async_timeout import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_DEVICE_ID, - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, - Platform, -) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client, device_registry, update_coordinator +from homeassistant.helpers import aiohttp_client, device_registry import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.typing import ConfigType from .const import ( AIOSHELLY_DEVICE_TIMEOUT_SEC, - ATTR_BETA, - ATTR_CHANNEL, - ATTR_CLICK_TYPE, - ATTR_DEVICE, - ATTR_GENERATION, - BATTERY_DEVICES_WITH_PERMANENT_CONNECTION, BLOCK, CONF_COAP_PORT, CONF_SLEEP_PERIOD, @@ -46,32 +30,18 @@ from .const import ( DEFAULT_COAP_PORT, DEVICE, DOMAIN, - DUAL_MODE_LIGHT_MODELS, - ENTRY_RELOAD_COOLDOWN, - EVENT_SHELLY_CLICK, - INPUTS_EVENTS_DICT, LOGGER, - MODELS_SUPPORTING_LIGHT_EFFECTS, - POLLING_TIMEOUT_SEC, REST, - REST_SENSORS_UPDATE_INTERVAL, RPC, - RPC_INPUTS_EVENTS_TYPES, RPC_POLL, - RPC_RECONNECT_INTERVAL, - RPC_SENSORS_POLLING_INTERVAL, - SHBTN_MODELS, - SLEEP_PERIOD_MULTIPLIER, - UPDATE_PERIOD_MULTIPLIER, ) -from .utils import ( - device_update_info, - get_block_device_name, - get_block_device_sleep_period, - get_coap_context, - get_device_entry_gen, - get_rpc_device_name, +from .coordinator import ( + BlockDeviceWrapper, + RpcDeviceWrapper, + RpcPollingWrapper, + ShellyDeviceRestWrapper, ) +from .utils import get_block_device_sleep_period, get_coap_context, get_device_entry_gen BLOCK_PLATFORMS: Final = [ Platform.BINARY_SENSOR, @@ -281,286 +251,6 @@ async def async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool return True -class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): - """Wrapper for a Shelly block based device with Home Assistant specific functions.""" - - def __init__( - self, hass: HomeAssistant, entry: ConfigEntry, device: BlockDevice - ) -> None: - """Initialize the Shelly device wrapper.""" - self.device_id: str | None = None - - if sleep_period := entry.data[CONF_SLEEP_PERIOD]: - update_interval = SLEEP_PERIOD_MULTIPLIER * sleep_period - else: - update_interval = ( - UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] - ) - - device_name = ( - get_block_device_name(device) if device.initialized else entry.title - ) - super().__init__( - hass, - LOGGER, - name=device_name, - update_interval=timedelta(seconds=update_interval), - ) - self.hass = hass - self.entry = entry - self.device = device - - self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer( - hass, - LOGGER, - cooldown=ENTRY_RELOAD_COOLDOWN, - immediate=False, - function=self._async_reload_entry, - ) - entry.async_on_unload(self._debounced_reload.async_cancel) - self._last_cfg_changed: int | None = None - self._last_mode: str | None = None - self._last_effect: int | None = None - - entry.async_on_unload( - self.async_add_listener(self._async_device_updates_handler) - ) - self._last_input_events_count: dict = {} - - entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) - ) - - async def _async_reload_entry(self) -> None: - """Reload entry.""" - LOGGER.debug("Reloading entry %s", self.name) - await self.hass.config_entries.async_reload(self.entry.entry_id) - - @callback - def _async_device_updates_handler(self) -> None: - """Handle device updates.""" - if not self.device.initialized: - return - - assert self.device.blocks - - # For buttons which are battery powered - set initial value for last_event_count - if self.model in SHBTN_MODELS and self._last_input_events_count.get(1) is None: - for block in self.device.blocks: - if block.type != "device": - continue - - if len(block.wakeupEvent) == 1 and block.wakeupEvent[0] == "button": - self._last_input_events_count[1] = -1 - - break - - # Check for input events and config change - cfg_changed = 0 - for block in self.device.blocks: - if block.type == "device": - cfg_changed = block.cfgChanged - - # For dual mode bulbs ignore change if it is due to mode/effect change - if self.model in DUAL_MODE_LIGHT_MODELS: - if "mode" in block.sensor_ids: - if self._last_mode != block.mode: - self._last_cfg_changed = None - self._last_mode = block.mode - - if self.model in MODELS_SUPPORTING_LIGHT_EFFECTS: - if "effect" in block.sensor_ids: - if self._last_effect != block.effect: - self._last_cfg_changed = None - self._last_effect = block.effect - - if ( - "inputEvent" not in block.sensor_ids - or "inputEventCnt" not in block.sensor_ids - ): - continue - - channel = int(block.channel or 0) + 1 - event_type = block.inputEvent - last_event_count = self._last_input_events_count.get(channel) - self._last_input_events_count[channel] = block.inputEventCnt - - if ( - last_event_count is None - or last_event_count == block.inputEventCnt - or event_type == "" - ): - continue - - if event_type in INPUTS_EVENTS_DICT: - self.hass.bus.async_fire( - EVENT_SHELLY_CLICK, - { - ATTR_DEVICE_ID: self.device_id, - ATTR_DEVICE: self.device.settings["device"]["hostname"], - ATTR_CHANNEL: channel, - ATTR_CLICK_TYPE: INPUTS_EVENTS_DICT[event_type], - ATTR_GENERATION: 1, - }, - ) - else: - LOGGER.warning( - "Shelly input event %s for device %s is not supported, please open issue", - event_type, - self.name, - ) - - if self._last_cfg_changed is not None and cfg_changed > self._last_cfg_changed: - LOGGER.info( - "Config for %s changed, reloading entry in %s seconds", - self.name, - ENTRY_RELOAD_COOLDOWN, - ) - self.hass.async_create_task(self._debounced_reload.async_call()) - self._last_cfg_changed = cfg_changed - - async def _async_update_data(self) -> None: - """Fetch data.""" - if sleep_period := self.entry.data.get(CONF_SLEEP_PERIOD): - # Sleeping device, no point polling it, just mark it unavailable - raise update_coordinator.UpdateFailed( - f"Sleeping device did not update within {sleep_period} seconds interval" - ) - - LOGGER.debug("Polling Shelly Block Device - %s", self.name) - try: - async with async_timeout.timeout(POLLING_TIMEOUT_SEC): - await self.device.update() - device_update_info(self.hass, self.device, self.entry) - except OSError as err: - raise update_coordinator.UpdateFailed("Error fetching data") from err - - @property - def model(self) -> str: - """Model of the device.""" - return cast(str, self.entry.data["model"]) - - @property - def mac(self) -> str: - """Mac address of the device.""" - return cast(str, self.entry.unique_id) - - @property - def sw_version(self) -> str: - """Firmware version of the device.""" - return self.device.firmware_version if self.device.initialized else "" - - def async_setup(self) -> None: - """Set up the wrapper.""" - dev_reg = device_registry.async_get(self.hass) - entry = dev_reg.async_get_or_create( - config_entry_id=self.entry.entry_id, - name=self.name, - connections={(device_registry.CONNECTION_NETWORK_MAC, self.mac)}, - manufacturer="Shelly", - model=aioshelly.const.MODEL_NAMES.get(self.model, self.model), - sw_version=self.sw_version, - hw_version=f"gen{self.device.gen} ({self.model})", - configuration_url=f"http://{self.entry.data[CONF_HOST]}", - ) - self.device_id = entry.id - self.device.subscribe_updates(self.async_set_updated_data) - - async def async_trigger_ota_update(self, beta: bool = False) -> None: - """Trigger or schedule an ota update.""" - update_data = self.device.status["update"] - LOGGER.debug("OTA update service - update_data: %s", update_data) - - if not update_data["has_update"] and not beta: - LOGGER.warning("No OTA update available for device %s", self.name) - return - - if beta and not update_data.get("beta_version"): - LOGGER.warning( - "No OTA update on beta channel available for device %s", self.name - ) - return - - if update_data["status"] == "updating": - LOGGER.warning("OTA update already in progress for %s", self.name) - return - - new_version = update_data["new_version"] - if beta: - new_version = update_data["beta_version"] - LOGGER.info( - "Start OTA update of device %s from '%s' to '%s'", - self.name, - self.device.firmware_version, - new_version, - ) - try: - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - result = await self.device.trigger_ota_update(beta=beta) - except (asyncio.TimeoutError, OSError) as err: - LOGGER.exception("Error while perform ota update: %s", err) - LOGGER.debug("Result of OTA update call: %s", result) - - def shutdown(self) -> None: - """Shutdown the wrapper.""" - self.device.shutdown() - - @callback - def _handle_ha_stop(self, _event: Event) -> None: - """Handle Home Assistant stopping.""" - LOGGER.debug("Stopping BlockDeviceWrapper for %s", self.name) - self.shutdown() - - -class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): - """Rest Wrapper for a Shelly device with Home Assistant specific functions.""" - - def __init__( - self, hass: HomeAssistant, device: BlockDevice, entry: ConfigEntry - ) -> None: - """Initialize the Shelly device wrapper.""" - if ( - device.settings["device"]["type"] - in BATTERY_DEVICES_WITH_PERMANENT_CONNECTION - ): - update_interval = ( - SLEEP_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] - ) - else: - update_interval = REST_SENSORS_UPDATE_INTERVAL - - super().__init__( - hass, - LOGGER, - name=get_block_device_name(device), - update_interval=timedelta(seconds=update_interval), - ) - self.device = device - self.entry = entry - - async def _async_update_data(self) -> None: - """Fetch data.""" - try: - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - LOGGER.debug("REST update for %s", self.name) - await self.device.update_status() - - if self.device.status["uptime"] > 2 * REST_SENSORS_UPDATE_INTERVAL: - return - old_firmware = self.device.firmware_version - await self.device.update_shelly() - if self.device.firmware_version == old_firmware: - return - device_update_info(self.hass, self.device, self.entry) - except OSError as err: - raise update_coordinator.UpdateFailed("Error fetching data") from err - - @property - def mac(self) -> str: - """Mac address of the device.""" - return cast(str, self.device.settings["device"]["mac"]) - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" if get_device_entry_gen(entry) == 2: @@ -629,212 +319,3 @@ def get_rpc_device_wrapper( return cast(RpcDeviceWrapper, wrapper) return None - - -class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): - """Wrapper for a Shelly RPC based device with Home Assistant specific functions.""" - - def __init__( - self, hass: HomeAssistant, entry: ConfigEntry, device: RpcDevice - ) -> None: - """Initialize the Shelly device wrapper.""" - self.device_id: str | None = None - - device_name = get_rpc_device_name(device) if device.initialized else entry.title - super().__init__( - hass, - LOGGER, - name=device_name, - update_interval=timedelta(seconds=RPC_RECONNECT_INTERVAL), - ) - self.entry = entry - self.device = device - - self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer( - hass, - LOGGER, - cooldown=ENTRY_RELOAD_COOLDOWN, - immediate=False, - function=self._async_reload_entry, - ) - entry.async_on_unload(self._debounced_reload.async_cancel) - - entry.async_on_unload( - self.async_add_listener(self._async_device_updates_handler) - ) - self._last_event: dict[str, Any] | None = None - - entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) - ) - - async def _async_reload_entry(self) -> None: - """Reload entry.""" - LOGGER.debug("Reloading entry %s", self.name) - await self.hass.config_entries.async_reload(self.entry.entry_id) - - @callback - def _async_device_updates_handler(self) -> None: - """Handle device updates.""" - if ( - not self.device.initialized - or not self.device.event - or self.device.event == self._last_event - ): - return - - self._last_event = self.device.event - - for event in self.device.event["events"]: - event_type = event.get("event") - if event_type is None: - continue - - if event_type == "config_changed": - LOGGER.info( - "Config for %s changed, reloading entry in %s seconds", - self.name, - ENTRY_RELOAD_COOLDOWN, - ) - self.hass.async_create_task(self._debounced_reload.async_call()) - elif event_type in RPC_INPUTS_EVENTS_TYPES: - self.hass.bus.async_fire( - EVENT_SHELLY_CLICK, - { - ATTR_DEVICE_ID: self.device_id, - ATTR_DEVICE: self.device.hostname, - ATTR_CHANNEL: event["id"] + 1, - ATTR_CLICK_TYPE: event["event"], - ATTR_GENERATION: 2, - }, - ) - - async def _async_update_data(self) -> None: - """Fetch data.""" - if self.device.connected: - return - - try: - LOGGER.debug("Reconnecting to Shelly RPC Device - %s", self.name) - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - await self.device.initialize() - device_update_info(self.hass, self.device, self.entry) - except OSError as err: - raise update_coordinator.UpdateFailed("Device disconnected") from err - - @property - def model(self) -> str: - """Model of the device.""" - return cast(str, self.entry.data["model"]) - - @property - def mac(self) -> str: - """Mac address of the device.""" - return cast(str, self.entry.unique_id) - - @property - def sw_version(self) -> str: - """Firmware version of the device.""" - return self.device.firmware_version if self.device.initialized else "" - - def async_setup(self) -> None: - """Set up the wrapper.""" - dev_reg = device_registry.async_get(self.hass) - entry = dev_reg.async_get_or_create( - config_entry_id=self.entry.entry_id, - name=self.name, - connections={(device_registry.CONNECTION_NETWORK_MAC, self.mac)}, - manufacturer="Shelly", - model=aioshelly.const.MODEL_NAMES.get(self.model, self.model), - sw_version=self.sw_version, - hw_version=f"gen{self.device.gen} ({self.model})", - configuration_url=f"http://{self.entry.data[CONF_HOST]}", - ) - self.device_id = entry.id - self.device.subscribe_updates(self.async_set_updated_data) - - async def async_trigger_ota_update(self, beta: bool = False) -> None: - """Trigger an ota update.""" - - update_data = self.device.status["sys"]["available_updates"] - LOGGER.debug("OTA update service - update_data: %s", update_data) - - if not bool(update_data) or (not update_data.get("stable") and not beta): - LOGGER.warning("No OTA update available for device %s", self.name) - return - - if beta and not update_data.get(ATTR_BETA): - LOGGER.warning( - "No OTA update on beta channel available for device %s", self.name - ) - return - - new_version = update_data.get("stable", {"version": ""})["version"] - if beta: - new_version = update_data.get(ATTR_BETA, {"version": ""})["version"] - - assert self.device.shelly - LOGGER.info( - "Start OTA update of device %s from '%s' to '%s'", - self.name, - self.device.firmware_version, - new_version, - ) - try: - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - await self.device.trigger_ota_update(beta=beta) - except (asyncio.TimeoutError, OSError) as err: - LOGGER.exception("Error while perform ota update: %s", err) - - LOGGER.debug("OTA update call successful") - - async def shutdown(self) -> None: - """Shutdown the wrapper.""" - await self.device.shutdown() - - async def _handle_ha_stop(self, _event: Event) -> None: - """Handle Home Assistant stopping.""" - LOGGER.debug("Stopping RpcDeviceWrapper for %s", self.name) - await self.shutdown() - - -class RpcPollingWrapper(update_coordinator.DataUpdateCoordinator): - """Polling Wrapper for a Shelly RPC based device.""" - - def __init__( - self, hass: HomeAssistant, entry: ConfigEntry, device: RpcDevice - ) -> None: - """Initialize the RPC polling coordinator.""" - self.device_id: str | None = None - - device_name = get_rpc_device_name(device) if device.initialized else entry.title - super().__init__( - hass, - LOGGER, - name=device_name, - update_interval=timedelta(seconds=RPC_SENSORS_POLLING_INTERVAL), - ) - self.entry = entry - self.device = device - - async def _async_update_data(self) -> None: - """Fetch data.""" - if not self.device.connected: - raise update_coordinator.UpdateFailed("Device disconnected") - - try: - LOGGER.debug("Polling Shelly RPC Device - %s", self.name) - async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - await self.device.update_status() - except (OSError, aioshelly.exceptions.RPCTimeout) as err: - raise update_coordinator.UpdateFailed("Device disconnected") from err - - @property - def model(self) -> str: - """Model of the device.""" - return cast(str, self.entry.data["model"]) - - @property - def mac(self) -> str: - """Mac address of the device.""" - return cast(str, self.entry.unique_id) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py new file mode 100644 index 00000000000..02a4e6ffba1 --- /dev/null +++ b/homeassistant/components/shelly/coordinator.py @@ -0,0 +1,533 @@ +"""Coordinators for the Shelly integration.""" +from __future__ import annotations + +import asyncio +from collections.abc import Coroutine +from datetime import timedelta +from typing import Any, cast + +import aioshelly +from aioshelly.block_device import BlockDevice +from aioshelly.rpc_device import RpcDevice +import async_timeout + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import device_registry, update_coordinator +from homeassistant.helpers.debounce import Debouncer + +from .const import ( + AIOSHELLY_DEVICE_TIMEOUT_SEC, + ATTR_BETA, + ATTR_CHANNEL, + ATTR_CLICK_TYPE, + ATTR_DEVICE, + ATTR_GENERATION, + BATTERY_DEVICES_WITH_PERMANENT_CONNECTION, + CONF_SLEEP_PERIOD, + DUAL_MODE_LIGHT_MODELS, + ENTRY_RELOAD_COOLDOWN, + EVENT_SHELLY_CLICK, + INPUTS_EVENTS_DICT, + LOGGER, + MODELS_SUPPORTING_LIGHT_EFFECTS, + POLLING_TIMEOUT_SEC, + REST_SENSORS_UPDATE_INTERVAL, + RPC_INPUTS_EVENTS_TYPES, + RPC_RECONNECT_INTERVAL, + RPC_SENSORS_POLLING_INTERVAL, + SHBTN_MODELS, + SLEEP_PERIOD_MULTIPLIER, + UPDATE_PERIOD_MULTIPLIER, +) +from .utils import device_update_info, get_block_device_name, get_rpc_device_name + + +class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): + """Wrapper for a Shelly block based device with Home Assistant specific functions.""" + + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, device: BlockDevice + ) -> None: + """Initialize the Shelly device wrapper.""" + self.device_id: str | None = None + + if sleep_period := entry.data[CONF_SLEEP_PERIOD]: + update_interval = SLEEP_PERIOD_MULTIPLIER * sleep_period + else: + update_interval = ( + UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] + ) + + device_name = ( + get_block_device_name(device) if device.initialized else entry.title + ) + super().__init__( + hass, + LOGGER, + name=device_name, + update_interval=timedelta(seconds=update_interval), + ) + self.hass = hass + self.entry = entry + self.device = device + + self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer( + hass, + LOGGER, + cooldown=ENTRY_RELOAD_COOLDOWN, + immediate=False, + function=self._async_reload_entry, + ) + entry.async_on_unload(self._debounced_reload.async_cancel) + self._last_cfg_changed: int | None = None + self._last_mode: str | None = None + self._last_effect: int | None = None + + entry.async_on_unload( + self.async_add_listener(self._async_device_updates_handler) + ) + self._last_input_events_count: dict = {} + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) + ) + + async def _async_reload_entry(self) -> None: + """Reload entry.""" + LOGGER.debug("Reloading entry %s", self.name) + await self.hass.config_entries.async_reload(self.entry.entry_id) + + @callback + def _async_device_updates_handler(self) -> None: + """Handle device updates.""" + if not self.device.initialized: + return + + assert self.device.blocks + + # For buttons which are battery powered - set initial value for last_event_count + if self.model in SHBTN_MODELS and self._last_input_events_count.get(1) is None: + for block in self.device.blocks: + if block.type != "device": + continue + + if len(block.wakeupEvent) == 1 and block.wakeupEvent[0] == "button": + self._last_input_events_count[1] = -1 + + break + + # Check for input events and config change + cfg_changed = 0 + for block in self.device.blocks: + if block.type == "device": + cfg_changed = block.cfgChanged + + # For dual mode bulbs ignore change if it is due to mode/effect change + if self.model in DUAL_MODE_LIGHT_MODELS: + if "mode" in block.sensor_ids: + if self._last_mode != block.mode: + self._last_cfg_changed = None + self._last_mode = block.mode + + if self.model in MODELS_SUPPORTING_LIGHT_EFFECTS: + if "effect" in block.sensor_ids: + if self._last_effect != block.effect: + self._last_cfg_changed = None + self._last_effect = block.effect + + if ( + "inputEvent" not in block.sensor_ids + or "inputEventCnt" not in block.sensor_ids + ): + continue + + channel = int(block.channel or 0) + 1 + event_type = block.inputEvent + last_event_count = self._last_input_events_count.get(channel) + self._last_input_events_count[channel] = block.inputEventCnt + + if ( + last_event_count is None + or last_event_count == block.inputEventCnt + or event_type == "" + ): + continue + + if event_type in INPUTS_EVENTS_DICT: + self.hass.bus.async_fire( + EVENT_SHELLY_CLICK, + { + ATTR_DEVICE_ID: self.device_id, + ATTR_DEVICE: self.device.settings["device"]["hostname"], + ATTR_CHANNEL: channel, + ATTR_CLICK_TYPE: INPUTS_EVENTS_DICT[event_type], + ATTR_GENERATION: 1, + }, + ) + else: + LOGGER.warning( + "Shelly input event %s for device %s is not supported, please open issue", + event_type, + self.name, + ) + + if self._last_cfg_changed is not None and cfg_changed > self._last_cfg_changed: + LOGGER.info( + "Config for %s changed, reloading entry in %s seconds", + self.name, + ENTRY_RELOAD_COOLDOWN, + ) + self.hass.async_create_task(self._debounced_reload.async_call()) + self._last_cfg_changed = cfg_changed + + async def _async_update_data(self) -> None: + """Fetch data.""" + if sleep_period := self.entry.data.get(CONF_SLEEP_PERIOD): + # Sleeping device, no point polling it, just mark it unavailable + raise update_coordinator.UpdateFailed( + f"Sleeping device did not update within {sleep_period} seconds interval" + ) + + LOGGER.debug("Polling Shelly Block Device - %s", self.name) + try: + async with async_timeout.timeout(POLLING_TIMEOUT_SEC): + await self.device.update() + device_update_info(self.hass, self.device, self.entry) + except OSError as err: + raise update_coordinator.UpdateFailed("Error fetching data") from err + + @property + def model(self) -> str: + """Model of the device.""" + return cast(str, self.entry.data["model"]) + + @property + def mac(self) -> str: + """Mac address of the device.""" + return cast(str, self.entry.unique_id) + + @property + def sw_version(self) -> str: + """Firmware version of the device.""" + return self.device.firmware_version if self.device.initialized else "" + + def async_setup(self) -> None: + """Set up the wrapper.""" + dev_reg = device_registry.async_get(self.hass) + entry = dev_reg.async_get_or_create( + config_entry_id=self.entry.entry_id, + name=self.name, + connections={(device_registry.CONNECTION_NETWORK_MAC, self.mac)}, + manufacturer="Shelly", + model=aioshelly.const.MODEL_NAMES.get(self.model, self.model), + sw_version=self.sw_version, + hw_version=f"gen{self.device.gen} ({self.model})", + configuration_url=f"http://{self.entry.data[CONF_HOST]}", + ) + self.device_id = entry.id + self.device.subscribe_updates(self.async_set_updated_data) + + async def async_trigger_ota_update(self, beta: bool = False) -> None: + """Trigger or schedule an ota update.""" + update_data = self.device.status["update"] + LOGGER.debug("OTA update service - update_data: %s", update_data) + + if not update_data["has_update"] and not beta: + LOGGER.warning("No OTA update available for device %s", self.name) + return + + if beta and not update_data.get("beta_version"): + LOGGER.warning( + "No OTA update on beta channel available for device %s", self.name + ) + return + + if update_data["status"] == "updating": + LOGGER.warning("OTA update already in progress for %s", self.name) + return + + new_version = update_data["new_version"] + if beta: + new_version = update_data["beta_version"] + LOGGER.info( + "Start OTA update of device %s from '%s' to '%s'", + self.name, + self.device.firmware_version, + new_version, + ) + try: + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): + result = await self.device.trigger_ota_update(beta=beta) + except (asyncio.TimeoutError, OSError) as err: + LOGGER.exception("Error while perform ota update: %s", err) + LOGGER.debug("Result of OTA update call: %s", result) + + def shutdown(self) -> None: + """Shutdown the wrapper.""" + self.device.shutdown() + + @callback + def _handle_ha_stop(self, _event: Event) -> None: + """Handle Home Assistant stopping.""" + LOGGER.debug("Stopping BlockDeviceWrapper for %s", self.name) + self.shutdown() + + +class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): + """Rest Wrapper for a Shelly device with Home Assistant specific functions.""" + + def __init__( + self, hass: HomeAssistant, device: BlockDevice, entry: ConfigEntry + ) -> None: + """Initialize the Shelly device wrapper.""" + if ( + device.settings["device"]["type"] + in BATTERY_DEVICES_WITH_PERMANENT_CONNECTION + ): + update_interval = ( + SLEEP_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] + ) + else: + update_interval = REST_SENSORS_UPDATE_INTERVAL + + super().__init__( + hass, + LOGGER, + name=get_block_device_name(device), + update_interval=timedelta(seconds=update_interval), + ) + self.device = device + self.entry = entry + + async def _async_update_data(self) -> None: + """Fetch data.""" + try: + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): + LOGGER.debug("REST update for %s", self.name) + await self.device.update_status() + + if self.device.status["uptime"] > 2 * REST_SENSORS_UPDATE_INTERVAL: + return + old_firmware = self.device.firmware_version + await self.device.update_shelly() + if self.device.firmware_version == old_firmware: + return + device_update_info(self.hass, self.device, self.entry) + except OSError as err: + raise update_coordinator.UpdateFailed("Error fetching data") from err + + @property + def mac(self) -> str: + """Mac address of the device.""" + return cast(str, self.device.settings["device"]["mac"]) + + +class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): + """Wrapper for a Shelly RPC based device with Home Assistant specific functions.""" + + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, device: RpcDevice + ) -> None: + """Initialize the Shelly device wrapper.""" + self.device_id: str | None = None + + device_name = get_rpc_device_name(device) if device.initialized else entry.title + super().__init__( + hass, + LOGGER, + name=device_name, + update_interval=timedelta(seconds=RPC_RECONNECT_INTERVAL), + ) + self.entry = entry + self.device = device + + self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer( + hass, + LOGGER, + cooldown=ENTRY_RELOAD_COOLDOWN, + immediate=False, + function=self._async_reload_entry, + ) + entry.async_on_unload(self._debounced_reload.async_cancel) + + entry.async_on_unload( + self.async_add_listener(self._async_device_updates_handler) + ) + self._last_event: dict[str, Any] | None = None + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) + ) + + async def _async_reload_entry(self) -> None: + """Reload entry.""" + LOGGER.debug("Reloading entry %s", self.name) + await self.hass.config_entries.async_reload(self.entry.entry_id) + + @callback + def _async_device_updates_handler(self) -> None: + """Handle device updates.""" + if ( + not self.device.initialized + or not self.device.event + or self.device.event == self._last_event + ): + return + + self._last_event = self.device.event + + for event in self.device.event["events"]: + event_type = event.get("event") + if event_type is None: + continue + + if event_type == "config_changed": + LOGGER.info( + "Config for %s changed, reloading entry in %s seconds", + self.name, + ENTRY_RELOAD_COOLDOWN, + ) + self.hass.async_create_task(self._debounced_reload.async_call()) + elif event_type in RPC_INPUTS_EVENTS_TYPES: + self.hass.bus.async_fire( + EVENT_SHELLY_CLICK, + { + ATTR_DEVICE_ID: self.device_id, + ATTR_DEVICE: self.device.hostname, + ATTR_CHANNEL: event["id"] + 1, + ATTR_CLICK_TYPE: event["event"], + ATTR_GENERATION: 2, + }, + ) + + async def _async_update_data(self) -> None: + """Fetch data.""" + if self.device.connected: + return + + try: + LOGGER.debug("Reconnecting to Shelly RPC Device - %s", self.name) + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): + await self.device.initialize() + device_update_info(self.hass, self.device, self.entry) + except OSError as err: + raise update_coordinator.UpdateFailed("Device disconnected") from err + + @property + def model(self) -> str: + """Model of the device.""" + return cast(str, self.entry.data["model"]) + + @property + def mac(self) -> str: + """Mac address of the device.""" + return cast(str, self.entry.unique_id) + + @property + def sw_version(self) -> str: + """Firmware version of the device.""" + return self.device.firmware_version if self.device.initialized else "" + + def async_setup(self) -> None: + """Set up the wrapper.""" + dev_reg = device_registry.async_get(self.hass) + entry = dev_reg.async_get_or_create( + config_entry_id=self.entry.entry_id, + name=self.name, + connections={(device_registry.CONNECTION_NETWORK_MAC, self.mac)}, + manufacturer="Shelly", + model=aioshelly.const.MODEL_NAMES.get(self.model, self.model), + sw_version=self.sw_version, + hw_version=f"gen{self.device.gen} ({self.model})", + configuration_url=f"http://{self.entry.data[CONF_HOST]}", + ) + self.device_id = entry.id + self.device.subscribe_updates(self.async_set_updated_data) + + async def async_trigger_ota_update(self, beta: bool = False) -> None: + """Trigger an ota update.""" + + update_data = self.device.status["sys"]["available_updates"] + LOGGER.debug("OTA update service - update_data: %s", update_data) + + if not bool(update_data) or (not update_data.get("stable") and not beta): + LOGGER.warning("No OTA update available for device %s", self.name) + return + + if beta and not update_data.get(ATTR_BETA): + LOGGER.warning( + "No OTA update on beta channel available for device %s", self.name + ) + return + + new_version = update_data.get("stable", {"version": ""})["version"] + if beta: + new_version = update_data.get(ATTR_BETA, {"version": ""})["version"] + + assert self.device.shelly + LOGGER.info( + "Start OTA update of device %s from '%s' to '%s'", + self.name, + self.device.firmware_version, + new_version, + ) + try: + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): + await self.device.trigger_ota_update(beta=beta) + except (asyncio.TimeoutError, OSError) as err: + LOGGER.exception("Error while perform ota update: %s", err) + + LOGGER.debug("OTA update call successful") + + async def shutdown(self) -> None: + """Shutdown the wrapper.""" + await self.device.shutdown() + + async def _handle_ha_stop(self, _event: Event) -> None: + """Handle Home Assistant stopping.""" + LOGGER.debug("Stopping RpcDeviceWrapper for %s", self.name) + await self.shutdown() + + +class RpcPollingWrapper(update_coordinator.DataUpdateCoordinator): + """Polling Wrapper for a Shelly RPC based device.""" + + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, device: RpcDevice + ) -> None: + """Initialize the RPC polling coordinator.""" + self.device_id: str | None = None + + device_name = get_rpc_device_name(device) if device.initialized else entry.title + super().__init__( + hass, + LOGGER, + name=device_name, + update_interval=timedelta(seconds=RPC_SENSORS_POLLING_INTERVAL), + ) + self.entry = entry + self.device = device + + async def _async_update_data(self) -> None: + """Fetch data.""" + if not self.device.connected: + raise update_coordinator.UpdateFailed("Device disconnected") + + try: + LOGGER.debug("Polling Shelly RPC Device - %s", self.name) + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): + await self.device.update_status() + except (OSError, aioshelly.exceptions.RPCTimeout) as err: + raise update_coordinator.UpdateFailed("Device disconnected") from err + + @property + def model(self) -> str: + """Model of the device.""" + return cast(str, self.entry.data["model"]) + + @property + def mac(self) -> str: + """Mac address of the device.""" + return cast(str, self.entry.unique_id) From 253f6616cf9f4159e5d3110bb742c67c6e3f53c4 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 4 Oct 2022 17:17:48 -0400 Subject: [PATCH 166/985] Bump ZHA dependencies (#79623) --- homeassistant/components/zha/manifest.json | 6 +++--- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 322f93e8373..803a7daabbe 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,12 +4,12 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.34.0", + "bellows==0.34.1", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.81", + "zha-quirks==0.0.82", "zigpy-deconz==0.19.0", - "zigpy==0.51.1", + "zigpy==0.51.2", "zigpy-xbee==0.16.0", "zigpy-zigate==0.10.0", "zigpy-znp==0.9.0" diff --git a/requirements_all.txt b/requirements_all.txt index eab1726fb46..a71fceed31a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.34.0 +bellows==0.34.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.4 @@ -2595,7 +2595,7 @@ zengge==0.2 zeroconf==0.39.1 # homeassistant.components.zha -zha-quirks==0.0.81 +zha-quirks==0.0.82 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2616,7 +2616,7 @@ zigpy-zigate==0.10.0 zigpy-znp==0.9.0 # homeassistant.components.zha -zigpy==0.51.1 +zigpy==0.51.2 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2bbdc6eb53f..3c5394b3d80 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -331,7 +331,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.34.0 +bellows==0.34.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.4 @@ -1796,7 +1796,7 @@ youless-api==0.16 zeroconf==0.39.1 # homeassistant.components.zha -zha-quirks==0.0.81 +zha-quirks==0.0.82 # homeassistant.components.zha zigpy-deconz==0.19.0 @@ -1811,7 +1811,7 @@ zigpy-zigate==0.10.0 zigpy-znp==0.9.0 # homeassistant.components.zha -zigpy==0.51.1 +zigpy==0.51.2 # homeassistant.components.zwave_js zwave-js-server-python==0.43.0 From f3e05534eed2be0aa43ce766133adc9586ab1977 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 5 Oct 2022 00:49:54 +0200 Subject: [PATCH 167/985] Use VOLUME device_class in flume (#79585) --- homeassistant/components/flume/const.py | 38 ------------------ homeassistant/components/flume/sensor.py | 49 +++++++++++++++++++++++- 2 files changed, 47 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/flume/const.py b/homeassistant/components/flume/const.py index 5e095961ed8..656c2eb1018 100644 --- a/homeassistant/components/flume/const.py +++ b/homeassistant/components/flume/const.py @@ -4,7 +4,6 @@ from __future__ import annotations from datetime import timedelta import logging -from homeassistant.components.sensor import SensorEntityDescription from homeassistant.const import Platform DOMAIN = "flume" @@ -19,43 +18,6 @@ DEVICE_SCAN_INTERVAL = timedelta(minutes=1) _LOGGER = logging.getLogger(__package__) FLUME_TYPE_SENSOR = 2 -FLUME_QUERIES_SENSOR: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="current_interval", - name="Current", - native_unit_of_measurement="gal/m", - ), - SensorEntityDescription( - key="month_to_date", - name="Current Month", - native_unit_of_measurement="gal", - ), - SensorEntityDescription( - key="week_to_date", - name="Current Week", - native_unit_of_measurement="gal", - ), - SensorEntityDescription( - key="today", - name="Current Day", - native_unit_of_measurement="gal", - ), - SensorEntityDescription( - key="last_60_min", - name="60 Minutes", - native_unit_of_measurement="gal/h", - ), - SensorEntityDescription( - key="last_24_hrs", - name="24 Hours", - native_unit_of_measurement="gal/d", - ), - SensorEntityDescription( - key="last_30_days", - name="30 Days", - native_unit_of_measurement="gal/mo", - ), -) FLUME_AUTH = "flume_auth" FLUME_HTTP_SESSION = "http_session" diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index 6d68058732d..51d1b54bee8 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -3,8 +3,13 @@ from numbers import Number from pyflume import FlumeData -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import VOLUME_GALLONS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -14,7 +19,6 @@ from .const import ( FLUME_AUTH, FLUME_DEVICES, FLUME_HTTP_SESSION, - FLUME_QUERIES_SENSOR, FLUME_TYPE_SENSOR, KEY_DEVICE_ID, KEY_DEVICE_LOCATION, @@ -24,6 +28,47 @@ from .const import ( from .coordinator import FlumeDeviceDataUpdateCoordinator from .entity import FlumeEntity +FLUME_QUERIES_SENSOR: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="current_interval", + name="Current", + native_unit_of_measurement=f"{VOLUME_GALLONS}/m", + ), + SensorEntityDescription( + key="month_to_date", + name="Current Month", + native_unit_of_measurement=VOLUME_GALLONS, + device_class=SensorDeviceClass.VOLUME, + ), + SensorEntityDescription( + key="week_to_date", + name="Current Week", + native_unit_of_measurement=VOLUME_GALLONS, + device_class=SensorDeviceClass.VOLUME, + ), + SensorEntityDescription( + key="today", + name="Current Day", + native_unit_of_measurement=VOLUME_GALLONS, + device_class=SensorDeviceClass.VOLUME, + ), + SensorEntityDescription( + key="last_60_min", + name="60 Minutes", + native_unit_of_measurement=f"{VOLUME_GALLONS}/h", + ), + SensorEntityDescription( + key="last_24_hrs", + name="24 Hours", + native_unit_of_measurement=f"{VOLUME_GALLONS}/d", + ), + SensorEntityDescription( + key="last_30_days", + name="30 Days", + native_unit_of_measurement=f"{VOLUME_GALLONS}/mo", + ), +) + async def async_setup_entry( hass: HomeAssistant, From 8d28da83ca4ac3a83617e0759fc17a559c7f288c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 5 Oct 2022 00:36:50 +0000 Subject: [PATCH 168/985] [ci skip] Translation update --- .../components/abode/translations/bg.json | 4 ++-- .../airthings_ble/translations/no.json | 23 +++++++++++++++++++ .../components/apcupsd/translations/bg.json | 5 ++++ .../aseko_pool_live/translations/bg.json | 2 +- .../components/bayesian/translations/hu.json | 12 ++++++++++ .../components/bayesian/translations/no.json | 12 ++++++++++ .../components/braviatv/translations/bg.json | 9 +++++--- .../components/braviatv/translations/et.json | 2 +- .../components/braviatv/translations/he.json | 8 ++++++- .../components/braviatv/translations/nl.json | 8 ++++++- .../components/braviatv/translations/no.json | 11 ++++++++- .../crownstone/translations/bg.json | 2 +- .../components/demo/translations/bg.json | 3 +++ .../devolo_home_control/translations/bg.json | 3 ++- .../components/econet/translations/bg.json | 1 + .../components/flipr/translations/bg.json | 2 +- .../forked_daapd/translations/no.json | 14 +++++------ .../components/hangouts/translations/bg.json | 2 +- .../components/icloud/translations/bg.json | 2 +- .../intellifire/translations/bg.json | 2 +- .../components/lametric/translations/nl.json | 8 ++++++- .../components/mazda/translations/bg.json | 2 +- .../components/melcloud/translations/bg.json | 2 +- .../components/mikrotik/translations/he.json | 9 +++++++- .../components/mikrotik/translations/no.json | 10 +++++++- .../components/nest/translations/no.json | 2 +- .../components/nobo_hub/translations/nl.json | 6 +++++ .../components/octoprint/translations/he.json | 6 +++++ .../components/octoprint/translations/no.json | 6 +++++ .../plum_lightpad/translations/bg.json | 2 +- .../components/poolsense/translations/bg.json | 2 +- .../components/renault/translations/bg.json | 2 +- .../translations/bg.json | 1 + .../rtsp_to_webrtc/translations/bg.json | 9 ++++++++ .../rtsp_to_webrtc/translations/de.json | 9 ++++++++ .../rtsp_to_webrtc/translations/es.json | 9 ++++++++ .../rtsp_to_webrtc/translations/et.json | 9 ++++++++ .../rtsp_to_webrtc/translations/hu.json | 9 ++++++++ .../rtsp_to_webrtc/translations/no.json | 9 ++++++++ .../rtsp_to_webrtc/translations/pt-BR.json | 9 ++++++++ .../rtsp_to_webrtc/translations/ru.json | 9 ++++++++ .../rtsp_to_webrtc/translations/zh-Hant.json | 9 ++++++++ .../components/sense/translations/bg.json | 5 ++++ .../components/sensor/translations/nl.json | 14 +++++++++-- .../components/sensor/translations/no.json | 6 +++-- .../components/skybell/translations/bg.json | 2 +- .../components/smarttub/translations/bg.json | 1 + .../components/spotify/translations/bg.json | 1 + .../steam_online/translations/bg.json | 6 +++++ .../components/tasmota/translations/nl.json | 5 ++++ .../components/tile/translations/bg.json | 2 +- .../components/tractive/translations/bg.json | 2 +- .../components/verisure/translations/bg.json | 10 ++++++++ .../components/vesync/translations/bg.json | 2 +- .../components/vicare/translations/bg.json | 2 +- .../volvooncall/translations/bg.json | 1 + .../components/zha/translations/bg.json | 3 ++- .../components/zha/translations/he.json | 1 + .../components/zha/translations/no.json | 2 ++ .../components/zwave_js/translations/nl.json | 5 ++++ 60 files changed, 294 insertions(+), 42 deletions(-) create mode 100644 homeassistant/components/airthings_ble/translations/no.json create mode 100644 homeassistant/components/bayesian/translations/hu.json create mode 100644 homeassistant/components/bayesian/translations/no.json diff --git a/homeassistant/components/abode/translations/bg.json b/homeassistant/components/abode/translations/bg.json index 955ed18c82c..a451dd3516a 100644 --- a/homeassistant/components/abode/translations/bg.json +++ b/homeassistant/components/abode/translations/bg.json @@ -11,13 +11,13 @@ "reauth_confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "username": "Email" + "username": "\u0418\u043c\u0435\u0439\u043b" } }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "username": "E-mail \u0430\u0434\u0440\u0435\u0441" + "username": "\u0418\u043c\u0435\u0439\u043b" }, "title": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0412\u0430\u0448\u0430\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 \u0432\u0445\u043e\u0434 \u0432 Abode" } diff --git a/homeassistant/components/airthings_ble/translations/no.json b/homeassistant/components/airthings_ble/translations/no.json new file mode 100644 index 00000000000..d23d4703ac3 --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "cannot_connect": "Tilkobling mislyktes", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "unknown": "Uventet feil" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/bg.json b/homeassistant/components/apcupsd/translations/bg.json index cc5f200ef95..0160e0fee55 100644 --- a/homeassistant/components/apcupsd/translations/bg.json +++ b/homeassistant/components/apcupsd/translations/bg.json @@ -14,5 +14,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 APC UPS Daemon \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/bg.json b/homeassistant/components/aseko_pool_live/translations/bg.json index 982674c337e..352bc37d0db 100644 --- a/homeassistant/components/aseko_pool_live/translations/bg.json +++ b/homeassistant/components/aseko_pool_live/translations/bg.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "email": "Email", + "email": "\u0418\u043c\u0435\u0439\u043b", "password": "\u041f\u0430\u0440\u043e\u043b\u0430" } } diff --git a/homeassistant/components/bayesian/translations/hu.json b/homeassistant/components/bayesian/translations/hu.json new file mode 100644 index 00000000000..de97169d84a --- /dev/null +++ b/homeassistant/components/bayesian/translations/hu.json @@ -0,0 +1,12 @@ +{ + "issues": { + "manual_migration": { + "description": "A Bayesian integr\u00e1ci\u00f3 mostant\u00f3l akkor is friss\u00edti a val\u00f3sz\u00edn\u0171s\u00e9get, ha a megfigyelt `to_state`, `above`, `below` vagy `value_template` \u00e9rt\u00e9ke nem csak `True`, hanem `False`. \u00cdgy m\u00e1r nem sz\u00fcks\u00e9ges, hogy minden egyes bin\u00e1ris \u00e1llapothoz duplik\u00e1lt, egym\u00e1st kieg\u00e9sz\u00edt\u0151 bejegyz\u00e9sek legyenek. `{entity}` duplik\u00e1lt bejegyz\u00e9s\u00e9t most m\u00e1r elt\u00e1vol\u00edthatja.", + "title": "K\u00e9zi YAML jav\u00edt\u00e1s sz\u00fcks\u00e9ges a Bayesian-hoz" + }, + "no_prob_given_false": { + "description": "A Bayesian integr\u00e1ci\u00f3ban a `prob_given_false` mostant\u00f3l k\u00f6telez\u0151 konfigur\u00e1ci\u00f3s v\u00e1ltoz\u00f3, mivel a kor\u00e1bbi alap\u00e9rtelmezett \u00e9rt\u00e9knek nem volt matematikai alapja. K\u00e9rem, adja hozz\u00e1 ezt a `configuration.yml` f\u00e1jlhoz a `bayesian/{entity}`-hez. Ezek az esem\u00e9nyek mindaddig figyelmen k\u00edv\u00fcl maradnak, am\u00edg ezt v\u00e9gzi el.", + "title": "K\u00e9zi YAML-kieg\u00e9sz\u00edt\u00e9s sz\u00fcks\u00e9ges a Bayesian-hoz" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bayesian/translations/no.json b/homeassistant/components/bayesian/translations/no.json new file mode 100644 index 00000000000..600e30346d3 --- /dev/null +++ b/homeassistant/components/bayesian/translations/no.json @@ -0,0 +1,12 @@ +{ + "issues": { + "manual_migration": { + "description": "Den Bayesianske integrasjonen oppdaterer n\u00e5 ogs\u00e5 sannsynligheten hvis den observerte `til_tilstand`, `over`, `under` eller `verdimal` evalueres til `False` i stedet for bare `Sant`. S\u00e5 det er ikke lenger n\u00f8dvendig \u00e5 ha dupliserte, komplement\u00e6re oppf\u00f8ringer for hver bin\u00e6r tilstand. Vennligst fjern den speilvendte oppf\u00f8ringen for ` {entity} `.", + "title": "Manuell YAML-fix kreves for Bayesian" + }, + "no_prob_given_false": { + "description": "I den Bayesianske integrasjonen er 'prob_given_false' n\u00e5 en n\u00f8dvendig konfigurasjonsvariabel siden det ikke var noen matematisk begrunnelse for den forrige standardverdien. Vennligst legg dette til i `configuration.yml` for `bayesian/ {entity} `. Disse observasjonene vil bli ignorert til du gj\u00f8r det.", + "title": "Manuell YAML-tilsetning kreves for Bayesian" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/bg.json b/homeassistant/components/braviatv/translations/bg.json index 08ab17032d4..3192f9c39c7 100644 --- a/homeassistant/components/braviatv/translations/bg.json +++ b/homeassistant/components/braviatv/translations/bg.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "not_bravia_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Bravia.", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", @@ -14,7 +15,8 @@ "step": { "authorize": { "data": { - "pin": "\u041f\u0418\u041d \u043a\u043e\u0434" + "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", + "use_psk": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 PSK \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" } }, "confirm": { @@ -22,7 +24,8 @@ }, "reauth_confirm": { "data": { - "pin": "\u041f\u0418\u041d \u043a\u043e\u0434" + "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", + "use_psk": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 PSK \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" } }, "user": { diff --git a/homeassistant/components/braviatv/translations/et.json b/homeassistant/components/braviatv/translations/et.json index d057ea37151..4e3ca6333d4 100644 --- a/homeassistant/components/braviatv/translations/et.json +++ b/homeassistant/components/braviatv/translations/et.json @@ -30,7 +30,7 @@ "pin": "PIN kood", "use_psk": "PSK autentimise kasutamine" }, - "description": "Sisestage Sony Bravia teleril n\u00e4idatud PIN-kood. \n\nKui PIN-koodi ei kuvata, peate teleril Home Assistant'i registreerimise t\u00fchistama, minge aadressile: Seaded -> Network -> Remote device settings -> Deregister remote device. \n\nPIN-koodi asemel v\u00f5ite kasutada PSK (Pre-Shared-Key). PSK on kasutaja m\u00e4\u00e4ratud salajane v\u00f5ti, mida kasutatakse juurdep\u00e4\u00e4su kontrollimiseks. See autentimismeetod on soovitatav kui stabiilsem. PSK lubamiseks teleril minge aadressil: Settings -> Network -> Home Network Setup -> IP Control. Seej\u00e4rel m\u00e4rgistage ruut \"Kasutage PSK autentimist\" ja sisestage PIN-koodi asemel PSK." + "description": "Sisesta Sony Bravia teleril n\u00e4idatud PIN-kood. \n\nKui PIN-koodi ei kuvata, peadeleril Home Assistant'i registreerimise t\u00fchistama, mine aadressile: Seaded -> Network -> Remote device settings -> Deregister remote device. \n\nPIN-koodi asemel v\u00f5id kasutada PSK (Pre-Shared-Key). PSK on kasutaja m\u00e4\u00e4ratud salajane v\u00f5ti, mida kasutatakse juurdep\u00e4\u00e4su kontrollimiseks. See autentimismeetod on soovitatav kui stabiilsem. PSK lubamiseks teleril mine aadressil: Settings -> Network -> Home Network Setup -> IP Control. Seej\u00e4rel m\u00e4rgista ruut \"Kasutage PSK autentimist\" ja sisesta PIN-koodi asemel PSK." }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/he.json b/homeassistant/components/braviatv/translations/he.json index b717638aa2f..29c90cda769 100644 --- a/homeassistant/components/braviatv/translations/he.json +++ b/homeassistant/components/braviatv/translations/he.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", @@ -17,6 +18,11 @@ "confirm": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" }, + "reauth_confirm": { + "data": { + "pin": "\u05e7\u05d5\u05d3 PIN" + } + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7" diff --git a/homeassistant/components/braviatv/translations/nl.json b/homeassistant/components/braviatv/translations/nl.json index 18a4f8881fb..867840c21a3 100644 --- a/homeassistant/components/braviatv/translations/nl.json +++ b/homeassistant/components/braviatv/translations/nl.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "no_ip_control": "IP-besturing is uitgeschakeld op uw tv of de tv wordt niet ondersteund.", - "not_bravia_device": "Dit apparaat is geen Bravia-TV." + "not_bravia_device": "Dit apparaat is geen Bravia-TV.", + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -23,6 +24,11 @@ "confirm": { "description": "Wil je beginnen met instellen?" }, + "reauth_confirm": { + "data": { + "pin": "Pincode" + } + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json index 7fa16b90e8a..881cccde8a5 100644 --- a/homeassistant/components/braviatv/translations/no.json +++ b/homeassistant/components/braviatv/translations/no.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "no_ip_control": "IP-kontrollen er deaktivert p\u00e5 TVen eller TV-en st\u00f8ttes ikke.", - "not_bravia_device": "Enheten er ikke en Bravia TV." + "not_bravia_device": "Enheten er ikke en Bravia TV.", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_unsuccessful": "Re-autentisering mislyktes. Fjern integrasjonen og konfigurer den p\u00e5 nytt." }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -23,6 +25,13 @@ "confirm": { "description": "Vil du starte oppsettet?" }, + "reauth_confirm": { + "data": { + "pin": "PIN kode", + "use_psk": "Bruk PSK-autentisering" + }, + "description": "Skriv inn PIN-koden som vises p\u00e5 Sony Bravia TV. \n\n Hvis PIN-koden ikke vises, m\u00e5 du avregistrere Home Assistant p\u00e5 TV-en din, g\u00e5 til: Innstillinger - > Nettverk - > Innstillinger for ekstern enhet - > Avregistrer ekstern enhet. \n\n Du kan bruke PSK (Pre-Shared-Key) i stedet for PIN. PSK er en brukerdefinert hemmelig n\u00f8kkel som brukes til tilgangskontroll. Denne autentiseringsmetoden anbefales som mer stabil. For \u00e5 aktivere PSK p\u00e5 TV-en, g\u00e5 til: Innstillinger - > Nettverk - > Oppsett for hjemmenettverk - > IP-kontroll. Kryss s\u00e5 av \u00abBruk PSK-autentisering\u00bb-boksen og skriv inn din PSK i stedet for PIN-kode." + }, "user": { "data": { "host": "Vert" diff --git a/homeassistant/components/crownstone/translations/bg.json b/homeassistant/components/crownstone/translations/bg.json index 2c567e2a1e8..94752b315bb 100644 --- a/homeassistant/components/crownstone/translations/bg.json +++ b/homeassistant/components/crownstone/translations/bg.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "email": "Email", + "email": "\u0418\u043c\u0435\u0439\u043b", "password": "\u041f\u0430\u0440\u043e\u043b\u0430" } } diff --git a/homeassistant/components/demo/translations/bg.json b/homeassistant/components/demo/translations/bg.json index 2ecf8f371eb..bd761c705ff 100644 --- a/homeassistant/components/demo/translations/bg.json +++ b/homeassistant/components/demo/translations/bg.json @@ -10,6 +10,9 @@ } }, "title": "\u0417\u0430\u0445\u0440\u0430\u043d\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0435 \u0435 \u0441\u0442\u0430\u0431\u0438\u043b\u043d\u043e" + }, + "unfixable_problem": { + "title": "\u0422\u043e\u0432\u0430 \u043d\u0435 \u0435 \u043f\u043e\u043f\u0440\u0430\u0432\u0438\u043c \u043f\u0440\u043e\u0431\u043b\u0435\u043c" } }, "title": "\u0414\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u044f" diff --git a/homeassistant/components/devolo_home_control/translations/bg.json b/homeassistant/components/devolo_home_control/translations/bg.json index a22746a1dd4..47ab5f03cbc 100644 --- a/homeassistant/components/devolo_home_control/translations/bg.json +++ b/homeassistant/components/devolo_home_control/translations/bg.json @@ -7,7 +7,8 @@ "step": { "user": { "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u0418\u043c\u0435\u0439\u043b / devolo ID" } }, "zeroconf_confirm": { diff --git a/homeassistant/components/econet/translations/bg.json b/homeassistant/components/econet/translations/bg.json index 3468d506903..637413ad06d 100644 --- a/homeassistant/components/econet/translations/bg.json +++ b/homeassistant/components/econet/translations/bg.json @@ -7,6 +7,7 @@ "step": { "user": { "data": { + "email": "\u0418\u043c\u0435\u0439\u043b", "password": "\u041f\u0430\u0440\u043e\u043b\u0430" } } diff --git a/homeassistant/components/flipr/translations/bg.json b/homeassistant/components/flipr/translations/bg.json index 4e79121f56b..fbc626a83d7 100644 --- a/homeassistant/components/flipr/translations/bg.json +++ b/homeassistant/components/flipr/translations/bg.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "email": "Email", + "email": "\u0418\u043c\u0435\u0439\u043b", "password": "\u041f\u0430\u0440\u043e\u043b\u0430" }, "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Flipr" diff --git a/homeassistant/components/forked_daapd/translations/no.json b/homeassistant/components/forked_daapd/translations/no.json index 4461101cbc2..05093ac08c7 100644 --- a/homeassistant/components/forked_daapd/translations/no.json +++ b/homeassistant/components/forked_daapd/translations/no.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "not_forked_daapd": "Enheten er ikke en forked-daapd-server." + "not_forked_daapd": "Enheten er ikke en Owntone-server." }, "error": { - "forbidden": "Kan ikke koble til, vennligst sjekk dine forked-daapd nettverkstillatelser", + "forbidden": "Kan ikke koble til. Sjekk dine Owntone-nettverkstillatelser.", "unknown_error": "Uventet feil", - "websocket_not_enabled": "websocket for forked-daapd server ikke aktivert.", + "websocket_not_enabled": "Owntone server websocket ikke aktivert.", "wrong_host_or_port": "Kan ikke koble til. Vennligst sjekk vert og port.", "wrong_password": "Feil passord.", - "wrong_server_type": "Forked-daapd integrasjon krever en gaffel-daapd server med versjon \"= 27.0." + "wrong_server_type": "Owntone-integrasjonen krever en Owntone-server med versjon > = 27.0." }, "flow_title": "{name} ( {host} )", "step": { @@ -21,7 +21,7 @@ "password": "API-passord (la st\u00e5 tomt hvis ingen passord)", "port": "" }, - "title": "Konfigurere forked-daapd-enhet" + "title": "Sett opp Owntone-enhet" } } }, @@ -34,8 +34,8 @@ "tts_pause_time": "Sekunder for \u00e5 sette pause f\u00f8r og etter TTS", "tts_volume": "TTS-volum (flyter i omr\u00e5det [0,1])" }, - "description": "Angi ulike alternativer for forked-daapd integrasjon.", - "title": "Konfigurer alternativer for forked-daapd" + "description": "Angi ulike alternativer for Owntone-integrasjonen.", + "title": "Konfigurer Owntone-alternativer" } } } diff --git a/homeassistant/components/hangouts/translations/bg.json b/homeassistant/components/hangouts/translations/bg.json index 09ffce392a6..8d8dae90ce0 100644 --- a/homeassistant/components/hangouts/translations/bg.json +++ b/homeassistant/components/hangouts/translations/bg.json @@ -19,7 +19,7 @@ "user": { "data": { "authorization_code": "\u041a\u043e\u0434 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f (\u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c \u0437\u0430 \u0440\u044a\u0447\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435)", - "email": "E-mail \u0430\u0434\u0440\u0435\u0441", + "email": "\u0418\u043c\u0435\u0439\u043b", "password": "\u041f\u0430\u0440\u043e\u043b\u0430" }, "title": "\u0412\u0445\u043e\u0434 \u0432 Google Hangouts" diff --git a/homeassistant/components/icloud/translations/bg.json b/homeassistant/components/icloud/translations/bg.json index 3c54ef831d1..fb3d0a4ce48 100644 --- a/homeassistant/components/icloud/translations/bg.json +++ b/homeassistant/components/icloud/translations/bg.json @@ -24,7 +24,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "username": "Email" + "username": "\u0418\u043c\u0435\u0439\u043b" } }, "verification_code": { diff --git a/homeassistant/components/intellifire/translations/bg.json b/homeassistant/components/intellifire/translations/bg.json index 9df377170f4..0a0b8c64e8e 100644 --- a/homeassistant/components/intellifire/translations/bg.json +++ b/homeassistant/components/intellifire/translations/bg.json @@ -14,7 +14,7 @@ "api_config": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "username": "Email" + "username": "\u0418\u043c\u0435\u0439\u043b" } }, "dhcp_confirm": { diff --git a/homeassistant/components/lametric/translations/nl.json b/homeassistant/components/lametric/translations/nl.json index f88338f4410..6907ef1fb33 100644 --- a/homeassistant/components/lametric/translations/nl.json +++ b/homeassistant/components/lametric/translations/nl.json @@ -13,7 +13,8 @@ "step": { "choice_enter_manual_or_fetch_cloud": { "menu_options": { - "manual_entry": "Handmatig invoeren" + "manual_entry": "Handmatig invoeren", + "pick_implementation": "Importeren van LaMetric.com (aanbevolen)" } }, "manual_entry": { @@ -24,6 +25,11 @@ }, "pick_implementation": { "title": "Kies een authenticatie methode" + }, + "user_cloud_select_device": { + "data": { + "device": "Selecteer het LaMetric-apparaat dat u wilt toevoegen" + } } } } diff --git a/homeassistant/components/mazda/translations/bg.json b/homeassistant/components/mazda/translations/bg.json index 6e9ce8d9a6a..1eb89184642 100644 --- a/homeassistant/components/mazda/translations/bg.json +++ b/homeassistant/components/mazda/translations/bg.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "email": "Email", + "email": "\u0418\u043c\u0435\u0439\u043b", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "region": "\u0420\u0435\u0433\u0438\u043e\u043d" } diff --git a/homeassistant/components/melcloud/translations/bg.json b/homeassistant/components/melcloud/translations/bg.json index 102f5304f60..5c098daefa8 100644 --- a/homeassistant/components/melcloud/translations/bg.json +++ b/homeassistant/components/melcloud/translations/bg.json @@ -9,7 +9,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "username": "Email" + "username": "\u0418\u043c\u0435\u0439\u043b" }, "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 MELCloud" } diff --git a/homeassistant/components/mikrotik/translations/he.json b/homeassistant/components/mikrotik/translations/he.json index bef2f812e0f..5ea9d3200a8 100644 --- a/homeassistant/components/mikrotik/translations/he.json +++ b/homeassistant/components/mikrotik/translations/he.json @@ -1,13 +1,20 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", diff --git a/homeassistant/components/mikrotik/translations/no.json b/homeassistant/components/mikrotik/translations/no.json index 73efde429fe..baad78fd111 100644 --- a/homeassistant/components/mikrotik/translations/no.json +++ b/homeassistant/components/mikrotik/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -9,6 +10,13 @@ "name_exists": "Navnet eksisterer" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Passordet for {username} er ugyldig.", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "host": "Vert", diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json index 89ba7bc8b7c..62514bf34a4 100644 --- a/homeassistant/components/nest/translations/no.json +++ b/homeassistant/components/nest/translations/no.json @@ -52,7 +52,7 @@ "data": { "project_id": "Prosjekt-ID for enhetstilgang" }, - "description": "Opprett et Nest Device Access-prosjekt som **krever en avgift p\u00e5 USD 5** for \u00e5 konfigurere.\n 1. G\u00e5 til [Device Access Console]( {device_access_console_url} ), og gjennom betalingsflyten.\n 1. Klikk p\u00e5 **Opprett prosjekt**\n 1. Gi Device Access-prosjektet ditt et navn og klikk p\u00e5 **Neste**.\n 1. Skriv inn din OAuth-klient-ID\n 1. Aktiver hendelser ved \u00e5 klikke **Aktiver** og **Opprett prosjekt**. \n\n Skriv inn Device Access Project ID nedenfor ([mer info]( {more_info_url} )).\n", + "description": "Opprett et Nest Device Access-prosjekt som **krever \u00e5 betale Google en avgift p\u00e5 USD 5** for \u00e5 konfigurere.\n 1. G\u00e5 til [Device Access Console]( {device_access_console_url} ), og gjennom betalingsflyten.\n 1. Klikk p\u00e5 **Opprett prosjekt**\n 1. Gi Device Access-prosjektet ditt et navn og klikk p\u00e5 **Neste**.\n 1. Skriv inn din OAuth-klient-ID\n 1. Aktiver hendelser ved \u00e5 klikke **Aktiver** og **Opprett prosjekt**. \n\n Skriv inn Device Access Project ID nedenfor ([mer info]( {more_info_url} )).\n", "title": "Nest: Opprett et Device Access Project" }, "device_project_upgrade": { diff --git a/homeassistant/components/nobo_hub/translations/nl.json b/homeassistant/components/nobo_hub/translations/nl.json index 9a8e90bcdb5..13ead2a1460 100644 --- a/homeassistant/components/nobo_hub/translations/nl.json +++ b/homeassistant/components/nobo_hub/translations/nl.json @@ -15,6 +15,12 @@ "ip_address": "IP-adres", "serial": "Serienummer (12 cijfers)" } + }, + "user": { + "data": { + "device": "Ontdekte hubs" + }, + "description": "Selecteer een Nob\u00f8 Ecohub om te configureren." } } } diff --git a/homeassistant/components/octoprint/translations/he.json b/homeassistant/components/octoprint/translations/he.json index 356676babee..dcb6c3a173a 100644 --- a/homeassistant/components/octoprint/translations/he.json +++ b/homeassistant/components/octoprint/translations/he.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { @@ -10,6 +11,11 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { + "reauth_confirm": { + "data": { + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", diff --git a/homeassistant/components/octoprint/translations/no.json b/homeassistant/components/octoprint/translations/no.json index 0432b190e8c..870a5188ff0 100644 --- a/homeassistant/components/octoprint/translations/no.json +++ b/homeassistant/components/octoprint/translations/no.json @@ -4,6 +4,7 @@ "already_configured": "Enheten er allerede konfigurert", "auth_failed": "Kan ikke hente APAn\u00f8kkel for program", "cannot_connect": "Tilkobling mislyktes", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "unknown": "Uventet feil" }, "error": { @@ -15,6 +16,11 @@ "get_api_key": "\u00c5pne OctoPrint UI og klikk \"Tillat\" p\u00e5 tilgangsforesp\u00f8rselen for \"Home Assistant\"." }, "step": { + "reauth_confirm": { + "data": { + "username": "Brukernavn" + } + }, "user": { "data": { "host": "Vert", diff --git a/homeassistant/components/plum_lightpad/translations/bg.json b/homeassistant/components/plum_lightpad/translations/bg.json index 597f4d36165..ba64157f269 100644 --- a/homeassistant/components/plum_lightpad/translations/bg.json +++ b/homeassistant/components/plum_lightpad/translations/bg.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "username": "Email" + "username": "\u0418\u043c\u0435\u0439\u043b" } } } diff --git a/homeassistant/components/poolsense/translations/bg.json b/homeassistant/components/poolsense/translations/bg.json index eb033e74f0f..71363189379 100644 --- a/homeassistant/components/poolsense/translations/bg.json +++ b/homeassistant/components/poolsense/translations/bg.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "email": "Email", + "email": "\u0418\u043c\u0435\u0439\u043b", "password": "\u041f\u0430\u0440\u043e\u043b\u0430" } } diff --git a/homeassistant/components/renault/translations/bg.json b/homeassistant/components/renault/translations/bg.json index f074b9653c5..364d397cb57 100644 --- a/homeassistant/components/renault/translations/bg.json +++ b/homeassistant/components/renault/translations/bg.json @@ -18,7 +18,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "username": "Email" + "username": "\u0418\u043c\u0435\u0439\u043b" } } } diff --git a/homeassistant/components/rituals_perfume_genie/translations/bg.json b/homeassistant/components/rituals_perfume_genie/translations/bg.json index 05ef3ed780e..7be659cab0b 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/bg.json +++ b/homeassistant/components/rituals_perfume_genie/translations/bg.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "email": "\u0418\u043c\u0435\u0439\u043b", "password": "\u041f\u0430\u0440\u043e\u043b\u0430" } } diff --git a/homeassistant/components/rtsp_to_webrtc/translations/bg.json b/homeassistant/components/rtsp_to_webrtc/translations/bg.json index 1c6120581b0..fbf65852d55 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/bg.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/bg.json @@ -3,5 +3,14 @@ "abort": { "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "\u0410\u0434\u0440\u0435\u0441 \u043d\u0430 Stun \u0441\u044a\u0440\u0432\u044a\u0440\u0430 (\u0445\u043e\u0441\u0442:\u043f\u043e\u0440\u0442)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rtsp_to_webrtc/translations/de.json b/homeassistant/components/rtsp_to_webrtc/translations/de.json index f836d8f3b49..75c7d590aa8 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/de.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/de.json @@ -23,5 +23,14 @@ "title": "RTSPtoWebRTC konfigurieren" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Adresse des Stun-Servers (host:port)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rtsp_to_webrtc/translations/es.json b/homeassistant/components/rtsp_to_webrtc/translations/es.json index ea3e8c4afc1..31de18c757b 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/es.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/es.json @@ -23,5 +23,14 @@ "title": "Configurar RTSPtoWebRTC" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Direcci\u00f3n del servidor Stun (host:puerto)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rtsp_to_webrtc/translations/et.json b/homeassistant/components/rtsp_to_webrtc/translations/et.json index e440831eb35..6eeaf1eee68 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/et.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/et.json @@ -23,5 +23,14 @@ "title": "RTSPtoWebRTC seadistamine" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Stun serveri aadress (host:port)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rtsp_to_webrtc/translations/hu.json b/homeassistant/components/rtsp_to_webrtc/translations/hu.json index 5da10dd88e4..3eafb9b4f19 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/hu.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/hu.json @@ -23,5 +23,14 @@ "title": "RTSPtoWebRTC konfigur\u00e1l\u00e1sa" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Stun szerver c\u00edme (host:port)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rtsp_to_webrtc/translations/no.json b/homeassistant/components/rtsp_to_webrtc/translations/no.json index 9f163b2099d..f5d8e32b041 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/no.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/no.json @@ -23,5 +23,14 @@ "title": "Konfigurer RTSPtoWebRTC" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Sl\u00e5 serveradresse (vert:port)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rtsp_to_webrtc/translations/pt-BR.json b/homeassistant/components/rtsp_to_webrtc/translations/pt-BR.json index 78560798862..707203441fe 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/pt-BR.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/pt-BR.json @@ -23,5 +23,14 @@ "title": "Configurar RTSPtoWebRTC" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Endere\u00e7o do servidor de atordoamento (host:porta)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rtsp_to_webrtc/translations/ru.json b/homeassistant/components/rtsp_to_webrtc/translations/ru.json index c6ba40a0d73..9441ea7d232 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/ru.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/ru.json @@ -23,5 +23,14 @@ "title": "RTSPtoWebRTC" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "\u0410\u0434\u0440\u0435\u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Stun (\u0445\u043e\u0441\u0442:\u043f\u043e\u0440\u0442)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rtsp_to_webrtc/translations/zh-Hant.json b/homeassistant/components/rtsp_to_webrtc/translations/zh-Hant.json index ace2e23312b..529e85fd978 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/zh-Hant.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/zh-Hant.json @@ -23,5 +23,14 @@ "title": "\u8a2d\u5b9a RTSPtoWebRTC" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Stun \u4f3a\u670d\u5668\u4f4d\u5740\uff08\u4e3b\u6a5f\uff1a\u901a\u8a0a\u57e0\uff09" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sense/translations/bg.json b/homeassistant/components/sense/translations/bg.json index f81ad124c51..91bf013c047 100644 --- a/homeassistant/components/sense/translations/bg.json +++ b/homeassistant/components/sense/translations/bg.json @@ -14,6 +14,11 @@ }, "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" }, + "user": { + "data": { + "email": "\u0418\u043c\u0435\u0439\u043b" + } + }, "validation": { "data": { "code": "\u041a\u043e\u0434 \u0437\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430" diff --git a/homeassistant/components/sensor/translations/nl.json b/homeassistant/components/sensor/translations/nl.json index 69e18061858..aaf71635015 100644 --- a/homeassistant/components/sensor/translations/nl.json +++ b/homeassistant/components/sensor/translations/nl.json @@ -6,11 +6,13 @@ "is_carbon_dioxide": "Huidig niveau {entity_name} kooldioxideconcentratie", "is_carbon_monoxide": "Huidig niveau {entity_name} koolmonoxideconcentratie", "is_current": "Huidige {entity_name} stroom", + "is_distance": "Huidig afstand van {entity_name}", "is_energy": "Huidige {entity_name} energie", "is_frequency": "Huidige {entity_name} frequentie", "is_gas": "Huidig {entity_name} gas", "is_humidity": "Huidige {entity_name} vochtigheidsgraad", "is_illuminance": "Huidige {entity_name} verlichtingssterkte", + "is_moisture": "Huidige vochtigheid van {entity_name}", "is_nitrogen_dioxide": "Huidige {entity_name} stikstofdioxideconcentratie", "is_nitrogen_monoxide": "Huidige {entity_name} stikstofmonoxideconcentratie", "is_nitrous_oxide": "Huidige {entity_name} distikstofmonoxideconcentratie", @@ -23,11 +25,14 @@ "is_pressure": "Huidige {entity_name} druk", "is_reactive_power": "Huidig {entity_name} blindvermogen", "is_signal_strength": "Huidige {entity_name} signaalsterkte", + "is_speed": "Huidige snelheid van {entity_name}", "is_sulphur_dioxide": "Huidige {entity_name} zwaveldioxideconcentratie", "is_temperature": "Huidige {entity_name} temperatuur", "is_value": "Huidige {entity_name} waarde", "is_volatile_organic_compounds": "Huidig {entity_name} vluchtige-organische-stoffenconcentratieniveau", - "is_voltage": "Huidige {entity_name} spanning" + "is_voltage": "Huidige {entity_name} spanning", + "is_volume": "Huidig volume van {entity_name}", + "is_weight": "Huidig gewicht van {entity_name}" }, "trigger_type": { "apparent_power": "{entity_name} schijnbare vermogensveranderingen", @@ -35,11 +40,13 @@ "carbon_dioxide": "{entity_name} kooldioxideconcentratie gewijzigd", "carbon_monoxide": "{entity_name} koolmonoxideconcentratie gewijzigd", "current": "{entity_name} huidige wijzigingen", + "distance": "Afstand van {entity_name} veranderd", "energy": "{entity_name} energieveranderingen", "frequency": "{entity_name} frequentie verandert", "gas": "{entity_name} gas verandert", "humidity": "{entity_name} vochtigheidsgraad gewijzigd", "illuminance": "{entity_name} verlichtingssterkte gewijzigd", + "moisture": "Vochtigheid van {entity_name} veranderd", "nitrogen_dioxide": "{entity_name} stikstofdioxideconcentratieverandering", "nitrogen_monoxide": "{entity_name} stikstofmonoxideconcentratieverandering", "nitrous_oxide": "{entity_name} distikstofmonoxideconcentratieverandering", @@ -52,11 +59,14 @@ "pressure": "{entity_name} druk gewijzigd", "reactive_power": "{entity_name} blindvermogen veranderingen", "signal_strength": "{entity_name} signaalsterkte gewijzigd", + "speed": "Snelheid van {entity_name} veranderd", "sulphur_dioxide": "{entity_name} zwaveldioxideconcentratieveranderingen", "temperature": "{entity_name} temperatuur gewijzigd", "value": "{entity_name} waarde gewijzigd", "volatile_organic_compounds": "{entity_name} vluchtige-organische-stoffenconcentratieveranderingen", - "voltage": "{entity_name} voltage verandert" + "voltage": "{entity_name} voltage verandert", + "volume": "Volume van {entity_name} veranderd", + "weight": "Gewicht van {entity_name} veranderd" } }, "state": { diff --git a/homeassistant/components/sensor/translations/no.json b/homeassistant/components/sensor/translations/no.json index 3fb1c7a793d..f75991aa881 100644 --- a/homeassistant/components/sensor/translations/no.json +++ b/homeassistant/components/sensor/translations/no.json @@ -31,7 +31,8 @@ "is_value": "Gjeldende {entity_name} verdi", "is_volatile_organic_compounds": "Gjeldende {entity_name} flyktige organiske forbindelser", "is_voltage": "Gjeldende {entity_name} spenning", - "is_volume": "Gjeldende {entity_name} -volum" + "is_volume": "Gjeldende {entity_name} -volum", + "is_weight": "N\u00e5v\u00e6rende vekt p\u00e5 {entity_name}" }, "trigger_type": { "apparent_power": "{entity_name} tilsynelatende kraftendringer", @@ -64,7 +65,8 @@ "value": "{entity_name} verdi endringer", "volatile_organic_compounds": "{entity_name} konsentrasjon av flyktige organiske forbindelser", "voltage": "{entity_name} spenningsendringer", - "volume": "{entity_name} volumendringer" + "volume": "{entity_name} volumendringer", + "weight": "Vektendringer {entity_name}" } }, "state": { diff --git a/homeassistant/components/skybell/translations/bg.json b/homeassistant/components/skybell/translations/bg.json index a8057a452ab..556fba19234 100644 --- a/homeassistant/components/skybell/translations/bg.json +++ b/homeassistant/components/skybell/translations/bg.json @@ -19,7 +19,7 @@ }, "user": { "data": { - "email": "Email", + "email": "\u0418\u043c\u0435\u0439\u043b", "password": "\u041f\u0430\u0440\u043e\u043b\u0430" } } diff --git a/homeassistant/components/smarttub/translations/bg.json b/homeassistant/components/smarttub/translations/bg.json index 48078b96c08..85d58d202d3 100644 --- a/homeassistant/components/smarttub/translations/bg.json +++ b/homeassistant/components/smarttub/translations/bg.json @@ -9,6 +9,7 @@ }, "user": { "data": { + "email": "\u0418\u043c\u0435\u0439\u043b", "password": "\u041f\u0430\u0440\u043e\u043b\u0430" } } diff --git a/homeassistant/components/spotify/translations/bg.json b/homeassistant/components/spotify/translations/bg.json index 35e3428d677..b9da3fe07e8 100644 --- a/homeassistant/components/spotify/translations/bg.json +++ b/homeassistant/components/spotify/translations/bg.json @@ -8,6 +8,7 @@ }, "issues": { "removed_yaml": { + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Spotify \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u043e.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043e\u0442 Home Assistant.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Spotify \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" } } diff --git a/homeassistant/components/steam_online/translations/bg.json b/homeassistant/components/steam_online/translations/bg.json index 66ae6cc081f..8d946452ca0 100644 --- a/homeassistant/components/steam_online/translations/bg.json +++ b/homeassistant/components/steam_online/translations/bg.json @@ -19,5 +19,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Steam \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u043e.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043e\u0442 Home Assistant.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Steam \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/nl.json b/homeassistant/components/tasmota/translations/nl.json index a116e840977..116f7142d63 100644 --- a/homeassistant/components/tasmota/translations/nl.json +++ b/homeassistant/components/tasmota/translations/nl.json @@ -16,5 +16,10 @@ "description": "Wil je Tasmota instellen?" } } + }, + "issues": { + "topic_no_prefix": { + "title": "Het Tasmota-apparaat {name} heeft een ongeldig MQTT-topic" + } } } \ No newline at end of file diff --git a/homeassistant/components/tile/translations/bg.json b/homeassistant/components/tile/translations/bg.json index 516ddb3d015..08a4edb4db8 100644 --- a/homeassistant/components/tile/translations/bg.json +++ b/homeassistant/components/tile/translations/bg.json @@ -16,7 +16,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "username": "Email" + "username": "\u0418\u043c\u0435\u0439\u043b" } } } diff --git a/homeassistant/components/tractive/translations/bg.json b/homeassistant/components/tractive/translations/bg.json index bd02d32720a..0276f3b11cd 100644 --- a/homeassistant/components/tractive/translations/bg.json +++ b/homeassistant/components/tractive/translations/bg.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "email": "Email", + "email": "\u0418\u043c\u0435\u0439\u043b", "password": "\u041f\u0430\u0440\u043e\u043b\u0430" } } diff --git a/homeassistant/components/verisure/translations/bg.json b/homeassistant/components/verisure/translations/bg.json index f5447e1d865..927c79f2674 100644 --- a/homeassistant/components/verisure/translations/bg.json +++ b/homeassistant/components/verisure/translations/bg.json @@ -13,10 +13,20 @@ "code": "\u041a\u043e\u0434 \u0437\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430" } }, + "reauth_confirm": { + "data": { + "email": "\u0418\u043c\u0435\u0439\u043b" + } + }, "reauth_mfa": { "data": { "code": "\u041a\u043e\u0434 \u0437\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430" } + }, + "user": { + "data": { + "email": "\u0418\u043c\u0435\u0439\u043b" + } } } } diff --git a/homeassistant/components/vesync/translations/bg.json b/homeassistant/components/vesync/translations/bg.json index c435a669d5a..56cdd7e1d91 100644 --- a/homeassistant/components/vesync/translations/bg.json +++ b/homeassistant/components/vesync/translations/bg.json @@ -7,7 +7,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "username": "E-mail \u0430\u0434\u0440\u0435\u0441" + "username": "\u0418\u043c\u0435\u0439\u043b" }, "title": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430" } diff --git a/homeassistant/components/vicare/translations/bg.json b/homeassistant/components/vicare/translations/bg.json index 242339c3815..e6c49007788 100644 --- a/homeassistant/components/vicare/translations/bg.json +++ b/homeassistant/components/vicare/translations/bg.json @@ -13,7 +13,7 @@ "data": { "client_id": "API \u043a\u043b\u044e\u0447", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "username": "Email" + "username": "\u0418\u043c\u0435\u0439\u043b" } } } diff --git a/homeassistant/components/volvooncall/translations/bg.json b/homeassistant/components/volvooncall/translations/bg.json index 598cf3c837f..62a0a14568d 100644 --- a/homeassistant/components/volvooncall/translations/bg.json +++ b/homeassistant/components/volvooncall/translations/bg.json @@ -15,6 +15,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "region": "\u0420\u0435\u0433\u0438\u043e\u043d", "scandinavian_miles": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0441\u043a\u0430\u043d\u0434\u0438\u043d\u0430\u0432\u0441\u043a\u0438 \u043c\u0438\u043b\u0438", + "unit_system": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0438", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } diff --git a/homeassistant/components/zha/translations/bg.json b/homeassistant/components/zha/translations/bg.json index 01a0ed3e134..cd133985a32 100644 --- a/homeassistant/components/zha/translations/bg.json +++ b/homeassistant/components/zha/translations/bg.json @@ -116,7 +116,8 @@ }, "options": { "abort": { - "not_zha_device": "\u0422\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0435 zha \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + "not_zha_device": "\u0422\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0435 zha \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" diff --git a/homeassistant/components/zha/translations/he.json b/homeassistant/components/zha/translations/he.json index 16f25bf00d7..f48b30bd826 100644 --- a/homeassistant/components/zha/translations/he.json +++ b/homeassistant/components/zha/translations/he.json @@ -57,6 +57,7 @@ "abort": { "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, + "flow_title": "{name}", "step": { "init": { "title": "\u05d4\u05d2\u05d3\u05e8\u05d4 \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc ZHA" diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json index c469eb54bd7..b4c8a87aa2f 100644 --- a/homeassistant/components/zha/translations/no.json +++ b/homeassistant/components/zha/translations/no.json @@ -116,6 +116,8 @@ }, "device_automation": { "action_type": { + "issue_all_led_effect": "Utstedelseseffekt for alle lysdioder", + "issue_individual_led_effect": "Utstedelseseffekt for individuell LED", "squawk": "Squawk", "warn": "Advare" }, diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json index f87b7a701e3..f57791b2333 100644 --- a/homeassistant/components/zwave_js/translations/nl.json +++ b/homeassistant/components/zwave_js/translations/nl.json @@ -91,6 +91,11 @@ "zwave_js.value_updated.value": "Waardeverandering op een Z-Wave JS-waarde" } }, + "issues": { + "invalid_server_version": { + "title": "Nieuwere versie van Z-Wave JS-server vereist" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Ophalen van ontdekkingsinformatie voor Z-Wave JS-add-on is mislukt.", From 214c2934de9ac7a8e28a59044dff114c4e2dabfa Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 5 Oct 2022 08:20:37 +0200 Subject: [PATCH 169/985] Bump UniFi dependency to v37 (#79617) --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 0ff781418d4..6bf9f8aa473 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==36"], + "requirements": ["aiounifi==37"], "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index a71fceed31a..a6c341b41f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,7 +276,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==36 +aiounifi==37 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c5394b3d80..af1e4372d52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -251,7 +251,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==36 +aiounifi==37 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From 416c10a793a982fb8c17259d36b99be458131cd0 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Wed, 5 Oct 2022 02:27:56 -0400 Subject: [PATCH 170/985] Supervisor update entity auto update from api (#79611) * Supervisor update entity auto update from api * Update api mocks in tests --- homeassistant/components/hassio/update.py | 6 +++++- tests/components/hassio/test_binary_sensor.py | 1 + tests/components/hassio/test_init.py | 9 ++++++++- tests/components/hassio/test_sensor.py | 1 + tests/components/hassio/test_update.py | 1 + 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/update.py b/homeassistant/components/hassio/update.py index e68dbece5b6..dcb2b18cdd3 100644 --- a/homeassistant/components/hassio/update.py +++ b/homeassistant/components/hassio/update.py @@ -219,7 +219,6 @@ class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity): class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity): """Update entity to handle updates for the Home Assistant Supervisor.""" - _attr_auto_update = True _attr_supported_features = UpdateEntityFeature.INSTALL _attr_title = "Home Assistant Supervisor" @@ -233,6 +232,11 @@ class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity): """Return native value of entity.""" return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_VERSION] + @property + def auto_update(self) -> bool: + """Return true if auto-update is enabled for supervisor.""" + return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_AUTO_UPDATE] + @property def release_url(self) -> str | None: """URL to the full release notes of the latest version available.""" diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py index 31667efadc6..a601f98f1c5 100644 --- a/tests/components/hassio/test_binary_sensor.py +++ b/tests/components/hassio/test_binary_sensor.py @@ -75,6 +75,7 @@ def mock_all(aioclient_mock, request): "result": "ok", "version": "1.0.0", "version_latest": "1.0.0", + "auto_update": True, "addons": [ { "name": "test", diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 41b679e448a..f0f94661d50 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -92,7 +92,11 @@ def mock_all(aioclient_mock, request, os_info): "http://127.0.0.1/supervisor/info", json={ "result": "ok", - "data": {"version_latest": "1.0.0", "version": "1.0.0"}, + "data": { + "version_latest": "1.0.0", + "version": "1.0.0", + "auto_update": True, + }, "addons": [ { "name": "test", @@ -536,6 +540,7 @@ async def test_device_registry_calls(hass): supervisor_mock_data = { "version": "1.0.0", "version_latest": "1.0.0", + "auto_update": True, "addons": [ { "name": "test", @@ -586,6 +591,7 @@ async def test_device_registry_calls(hass): supervisor_mock_data = { "version": "1.0.0", "version_latest": "1.0.0", + "auto_update": True, "addons": [ { "name": "test2", @@ -620,6 +626,7 @@ async def test_device_registry_calls(hass): supervisor_mock_data = { "version": "1.0.0", "version_latest": "1.0.0", + "auto_update": True, "addons": [ { "name": "test2", diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index 868448cec2d..16cce09b800 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -68,6 +68,7 @@ def mock_all(aioclient_mock, request): "result": "ok", "version": "1.0.0", "version_latest": "1.0.0", + "auto_update": True, "addons": [ { "name": "test", diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index 48f6d894de0..aaa77cde129 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -79,6 +79,7 @@ def mock_all(aioclient_mock, request): "result": "ok", "version": "1.0.0", "version_latest": "1.0.1dev222", + "auto_update": True, "addons": [ { "name": "test", From bcbce6f1591ac005675fa2b720057b3fb20a071e Mon Sep 17 00:00:00 2001 From: kpine Date: Tue, 4 Oct 2022 23:30:34 -0700 Subject: [PATCH 171/985] Allow picking multiple entity targets for zwave_js.refresh_value service (#79634) Allow selection of multiple entities for zwave_js.refresh_value service --- homeassistant/components/zwave_js/services.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index eccd46745a3..687d486888c 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -105,6 +105,7 @@ refresh_value: selector: entity: integration: zwave_js + multiple: true refresh_all_values: name: Refresh all values? description: Whether to refresh all values (true) or just the primary value (false) From 905950f341236d214a27d822b84b223b1985fe1f Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 5 Oct 2022 08:58:24 +0200 Subject: [PATCH 172/985] Netatmo add supported brands (#79563) --- homeassistant/components/netatmo/manifest.json | 8 +++++++- homeassistant/generated/supported_brands.py | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 4095762c666..74d34056241 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -11,5 +11,11 @@ "models": ["Healty Home Coach", "Netatmo Relay", "Presence", "Welcome"] }, "iot_class": "cloud_polling", - "loggers": ["pyatmo"] + "loggers": ["pyatmo"], + "supported_brands": { + "legrand": "Legrand", + "bubendorff": "Bubendorff", + "smarther": "Smarther", + "bticino": "BTicino" + } } diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py index 50490d2c847..5c641c0f95e 100644 --- a/homeassistant/generated/supported_brands.py +++ b/homeassistant/generated/supported_brands.py @@ -8,6 +8,7 @@ HAS_SUPPORTED_BRANDS = [ "hunterdouglas_powerview", "inkbird", "motion_blinds", + "netatmo", "overkiz", "renault", "thermobeacon", From 92c9ddf3e3374333ba29cf1ea8d78b2ef352c4b7 Mon Sep 17 00:00:00 2001 From: Jafar Atili Date: Wed, 5 Oct 2022 10:25:46 +0300 Subject: [PATCH 173/985] Add supported brands for switchbee (#79595) --- homeassistant/components/switchbee/manifest.json | 5 ++++- homeassistant/generated/supported_brands.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switchbee/manifest.json b/homeassistant/components/switchbee/manifest.json index 75e5b2e9bfd..5ca066e3bc0 100644 --- a/homeassistant/components/switchbee/manifest.json +++ b/homeassistant/components/switchbee/manifest.json @@ -5,5 +5,8 @@ "documentation": "https://www.home-assistant.io/integrations/switchbee", "requirements": ["pyswitchbee==1.5.5"], "codeowners": ["@jafar-atili"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "supported_brands": { + "bswitch": "BSwitch" + } } diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py index 5c641c0f95e..15f2a580a29 100644 --- a/homeassistant/generated/supported_brands.py +++ b/homeassistant/generated/supported_brands.py @@ -11,6 +11,7 @@ HAS_SUPPORTED_BRANDS = [ "netatmo", "overkiz", "renault", + "switchbee", "thermobeacon", "wemo", "yalexs_ble", From 42ca4764a06fd583e07acf0a7848da1fd0bf12e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Oct 2022 10:53:27 +0300 Subject: [PATCH 174/985] Bump actions/checkout from 3.0.2 to 3.1.0 (#79635) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.0.2 to 3.1.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3.0.2...v3.1.0) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 12 ++++++------ .github/workflows/ci.yaml | 28 ++++++++++++++-------------- .github/workflows/translations.yaml | 4 ++-- .github/workflows/wheels.yml | 6 +++--- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 5516af1ab4d..4c3dc19a040 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -24,7 +24,7 @@ jobs: publish: ${{ steps.version.outputs.publish }} steps: - name: Checkout the repository - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 with: fetch-depth: 0 @@ -67,7 +67,7 @@ jobs: if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' steps: - name: Checkout the repository - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v4.1.0 @@ -100,7 +100,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Download nightly wheels of frontend if: needs.init.outputs.channel == 'dev' @@ -198,7 +198,7 @@ jobs: - yellow steps: - name: Checkout the repository - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set build additional args run: | @@ -241,7 +241,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Initialize git uses: home-assistant/actions/helpers/git-init@master @@ -280,7 +280,7 @@ jobs: - "homeassistant" steps: - name: Checkout the repository - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Login to DockerHub if: matrix.registry == 'homeassistant' diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0c3bbe21afd..5f4b745091e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -58,7 +58,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Generate partial Python venv restore key id: generate_python_cache_key run: >- @@ -169,7 +169,7 @@ jobs: - info steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v4.1.0 @@ -208,7 +208,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v4.1.0 id: python @@ -257,7 +257,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v4.1.0 id: python @@ -309,7 +309,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v4.1.0 id: python @@ -350,7 +350,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v4.1.0 id: python @@ -472,7 +472,7 @@ jobs: python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v4.1.0 @@ -535,7 +535,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v4.1.0 @@ -567,7 +567,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v4.1.0 @@ -600,7 +600,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v4.1.0 @@ -644,7 +644,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v4.1.0 @@ -692,7 +692,7 @@ jobs: name: Run pip check ${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v4.1.0 @@ -746,7 +746,7 @@ jobs: bluez \ ffmpeg - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v4.1.0 @@ -839,7 +839,7 @@ jobs: - pytest steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Download all coverage artifacts uses: actions/download-artifact@v3 - name: Upload coverage to Codecov (full coverage) diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index bc9fa63c86c..54864b9e0c0 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v4.1.0 @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v4.1.0 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 2ffc2f1f721..2bd808f0e8a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -22,7 +22,7 @@ jobs: architectures: ${{ steps.info.outputs.architectures }} steps: - name: Checkout the repository - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Get information id: info @@ -79,7 +79,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Download env_file uses: actions/download-artifact@v3 @@ -116,7 +116,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Download env_file uses: actions/download-artifact@v3 From 18033532caebbf4c9331cb7f9a29a535bc02ac01 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 5 Oct 2022 09:59:18 +0200 Subject: [PATCH 175/985] Fix search throwing on templated services (#79637) --- homeassistant/const.py | 1 + homeassistant/helpers/config_validation.py | 8 +++++-- homeassistant/helpers/script.py | 14 +++++------ homeassistant/helpers/service.py | 2 +- tests/components/search/test_init.py | 28 ++++++++++++++++++++++ 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ad81eb8c692..4f4da4925d0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -227,6 +227,7 @@ CONF_SENSOR_TYPE: Final = "sensor_type" CONF_SEQUENCE: Final = "sequence" CONF_SERVICE: Final = "service" CONF_SERVICE_DATA: Final = "data" +CONF_SERVICE_DATA_TEMPLATE: Final = "data_template" CONF_SERVICE_TEMPLATE: Final = "service_template" CONF_SHOW_ON_MAP: Final = "show_on_map" CONF_SLAVE: Final = "slave" diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 4c2fed60bb4..f6e77ef0018 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -63,6 +63,8 @@ from homeassistant.const import ( CONF_SCENE, CONF_SEQUENCE, CONF_SERVICE, + CONF_SERVICE_DATA, + CONF_SERVICE_DATA_TEMPLATE, CONF_SERVICE_TEMPLATE, CONF_STATE, CONF_STOP, @@ -1119,8 +1121,10 @@ SERVICE_SCHEMA = vol.All( vol.Exclusive(CONF_SERVICE_TEMPLATE, "service name"): vol.Any( service, dynamic_template ), - vol.Optional("data"): vol.Any(template, vol.All(dict, template_complex)), - vol.Optional("data_template"): vol.Any( + vol.Optional(CONF_SERVICE_DATA): vol.Any( + template, vol.All(dict, template_complex) + ), + vol.Optional(CONF_SERVICE_DATA_TEMPLATE): vol.Any( template, vol.All(dict, template_complex) ), vol.Optional(CONF_ENTITY_ID): comp_entity_ids, diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index e472934fc76..5fc0fdc4706 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -50,6 +50,7 @@ from homeassistant.const import ( CONF_SEQUENCE, CONF_SERVICE, CONF_SERVICE_DATA, + CONF_SERVICE_DATA_TEMPLATE, CONF_STOP, CONF_TARGET, CONF_THEN, @@ -1112,11 +1113,10 @@ async def _async_stop_scripts_at_shutdown(hass, event): _VarsType = Union[dict[str, Any], MappingProxyType] -def _referenced_extract_ids( - data: dict[str, Any] | None, key: str, found: set[str] -) -> None: +def _referenced_extract_ids(data: Any, key: str, found: set[str]) -> None: """Extract referenced IDs.""" - if not data: + # Data may not exist, or be a template + if not isinstance(data, dict): return item_ids = data.get(key) @@ -1300,7 +1300,7 @@ class Script: for data in ( step.get(CONF_TARGET), step.get(CONF_SERVICE_DATA), - step.get(service.CONF_SERVICE_DATA_TEMPLATE), + step.get(CONF_SERVICE_DATA_TEMPLATE), ): _referenced_extract_ids(data, ATTR_AREA_ID, referenced) @@ -1340,7 +1340,7 @@ class Script: for data in ( step.get(CONF_TARGET), step.get(CONF_SERVICE_DATA), - step.get(service.CONF_SERVICE_DATA_TEMPLATE), + step.get(CONF_SERVICE_DATA_TEMPLATE), ): _referenced_extract_ids(data, ATTR_DEVICE_ID, referenced) @@ -1391,7 +1391,7 @@ class Script: step, step.get(CONF_TARGET), step.get(CONF_SERVICE_DATA), - step.get(service.CONF_SERVICE_DATA_TEMPLATE), + step.get(CONF_SERVICE_DATA_TEMPLATE), ): _referenced_extract_ids(data, ATTR_ENTITY_ID, referenced) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 7675686844c..138fa739794 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_ENTITY_ID, CONF_SERVICE, CONF_SERVICE_DATA, + CONF_SERVICE_DATA_TEMPLATE, CONF_SERVICE_TEMPLATE, CONF_TARGET, ENTITY_MATCH_ALL, @@ -52,7 +53,6 @@ if TYPE_CHECKING: CONF_SERVICE_ENTITY_ID = "entity_id" -CONF_SERVICE_DATA_TEMPLATE = "data_template" _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/search/test_init.py b/tests/components/search/test_init.py index a728ef9b8c4..cc04680d8a4 100644 --- a/tests/components/search/test_init.py +++ b/tests/components/search/test_init.py @@ -183,6 +183,22 @@ async def test_search(hass): }, ] }, + "script_with_templated_services": { + "sequence": [ + { + "service": "test.script", + "target": "{{ {'entity_id':'test.test1'} }}", + }, + { + "service": "test.script", + "data": "{{ {'entity_id':'test.test2'} }}", + }, + { + "service": "test.script", + "data_template": "{{ {'entity_id':'test.test3'} }}", + }, + ] + }, } }, ) @@ -304,6 +320,18 @@ async def test_search(hass): searcher = search.Searcher(hass, device_reg, entity_reg, entity_sources) assert searcher.async_search(search_type, search_id) == {} + # Test search of templated script. We can't find referenced areas, devices or + # entities within templated services, but searching them should not raise or + # otherwise fail. + assert hass.states.get("script.script_with_templated_services") + for search_type, search_id in ( + ("area", "script.script_with_templated_services"), + ("device", "script.script_with_templated_services"), + ("entity", "script.script_with_templated_services"), + ): + searcher = search.Searcher(hass, device_reg, entity_reg, entity_sources) + assert searcher.async_search(search_type, search_id) == {} + searcher = search.Searcher(hass, device_reg, entity_reg, entity_sources) assert searcher.async_search("entity", "light.wled_config_entry_source") == { "config_entry": {wled_config_entry.entry_id}, From 33bdc67a61f3be8bb10236c6dbf5742fda496830 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 5 Oct 2022 04:17:13 -0400 Subject: [PATCH 176/985] Remove superfluous strings from Lidarr (#79631) --- homeassistant/components/lidarr/strings.json | 10 ---------- homeassistant/components/lidarr/translations/en.json | 10 ---------- 2 files changed, 20 deletions(-) diff --git a/homeassistant/components/lidarr/strings.json b/homeassistant/components/lidarr/strings.json index 662d930cbef..ffa91c23f2a 100644 --- a/homeassistant/components/lidarr/strings.json +++ b/homeassistant/components/lidarr/strings.json @@ -28,15 +28,5 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } - }, - "options": { - "step": { - "init": { - "data": { - "upcoming_days": "Number of upcoming days to display on calendar", - "max_records": "Number of maximum records to display on wanted and queue" - } - } - } } } diff --git a/homeassistant/components/lidarr/translations/en.json b/homeassistant/components/lidarr/translations/en.json index 0e0475d25cd..cdb21be7fb2 100644 --- a/homeassistant/components/lidarr/translations/en.json +++ b/homeassistant/components/lidarr/translations/en.json @@ -28,15 +28,5 @@ "description": "API key can be retrieved automatically if login credentials were not set in application.\nYour API key can be found in Settings > General in the Lidarr Web UI." } } - }, - "options": { - "step": { - "init": { - "data": { - "max_records": "Number of maximum records to display on wanted and queue", - "upcoming_days": "Number of upcoming days to display on calendar" - } - } - } } } \ No newline at end of file From 9dd9147343acdf8b3707257de45053af1ec194e3 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 5 Oct 2022 08:24:52 +0000 Subject: [PATCH 177/985] Use HA `uuid` as `client_id` in BraviaTV (#79618) * Use uuid as clientid/nickname * Fixes after rebase * Move gen_instance_ids() to utils * Store client_id and nickname in config_entry * Update tests * Clean names * Rename consts --- homeassistant/components/braviatv/__init__.py | 9 +++---- .../components/braviatv/config_flow.py | 27 ++++++++++++++----- homeassistant/components/braviatv/const.py | 6 +++-- .../components/braviatv/coordinator.py | 22 ++++++++++----- tests/components/braviatv/test_config_flow.py | 15 +++++++++++ 5 files changed, 59 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index d06482e5c71..321d864f036 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -7,11 +7,11 @@ from aiohttp import CookieJar from pybravia import BraviaTV from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN, Platform +from homeassistant.const import CONF_HOST, CONF_MAC, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_create_clientsession -from .const import CONF_IGNORED_SOURCES, CONF_USE_PSK, DOMAIN +from .const import CONF_IGNORED_SOURCES, DOMAIN from .coordinator import BraviaTVCoordinator PLATFORMS: Final[list[Platform]] = [ @@ -25,8 +25,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b """Set up a config entry.""" host = config_entry.data[CONF_HOST] mac = config_entry.data[CONF_MAC] - pin = config_entry.data[CONF_PIN] - use_psk = config_entry.data.get(CONF_USE_PSK, False) ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES, []) session = async_create_clientsession( @@ -36,8 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b coordinator = BraviaTVCoordinator( hass=hass, client=client, - pin=pin, - use_psk=use_psk, + config=config_entry.data, ignored_sources=ignored_sources, ) config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index dbd08809703..f5c7826b825 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -15,6 +15,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PIN from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import instance_id from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.util.network import is_host_valid @@ -24,11 +25,12 @@ from .const import ( ATTR_CID, ATTR_MAC, ATTR_MODEL, - CLIENTID_PREFIX, + CONF_CLIENT_ID, CONF_IGNORED_SOURCES, + CONF_NICKNAME, CONF_USE_PSK, DOMAIN, - NICKNAME, + NICKNAME_PREFIX, ) @@ -42,6 +44,8 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.client: BraviaTV | None = None self.device_config: dict[str, Any] = {} self.entry: ConfigEntry | None = None + self.client_id: str = "" + self.nickname: str = "" @staticmethod @callback @@ -68,8 +72,10 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if use_psk: await self.client.connect(psk=pin) else: + self.device_config[CONF_CLIENT_ID] = self.client_id + self.device_config[CONF_NICKNAME] = self.nickname await self.client.connect( - pin=pin, clientid=CLIENTID_PREFIX, nickname=NICKNAME + pin=pin, clientid=self.client_id, nickname=self.nickname ) await self.client.set_wol_mode(True) @@ -110,6 +116,7 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Authorize Bravia TV device.""" errors: dict[str, str] = {} + self.client_id, self.nickname = await self.gen_instance_ids() if user_input is not None: self.device_config[CONF_PIN] = user_input[CONF_PIN] @@ -126,7 +133,7 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): assert self.client try: - await self.client.pair(CLIENTID_PREFIX, NICKNAME) + await self.client.pair(self.client_id, self.nickname) except BraviaTVError: return self.async_abort(reason="no_ip_control") @@ -190,6 +197,7 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Dialog that informs the user that reauth is required.""" self.create_client() + client_id, nickname = await self.gen_instance_ids() assert self.client is not None assert self.entry is not None @@ -201,8 +209,10 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if use_psk: await self.client.connect(psk=pin) else: + self.device_config[CONF_CLIENT_ID] = client_id + self.device_config[CONF_NICKNAME] = nickname await self.client.connect( - pin=pin, clientid=CLIENTID_PREFIX, nickname=NICKNAME + pin=pin, clientid=client_id, nickname=nickname ) await self.client.set_wol_mode(True) except BraviaTVError: @@ -215,7 +225,7 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="reauth_successful") try: - await self.client.pair(CLIENTID_PREFIX, NICKNAME) + await self.client.pair(client_id, nickname) except BraviaTVError: return self.async_abort(reason="reauth_unsuccessful") @@ -229,6 +239,11 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ), ) + async def gen_instance_ids(self) -> tuple[str, str]: + """Generate client_id and nickname.""" + uuid = await instance_id.async_get(self.hass) + return uuid, f"{NICKNAME_PREFIX} {uuid[:6]}" + class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow): """Config flow options for Bravia TV.""" diff --git a/homeassistant/components/braviatv/const.py b/homeassistant/components/braviatv/const.py index 8855499914c..e7bdf00d507 100644 --- a/homeassistant/components/braviatv/const.py +++ b/homeassistant/components/braviatv/const.py @@ -8,9 +8,11 @@ ATTR_MAC: Final = "macAddr" ATTR_MANUFACTURER: Final = "Sony" ATTR_MODEL: Final = "model" +CONF_CLIENT_ID: Final = "client_id" CONF_IGNORED_SOURCES: Final = "ignored_sources" +CONF_NICKNAME: Final = "nickname" CONF_USE_PSK: Final = "use_psk" -CLIENTID_PREFIX: Final = "HomeAssistant" DOMAIN: Final = "braviatv" -NICKNAME: Final = "Home Assistant" +LEGACY_CLIENT_ID: Final = "HomeAssistant" +NICKNAME_PREFIX: Final = "Home Assistant" diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index 46dd39bf470..1262e7bf7cc 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -5,6 +5,7 @@ from collections.abc import Awaitable, Callable, Coroutine, Iterable from datetime import timedelta from functools import wraps import logging +from types import MappingProxyType from typing import Any, Final, TypeVar from pybravia import ( @@ -19,12 +20,20 @@ from pybravia import ( from typing_extensions import Concatenate, ParamSpec from homeassistant.components.media_player import MediaType +from homeassistant.const import CONF_PIN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CLIENTID_PREFIX, DOMAIN, NICKNAME +from .const import ( + CONF_CLIENT_ID, + CONF_NICKNAME, + CONF_USE_PSK, + DOMAIN, + LEGACY_CLIENT_ID, + NICKNAME_PREFIX, +) _BraviaTVCoordinatorT = TypeVar("_BraviaTVCoordinatorT", bound="BraviaTVCoordinator") _P = ParamSpec("_P") @@ -61,15 +70,16 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self, hass: HomeAssistant, client: BraviaTV, - pin: str, - use_psk: bool, + config: MappingProxyType[str, Any], ignored_sources: list[str], ) -> None: """Initialize Bravia TV Client.""" self.client = client - self.pin = pin - self.use_psk = use_psk + self.pin = config[CONF_PIN] + self.use_psk = config.get(CONF_USE_PSK, False) + self.client_id = config.get(CONF_CLIENT_ID, LEGACY_CLIENT_ID) + self.nickname = config.get(CONF_NICKNAME, NICKNAME_PREFIX) self.ignored_sources = ignored_sources self.source: str | None = None self.source_list: list[str] = [] @@ -119,7 +129,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): await self.client.connect(psk=self.pin) else: await self.client.connect( - pin=self.pin, clientid=CLIENTID_PREFIX, nickname=NICKNAME + pin=self.pin, clientid=self.client_id, nickname=self.nickname ) self.connected = True diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index cc70d685b78..58e684a1378 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -12,12 +12,16 @@ import pytest from homeassistant import data_entry_flow from homeassistant.components import ssdp from homeassistant.components.braviatv.const import ( + CONF_CLIENT_ID, CONF_IGNORED_SOURCES, + CONF_NICKNAME, CONF_USE_PSK, DOMAIN, + NICKNAME_PREFIX, ) from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN +from homeassistant.helpers import instance_id from tests.common import MockConfigEntry @@ -93,6 +97,7 @@ async def test_show_form(hass): async def test_ssdp_discovery(hass): """Test that the device is discovered.""" + uuid = await instance_id.async_get(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, @@ -129,6 +134,8 @@ async def test_ssdp_discovery(hass): CONF_PIN: "1234", CONF_USE_PSK: False, CONF_MAC: "AA:BB:CC:DD:EE:FF", + CONF_CLIENT_ID: uuid, + CONF_NICKNAME: f"{NICKNAME_PREFIX} {uuid[:6]}", } @@ -270,6 +277,8 @@ async def test_duplicate_error(hass): async def test_create_entry(hass): """Test that the user step works.""" + uuid = await instance_id.async_get(hass) + with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( "pybravia.BraviaTV.set_wol_mode" ), patch( @@ -297,11 +306,15 @@ async def test_create_entry(hass): CONF_PIN: "1234", CONF_USE_PSK: False, CONF_MAC: "AA:BB:CC:DD:EE:FF", + CONF_CLIENT_ID: uuid, + CONF_NICKNAME: f"{NICKNAME_PREFIX} {uuid[:6]}", } async def test_create_entry_with_ipv6_address(hass): """Test that the user step works with device IPv6 address.""" + uuid = await instance_id.async_get(hass) + with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( "pybravia.BraviaTV.set_wol_mode" ), patch( @@ -331,6 +344,8 @@ async def test_create_entry_with_ipv6_address(hass): CONF_PIN: "1234", CONF_USE_PSK: False, CONF_MAC: "AA:BB:CC:DD:EE:FF", + CONF_CLIENT_ID: uuid, + CONF_NICKNAME: f"{NICKNAME_PREFIX} {uuid[:6]}", } From 723b018b631a4c5ab9324c487daca5b23ac4d913 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 5 Oct 2022 12:36:06 +0300 Subject: [PATCH 178/985] Refactor Shelly tests - do not access hass.data (#79606) --- tests/components/shelly/__init__.py | 25 +++ tests/components/shelly/conftest.py | 131 +++++---------- tests/components/shelly/test_button.py | 55 ++----- tests/components/shelly/test_cover.py | 110 ++++--------- .../components/shelly/test_device_trigger.py | 154 +++++++----------- tests/components/shelly/test_diagnostics.py | 21 +-- tests/components/shelly/test_logbook.py | 24 ++- tests/components/shelly/test_switch.py | 90 +++------- tests/components/shelly/test_update.py | 62 ++----- 9 files changed, 245 insertions(+), 427 deletions(-) diff --git a/tests/components/shelly/__init__.py b/tests/components/shelly/__init__.py index 3c502c81deb..326e62432d3 100644 --- a/tests/components/shelly/__init__.py +++ b/tests/components/shelly/__init__.py @@ -1 +1,26 @@ """Tests for the Shelly integration.""" +from homeassistant.components.shelly.const import CONF_SLEEP_PERIOD, DOMAIN +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def init_integration( + hass: HomeAssistant, gen: int, model="SHSW-25" +) -> MockConfigEntry: + """Set up the Shelly integration in Home Assistant.""" + data = { + CONF_HOST: "192.168.1.37", + CONF_SLEEP_PERIOD: 0, + "model": model, + "gen": gen, + } + + entry = MockConfigEntry(domain=DOMAIN, data=data, unique_id=DOMAIN) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 49e86e118e3..8890201ad6d 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -3,30 +3,12 @@ from unittest.mock import AsyncMock, Mock, patch import pytest -from homeassistant.components.shelly import ( - BlockDeviceWrapper, - RpcDeviceWrapper, - RpcPollingWrapper, - ShellyDeviceRestWrapper, -) from homeassistant.components.shelly.const import ( - BLOCK, - DATA_CONFIG_ENTRY, - DOMAIN, EVENT_SHELLY_CLICK, - REST, REST_SENSORS_UPDATE_INTERVAL, - RPC, - RPC_POLL, ) -from homeassistant.setup import async_setup_component -from tests.common import ( - MockConfigEntry, - async_capture_events, - async_mock_service, - mock_device_registry, -) +from tests.common import async_capture_events, async_mock_service, mock_device_registry MOCK_SETTINGS = { "name": "Test name", @@ -144,80 +126,57 @@ def events(hass): @pytest.fixture -async def coap_wrapper(hass): - """Setups a coap wrapper with mocked device.""" - await async_setup_component(hass, "shelly", {}) +async def mock_block_device(): + """Mock block (Gen1, CoAP) device.""" + with patch("homeassistant.components.shelly.utils.COAP", autospec=True), patch( + "aioshelly.block_device.BlockDevice.create" + ) as block_device_mock: - config_entry = MockConfigEntry( - domain=DOMAIN, - data={"sleep_period": 0, "model": "SHSW-25", "host": "1.2.3.4"}, - unique_id="12345678", - ) - config_entry.add_to_hass(hass) + def update(): + block_device_mock.return_value.subscribe_updates.call_args[0][0]({}) - device = Mock( - blocks=MOCK_BLOCKS, - settings=MOCK_SETTINGS, - shelly=MOCK_SHELLY_COAP, - status=MOCK_STATUS_COAP, - firmware_version="some fw string", - update=AsyncMock(), - update_status=AsyncMock(), - trigger_ota_update=AsyncMock(), - trigger_reboot=AsyncMock(), - initialized=True, - ) + device = Mock( + blocks=MOCK_BLOCKS, + settings=MOCK_SETTINGS, + shelly=MOCK_SHELLY_COAP, + status=MOCK_STATUS_COAP, + firmware_version="some fw string", + update=AsyncMock(), + update_status=AsyncMock(), + trigger_ota_update=AsyncMock(), + trigger_reboot=AsyncMock(), + initialize=AsyncMock(), + initialized=True, + ) + block_device_mock.return_value = device + block_device_mock.return_value.mock_update = Mock(side_effect=update) - hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} - hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] = {} - hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][ - REST - ] = ShellyDeviceRestWrapper(hass, device, config_entry) - - wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][ - BLOCK - ] = BlockDeviceWrapper(hass, config_entry, device) - - wrapper.async_setup() - - return wrapper + yield block_device_mock.return_value @pytest.fixture -async def rpc_wrapper(hass): - """Setups a rpc wrapper with mocked device.""" - await async_setup_component(hass, "shelly", {}) +async def mock_rpc_device(): + """Mock rpc (Gen2, Websocket) device.""" + with patch("aioshelly.rpc_device.RpcDevice.create") as rpc_device_mock: - config_entry = MockConfigEntry( - domain=DOMAIN, - data={"sleep_period": 0, "model": "SNSW-001P16EU", "gen": 2, "host": "1.2.3.4"}, - unique_id="12345678", - ) - config_entry.add_to_hass(hass) + def update(): + rpc_device_mock.return_value.subscribe_updates.call_args[0][0]({}) - device = Mock( - call_rpc=AsyncMock(), - config=MOCK_CONFIG, - event={}, - shelly=MOCK_SHELLY_RPC, - status=MOCK_STATUS_RPC, - firmware_version="some fw string", - update=AsyncMock(), - trigger_ota_update=AsyncMock(), - trigger_reboot=AsyncMock(), - initialized=True, - shutdown=AsyncMock(), - ) + device = Mock( + call_rpc=AsyncMock(), + config=MOCK_CONFIG, + event={}, + shelly=MOCK_SHELLY_RPC, + status=MOCK_STATUS_RPC, + firmware_version="some fw string", + update=AsyncMock(), + trigger_ota_update=AsyncMock(), + trigger_reboot=AsyncMock(), + initialized=True, + shutdown=AsyncMock(), + ) - hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} - hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] = {} - hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][ - RPC_POLL - ] = RpcPollingWrapper(hass, config_entry, device) + rpc_device_mock.return_value = device + rpc_device_mock.return_value.mock_update = Mock(side_effect=update) - wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][ - RPC - ] = RpcDeviceWrapper(hass, config_entry, device) - wrapper.async_setup() - - return wrapper + yield rpc_device_mock.return_value diff --git a/tests/components/shelly/test_button.py b/tests/components/shelly/test_button.py index 8bbae677fb6..bd20be7c645 100644 --- a/tests/components/shelly/test_button.py +++ b/tests/components/shelly/test_button.py @@ -1,33 +1,17 @@ """Tests for Shelly button platform.""" from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS -from homeassistant.components.shelly.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import async_get + +from . import init_integration -async def test_block_button(hass: HomeAssistant, coap_wrapper): +async def test_block_button(hass: HomeAssistant, mock_block_device): """Test block device reboot button.""" - assert coap_wrapper - - entity_registry = async_get(hass) - entity_registry.async_get_or_create( - BUTTON_DOMAIN, - DOMAIN, - "test_name_reboot", - suggested_object_id="test_name_reboot", - disabled_by=None, - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, BUTTON_DOMAIN) - ) - await hass.async_block_till_done() + await init_integration(hass, 1) # reboot button - state = hass.states.get("button.test_name_reboot") - - assert state - assert state.state == STATE_UNKNOWN + assert hass.states.get("button.test_name_reboot").state == STATE_UNKNOWN await hass.services.async_call( BUTTON_DOMAIN, @@ -35,33 +19,15 @@ async def test_block_button(hass: HomeAssistant, coap_wrapper): {ATTR_ENTITY_ID: "button.test_name_reboot"}, blocking=True, ) - await hass.async_block_till_done() - assert coap_wrapper.device.trigger_reboot.call_count == 1 + assert mock_block_device.trigger_reboot.call_count == 1 -async def test_rpc_button(hass: HomeAssistant, rpc_wrapper): +async def test_rpc_button(hass: HomeAssistant, mock_rpc_device): """Test rpc device OTA button.""" - assert rpc_wrapper - - entity_registry = async_get(hass) - entity_registry.async_get_or_create( - BUTTON_DOMAIN, - DOMAIN, - "test_name_reboot", - suggested_object_id="test_name_reboot", - disabled_by=None, - ) - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, BUTTON_DOMAIN) - ) - await hass.async_block_till_done() + await init_integration(hass, 2) # reboot button - state = hass.states.get("button.test_name_reboot") - - assert state - assert state.state == STATE_UNKNOWN + assert hass.states.get("button.test_name_reboot").state == STATE_UNKNOWN await hass.services.async_call( BUTTON_DOMAIN, @@ -69,5 +35,4 @@ async def test_rpc_button(hass: HomeAssistant, rpc_wrapper): {ATTR_ENTITY_ID: "button.test_name_reboot"}, blocking=True, ) - await hass.async_block_till_done() - assert rpc_wrapper.device.trigger_reboot.call_count == 1 + assert mock_rpc_device.trigger_reboot.call_count == 1 diff --git a/tests/components/shelly/test_cover.py b/tests/components/shelly/test_cover.py index ab8c9a9a876..3a032a9de20 100644 --- a/tests/components/shelly/test_cover.py +++ b/tests/components/shelly/test_cover.py @@ -13,20 +13,16 @@ from homeassistant.components.cover import ( STATE_OPENING, ) from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.helpers.entity_component import async_update_entity + +from . import init_integration ROLLER_BLOCK_ID = 1 -async def test_block_device_services(hass, coap_wrapper, monkeypatch): +async def test_block_device_services(hass, mock_block_device, monkeypatch): """Test block device cover services.""" - assert coap_wrapper - - monkeypatch.setitem(coap_wrapper.device.settings, "mode", "roller") - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, COVER_DOMAIN) - ) - await hass.async_block_till_done() + monkeypatch.setitem(mock_block_device.settings, "mode", "roller") + await init_integration(hass, 1) await hass.services.async_call( COVER_DOMAIN, @@ -62,46 +58,28 @@ async def test_block_device_services(hass, coap_wrapper, monkeypatch): assert hass.states.get("cover.test_name").state == STATE_CLOSED -async def test_block_device_update(hass, coap_wrapper, monkeypatch): +async def test_block_device_update(hass, mock_block_device, monkeypatch): """Test block device update.""" - assert coap_wrapper + monkeypatch.setattr(mock_block_device.blocks[ROLLER_BLOCK_ID], "rollerPos", 0) + await init_integration(hass, 1) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, COVER_DOMAIN) - ) - await hass.async_block_till_done() - - monkeypatch.setattr(coap_wrapper.device.blocks[ROLLER_BLOCK_ID], "rollerPos", 0) - await async_update_entity(hass, "cover.test_name") - await hass.async_block_till_done() assert hass.states.get("cover.test_name").state == STATE_CLOSED - monkeypatch.setattr(coap_wrapper.device.blocks[ROLLER_BLOCK_ID], "rollerPos", 100) - await async_update_entity(hass, "cover.test_name") - await hass.async_block_till_done() + monkeypatch.setattr(mock_block_device.blocks[ROLLER_BLOCK_ID], "rollerPos", 100) + mock_block_device.mock_update() assert hass.states.get("cover.test_name").state == STATE_OPEN -async def test_block_device_no_roller_blocks(hass, coap_wrapper, monkeypatch): +async def test_block_device_no_roller_blocks(hass, mock_block_device, monkeypatch): """Test block device without roller blocks.""" - assert coap_wrapper - - monkeypatch.setattr(coap_wrapper.device.blocks[ROLLER_BLOCK_ID], "type", None) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, COVER_DOMAIN) - ) - await hass.async_block_till_done() + monkeypatch.setattr(mock_block_device.blocks[ROLLER_BLOCK_ID], "type", None) + await init_integration(hass, 1) assert hass.states.get("cover.test_name") is None -async def test_rpc_device_services(hass, rpc_wrapper, monkeypatch): +async def test_rpc_device_services(hass, mock_rpc_device, monkeypatch): """Test RPC device cover services.""" - assert rpc_wrapper - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, COVER_DOMAIN) - ) - await hass.async_block_till_done() + await init_integration(hass, 2) await hass.services.async_call( COVER_DOMAIN, @@ -112,81 +90,57 @@ async def test_rpc_device_services(hass, rpc_wrapper, monkeypatch): state = hass.states.get("cover.test_cover_0") assert state.attributes[ATTR_CURRENT_POSITION] == 50 - monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "state", "opening") + monkeypatch.setitem(mock_rpc_device.status["cover:0"], "state", "opening") await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: "cover.test_cover_0"}, blocking=True, ) - rpc_wrapper.async_set_updated_data("") + mock_rpc_device.mock_update() assert hass.states.get("cover.test_cover_0").state == STATE_OPENING - monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "state", "closing") + monkeypatch.setitem(mock_rpc_device.status["cover:0"], "state", "closing") await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: "cover.test_cover_0"}, blocking=True, ) - rpc_wrapper.async_set_updated_data("") + mock_rpc_device.mock_update() assert hass.states.get("cover.test_cover_0").state == STATE_CLOSING - monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "state", "closed") + monkeypatch.setitem(mock_rpc_device.status["cover:0"], "state", "closed") await hass.services.async_call( COVER_DOMAIN, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: "cover.test_cover_0"}, blocking=True, ) - rpc_wrapper.async_set_updated_data("") + mock_rpc_device.mock_update() assert hass.states.get("cover.test_cover_0").state == STATE_CLOSED -async def test_rpc_device_no_cover_keys(hass, rpc_wrapper, monkeypatch): +async def test_rpc_device_no_cover_keys(hass, mock_rpc_device, monkeypatch): """Test RPC device without cover keys.""" - assert rpc_wrapper - - monkeypatch.delitem(rpc_wrapper.device.status, "cover:0") - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, COVER_DOMAIN) - ) - await hass.async_block_till_done() + monkeypatch.delitem(mock_rpc_device.status, "cover:0") + await init_integration(hass, 2) assert hass.states.get("cover.test_cover_0") is None -async def test_rpc_device_update(hass, rpc_wrapper, monkeypatch): +async def test_rpc_device_update(hass, mock_rpc_device, monkeypatch): """Test RPC device update.""" - assert rpc_wrapper - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, COVER_DOMAIN) - ) - await hass.async_block_till_done() - - monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "state", "closed") - await async_update_entity(hass, "cover.test_cover_0") - await hass.async_block_till_done() + monkeypatch.setitem(mock_rpc_device.status["cover:0"], "state", "closed") + await init_integration(hass, 2) assert hass.states.get("cover.test_cover_0").state == STATE_CLOSED - monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "state", "open") - await async_update_entity(hass, "cover.test_cover_0") - await hass.async_block_till_done() + monkeypatch.setitem(mock_rpc_device.status["cover:0"], "state", "open") + mock_rpc_device.mock_update() assert hass.states.get("cover.test_cover_0").state == STATE_OPEN -async def test_rpc_device_no_position_control(hass, rpc_wrapper, monkeypatch): +async def test_rpc_device_no_position_control(hass, mock_rpc_device, monkeypatch): """Test RPC device with no position control.""" - assert rpc_wrapper - - monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "pos_control", False) - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, COVER_DOMAIN) - ) - await hass.async_block_till_done() - - await async_update_entity(hass, "cover.test_cover_0") - await hass.async_block_till_done() + monkeypatch.setitem(mock_rpc_device.status["cover:0"], "pos_control", False) + await init_integration(hass, 2) assert hass.states.get("cover.test_cover_0").state == STATE_OPEN diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index b638032e96e..d5881696bf6 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -1,6 +1,4 @@ """The tests for Shelly device triggers.""" -from unittest.mock import AsyncMock, Mock - import pytest from homeassistant.components import automation @@ -8,20 +6,23 @@ from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) -from homeassistant.components.shelly import BlockDeviceWrapper from homeassistant.components.shelly.const import ( ATTR_CHANNEL, ATTR_CLICK_TYPE, - BLOCK, CONF_SUBTYPE, - DATA_CONFIG_ENTRY, DOMAIN, EVENT_SHELLY_CLICK, ) from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.helpers import device_registry +from homeassistant.helpers.device_registry import ( + async_entries_for_config_entry, + async_get as async_get_dev_reg, +) from homeassistant.setup import async_setup_component +from . import init_integration + from tests.common import ( MockConfigEntry, assert_lists_same, @@ -39,48 +40,52 @@ from tests.common import ( ], ) async def test_get_triggers_block_device( - hass, coap_wrapper, monkeypatch, button_type, is_valid + hass, mock_block_device, monkeypatch, button_type, is_valid ): """Test we get the expected triggers from a shelly block device.""" - assert coap_wrapper - monkeypatch.setitem( - coap_wrapper.device.settings, + mock_block_device.settings, "relays", [ {"btn_type": button_type}, {"btn_type": "toggle"}, ], ) + entry = await init_integration(hass, 1) + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] expected_triggers = [] if is_valid: expected_triggers = [ { CONF_PLATFORM: "device", - CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_DEVICE_ID: device.id, CONF_DOMAIN: DOMAIN, - CONF_TYPE: type, + CONF_TYPE: type_, CONF_SUBTYPE: "button1", "metadata": {}, } - for type in ["single", "long"] + for type_ in ["single", "long"] ] triggers = await async_get_device_automations( - hass, DeviceAutomationType.TRIGGER, coap_wrapper.device_id + hass, DeviceAutomationType.TRIGGER, device.id ) - + triggers = [value for value in triggers if value["domain"] == DOMAIN] assert_lists_same(triggers, expected_triggers) -async def test_get_triggers_rpc_device(hass, rpc_wrapper): +async def test_get_triggers_rpc_device(hass, mock_rpc_device): """Test we get the expected triggers from a shelly RPC device.""" - assert rpc_wrapper + entry = await init_integration(hass, 2) + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + expected_triggers = [ { CONF_PLATFORM: "device", - CONF_DEVICE_ID: rpc_wrapper.device_id, + CONF_DEVICE_ID: device.id, CONF_DOMAIN: DOMAIN, CONF_TYPE: type, CONF_SUBTYPE: "button1", @@ -90,43 +95,22 @@ async def test_get_triggers_rpc_device(hass, rpc_wrapper): ] triggers = await async_get_device_automations( - hass, DeviceAutomationType.TRIGGER, rpc_wrapper.device_id + hass, DeviceAutomationType.TRIGGER, device.id ) - + triggers = [value for value in triggers if value["domain"] == DOMAIN] assert_lists_same(triggers, expected_triggers) -async def test_get_triggers_button(hass): +async def test_get_triggers_button(hass, mock_block_device): """Test we get the expected triggers from a shelly button.""" - await async_setup_component(hass, "shelly", {}) - - config_entry = MockConfigEntry( - domain=DOMAIN, - data={"sleep_period": 43200, "model": "SHBTN-1", "host": "1.2.3.4"}, - unique_id="12345678", - ) - config_entry.add_to_hass(hass) - - device = Mock( - blocks=None, - settings=None, - shelly=None, - update=AsyncMock(), - initialized=False, - ) - - hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} - hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] = {} - coap_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][ - BLOCK - ] = BlockDeviceWrapper(hass, config_entry, device) - - coap_wrapper.async_setup() + entry = await init_integration(hass, 1, model="SHBTN-1") + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] expected_triggers = [ { CONF_PLATFORM: "device", - CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_DEVICE_ID: device.id, CONF_DOMAIN: DOMAIN, CONF_TYPE: type, CONF_SUBTYPE: "button", @@ -136,51 +120,33 @@ async def test_get_triggers_button(hass): ] triggers = await async_get_device_automations( - hass, DeviceAutomationType.TRIGGER, coap_wrapper.device_id + hass, DeviceAutomationType.TRIGGER, device.id ) - + triggers = [value for value in triggers if value["domain"] == DOMAIN] assert_lists_same(triggers, expected_triggers) -async def test_get_triggers_non_initialized_devices(hass): +async def test_get_triggers_non_initialized_devices( + hass, mock_block_device, monkeypatch +): """Test we get the empty triggers for non-initialized devices.""" - await async_setup_component(hass, "shelly", {}) - - config_entry = MockConfigEntry( - domain=DOMAIN, - data={"sleep_period": 43200, "model": "SHDW-2", "host": "1.2.3.4"}, - unique_id="12345678", - ) - config_entry.add_to_hass(hass) - - device = Mock( - blocks=None, - settings=None, - shelly=None, - update=AsyncMock(), - initialized=False, - ) - - hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} - hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] = {} - coap_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][ - BLOCK - ] = BlockDeviceWrapper(hass, config_entry, device) - - coap_wrapper.async_setup() + monkeypatch.setattr(mock_block_device, "initialized", False) + entry = await init_integration(hass, 1) + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] expected_triggers = [] triggers = await async_get_device_automations( - hass, DeviceAutomationType.TRIGGER, coap_wrapper.device_id + hass, DeviceAutomationType.TRIGGER, device.id ) - + triggers = [value for value in triggers if value["domain"] == DOMAIN] assert_lists_same(triggers, expected_triggers) -async def test_get_triggers_for_invalid_device_id(hass, device_reg, coap_wrapper): +async def test_get_triggers_for_invalid_device_id(hass, device_reg, mock_block_device): """Test error raised for invalid shelly device_id.""" - assert coap_wrapper + await init_integration(hass, 1) config_entry = MockConfigEntry(domain=DOMAIN, data={}) config_entry.add_to_hass(hass) invalid_device = device_reg.async_get_or_create( @@ -194,9 +160,11 @@ async def test_get_triggers_for_invalid_device_id(hass, device_reg, coap_wrapper ) -async def test_if_fires_on_click_event_block_device(hass, calls, coap_wrapper): +async def test_if_fires_on_click_event_block_device(hass, calls, mock_block_device): """Test for click_event trigger firing for block device.""" - assert coap_wrapper + entry = await init_integration(hass, 1) + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] assert await async_setup_component( hass, @@ -207,7 +175,7 @@ async def test_if_fires_on_click_event_block_device(hass, calls, coap_wrapper): "trigger": { CONF_PLATFORM: "device", CONF_DOMAIN: DOMAIN, - CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_DEVICE_ID: device.id, CONF_TYPE: "single", CONF_SUBTYPE: "button1", }, @@ -221,7 +189,7 @@ async def test_if_fires_on_click_event_block_device(hass, calls, coap_wrapper): ) message = { - CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_DEVICE_ID: device.id, ATTR_CLICK_TYPE: "single", ATTR_CHANNEL: 1, } @@ -232,9 +200,11 @@ async def test_if_fires_on_click_event_block_device(hass, calls, coap_wrapper): assert calls[0].data["some"] == "test_trigger_single_click" -async def test_if_fires_on_click_event_rpc_device(hass, calls, rpc_wrapper): +async def test_if_fires_on_click_event_rpc_device(hass, calls, mock_rpc_device): """Test for click_event trigger firing for rpc device.""" - assert rpc_wrapper + entry = await init_integration(hass, 2) + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] assert await async_setup_component( hass, @@ -245,7 +215,7 @@ async def test_if_fires_on_click_event_rpc_device(hass, calls, rpc_wrapper): "trigger": { CONF_PLATFORM: "device", CONF_DOMAIN: DOMAIN, - CONF_DEVICE_ID: rpc_wrapper.device_id, + CONF_DEVICE_ID: device.id, CONF_TYPE: "single_push", CONF_SUBTYPE: "button1", }, @@ -259,7 +229,7 @@ async def test_if_fires_on_click_event_rpc_device(hass, calls, rpc_wrapper): ) message = { - CONF_DEVICE_ID: rpc_wrapper.device_id, + CONF_DEVICE_ID: device.id, ATTR_CLICK_TYPE: "single_push", ATTR_CHANNEL: 1, } @@ -270,9 +240,9 @@ async def test_if_fires_on_click_event_rpc_device(hass, calls, rpc_wrapper): assert calls[0].data["some"] == "test_trigger_single_push" -async def test_validate_trigger_block_device_not_ready(hass, calls, coap_wrapper): +async def test_validate_trigger_block_device_not_ready(hass, calls, mock_block_device): """Test validate trigger config when block device is not ready.""" - assert coap_wrapper + await init_integration(hass, 1) assert await async_setup_component( hass, @@ -307,10 +277,8 @@ async def test_validate_trigger_block_device_not_ready(hass, calls, coap_wrapper assert calls[0].data["some"] == "test_trigger_single_click" -async def test_validate_trigger_rpc_device_not_ready(hass, calls, rpc_wrapper): +async def test_validate_trigger_rpc_device_not_ready(hass, calls, mock_rpc_device): """Test validate trigger config when RPC device is not ready.""" - assert rpc_wrapper - assert await async_setup_component( hass, automation.DOMAIN, @@ -344,9 +312,11 @@ async def test_validate_trigger_rpc_device_not_ready(hass, calls, rpc_wrapper): assert calls[0].data["some"] == "test_trigger_single_push" -async def test_validate_trigger_invalid_triggers(hass, coap_wrapper): +async def test_validate_trigger_invalid_triggers(hass, mock_block_device): """Test for click_event with invalid triggers.""" - assert coap_wrapper + entry = await init_integration(hass, 1) + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] assert await async_setup_component( hass, @@ -357,7 +327,7 @@ async def test_validate_trigger_invalid_triggers(hass, coap_wrapper): "trigger": { CONF_PLATFORM: "device", CONF_DOMAIN: DOMAIN, - CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_DEVICE_ID: device.id, CONF_TYPE: "single", CONF_SUBTYPE: "button3", }, diff --git a/tests/components/shelly/test_diagnostics.py b/tests/components/shelly/test_diagnostics.py index 137149f1608..93d56027fab 100644 --- a/tests/components/shelly/test_diagnostics.py +++ b/tests/components/shelly/test_diagnostics.py @@ -6,6 +6,7 @@ from homeassistant.components.shelly.const import DOMAIN from homeassistant.components.shelly.diagnostics import TO_REDACT from homeassistant.core import HomeAssistant +from . import init_integration from .conftest import MOCK_STATUS_COAP from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -14,10 +15,10 @@ RELAY_BLOCK_ID = 0 async def test_block_config_entry_diagnostics( - hass: HomeAssistant, hass_client: ClientSession, coap_wrapper + hass: HomeAssistant, hass_client: ClientSession, mock_block_device ): """Test config entry diagnostics for block device.""" - assert coap_wrapper + await init_integration(hass, 1) entry = hass.config_entries.async_entries(DOMAIN)[0] entry_dict = entry.as_dict() @@ -30,9 +31,9 @@ async def test_block_config_entry_diagnostics( assert result == { "entry": entry_dict, "device_info": { - "name": coap_wrapper.name, - "model": coap_wrapper.model, - "sw_version": coap_wrapper.sw_version, + "name": "Test name", + "model": "SHSW-25", + "sw_version": "some fw string", }, "device_settings": {"coiot": {"update_period": 15}}, "device_status": MOCK_STATUS_COAP, @@ -42,10 +43,10 @@ async def test_block_config_entry_diagnostics( async def test_rpc_config_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSession, - rpc_wrapper, + mock_rpc_device, ): """Test config entry diagnostics for rpc device.""" - assert rpc_wrapper + await init_integration(hass, 2) entry = hass.config_entries.async_entries(DOMAIN)[0] entry_dict = entry.as_dict() @@ -58,9 +59,9 @@ async def test_rpc_config_entry_diagnostics( assert result == { "entry": entry_dict, "device_info": { - "name": rpc_wrapper.name, - "model": rpc_wrapper.model, - "sw_version": rpc_wrapper.sw_version, + "name": "Test name", + "model": "SHSW-25", + "sw_version": "some fw string", }, "device_settings": {}, "device_status": { diff --git a/tests/components/shelly/test_logbook.py b/tests/components/shelly/test_logbook.py index 5e267dcfd8f..b176b37c7e9 100644 --- a/tests/components/shelly/test_logbook.py +++ b/tests/components/shelly/test_logbook.py @@ -7,14 +7,23 @@ from homeassistant.components.shelly.const import ( EVENT_SHELLY_CLICK, ) from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.helpers.device_registry import ( + async_entries_for_config_entry, + async_get as async_get_dev_reg, +) from homeassistant.setup import async_setup_component +from . import init_integration + from tests.components.logbook.common import MockRow, mock_humanify -async def test_humanify_shelly_click_event_block_device(hass, coap_wrapper): +async def test_humanify_shelly_click_event_block_device(hass, mock_block_device): """Test humanifying Shelly click event for block device.""" - assert coap_wrapper + entry = await init_integration(hass, 1) + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) @@ -24,7 +33,7 @@ async def test_humanify_shelly_click_event_block_device(hass, coap_wrapper): MockRow( EVENT_SHELLY_CLICK, { - ATTR_DEVICE_ID: coap_wrapper.device_id, + ATTR_DEVICE_ID: device.id, ATTR_DEVICE: "shellyix3-12345678", ATTR_CLICK_TYPE: "single", ATTR_CHANNEL: 1, @@ -57,9 +66,12 @@ async def test_humanify_shelly_click_event_block_device(hass, coap_wrapper): ) -async def test_humanify_shelly_click_event_rpc_device(hass, rpc_wrapper): +async def test_humanify_shelly_click_event_rpc_device(hass, mock_rpc_device): """Test humanifying Shelly click event for rpc device.""" - assert rpc_wrapper + entry = await init_integration(hass, 2) + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, entry.entry_id)[0] + hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) @@ -69,7 +81,7 @@ async def test_humanify_shelly_click_event_rpc_device(hass, rpc_wrapper): MockRow( EVENT_SHELLY_CLICK, { - ATTR_DEVICE_ID: rpc_wrapper.device_id, + ATTR_DEVICE_ID: device.id, ATTR_DEVICE: "shellyplus1pm-12345678", ATTR_CLICK_TYPE: "single_push", ATTR_CHANNEL: 1, diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index cb93d9dace5..c2c7d90943d 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -7,19 +7,15 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.helpers.entity_component import async_update_entity + +from . import init_integration RELAY_BLOCK_ID = 0 -async def test_block_device_services(hass, coap_wrapper): +async def test_block_device_services(hass, mock_block_device): """Test block device turn on/off services.""" - assert coap_wrapper - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, SWITCH_DOMAIN) - ) - await hass.async_block_till_done() + await init_integration(hass, 1) await hass.services.async_call( SWITCH_DOMAIN, @@ -38,72 +34,43 @@ async def test_block_device_services(hass, coap_wrapper): assert hass.states.get("switch.test_name_channel_1").state == STATE_OFF -async def test_block_device_update(hass, coap_wrapper, monkeypatch): +async def test_block_device_update(hass, mock_block_device, monkeypatch): """Test block device update.""" - assert coap_wrapper - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, SWITCH_DOMAIN) - ) - await hass.async_block_till_done() - - monkeypatch.setattr(coap_wrapper.device.blocks[RELAY_BLOCK_ID], "output", False) - await async_update_entity(hass, "switch.test_name_channel_1") - await hass.async_block_till_done() + monkeypatch.setattr(mock_block_device.blocks[RELAY_BLOCK_ID], "output", False) + await init_integration(hass, 1) assert hass.states.get("switch.test_name_channel_1").state == STATE_OFF - monkeypatch.setattr(coap_wrapper.device.blocks[RELAY_BLOCK_ID], "output", True) - await async_update_entity(hass, "switch.test_name_channel_1") - await hass.async_block_till_done() + monkeypatch.setattr(mock_block_device.blocks[RELAY_BLOCK_ID], "output", True) + mock_block_device.mock_update() assert hass.states.get("switch.test_name_channel_1").state == STATE_ON -async def test_block_device_no_relay_blocks(hass, coap_wrapper, monkeypatch): +async def test_block_device_no_relay_blocks(hass, mock_block_device, monkeypatch): """Test block device without relay blocks.""" - assert coap_wrapper - - monkeypatch.setattr(coap_wrapper.device.blocks[RELAY_BLOCK_ID], "type", "roller") - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, SWITCH_DOMAIN) - ) - await hass.async_block_till_done() + monkeypatch.setattr(mock_block_device.blocks[RELAY_BLOCK_ID], "type", "roller") + await init_integration(hass, 1) assert hass.states.get("switch.test_name_channel_1") is None -async def test_block_device_mode_roller(hass, coap_wrapper, monkeypatch): +async def test_block_device_mode_roller(hass, mock_block_device, monkeypatch): """Test block device in roller mode.""" - assert coap_wrapper - - monkeypatch.setitem(coap_wrapper.device.settings, "mode", "roller") - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, SWITCH_DOMAIN) - ) - await hass.async_block_till_done() + monkeypatch.setitem(mock_block_device.settings, "mode", "roller") + await init_integration(hass, 1) assert hass.states.get("switch.test_name_channel_1") is None -async def test_block_device_app_type_light(hass, coap_wrapper, monkeypatch): +async def test_block_device_app_type_light(hass, mock_block_device, monkeypatch): """Test block device in app type set to light mode.""" - assert coap_wrapper - monkeypatch.setitem( - coap_wrapper.device.settings["relays"][0], "appliance_type", "light" + mock_block_device.settings["relays"][RELAY_BLOCK_ID], "appliance_type", "light" ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, SWITCH_DOMAIN) - ) - await hass.async_block_till_done() + await init_integration(hass, 1) assert hass.states.get("switch.test_name_channel_1") is None -async def test_rpc_device_services(hass, rpc_wrapper, monkeypatch): +async def test_rpc_device_services(hass, mock_rpc_device, monkeypatch): """Test RPC device turn on/off services.""" - assert rpc_wrapper - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, SWITCH_DOMAIN) - ) - await hass.async_block_till_done() + await init_integration(hass, 2) await hass.services.async_call( SWITCH_DOMAIN, @@ -113,28 +80,21 @@ async def test_rpc_device_services(hass, rpc_wrapper, monkeypatch): ) assert hass.states.get("switch.test_switch_0").state == STATE_ON - monkeypatch.setitem(rpc_wrapper.device.status["switch:0"], "output", False) + monkeypatch.setitem(mock_rpc_device.status["switch:0"], "output", False) await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "switch.test_switch_0"}, blocking=True, ) - rpc_wrapper.async_set_updated_data("") + mock_rpc_device.mock_update() assert hass.states.get("switch.test_switch_0").state == STATE_OFF -async def test_rpc_device_switch_type_lights_mode(hass, rpc_wrapper, monkeypatch): +async def test_rpc_device_switch_type_lights_mode(hass, mock_rpc_device, monkeypatch): """Test RPC device with switch in consumption type lights mode.""" - assert rpc_wrapper - monkeypatch.setitem( - rpc_wrapper.device.config["sys"]["ui_data"], - "consumption_types", - ["lights"], + mock_rpc_device.config["sys"]["ui_data"], "consumption_types", ["lights"] ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, SWITCH_DOMAIN) - ) - await hass.async_block_till_done() + await init_integration(hass, 2) assert hass.states.get("switch.test_switch_0") is None diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index 4d863c59390..7cff529f48a 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -6,11 +6,11 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.entity_registry import async_get +from . import init_integration -async def test_block_update(hass: HomeAssistant, coap_wrapper, monkeypatch): + +async def test_block_update(hass: HomeAssistant, mock_block_device, monkeypatch): """Test block device update entity.""" - assert coap_wrapper - entity_registry = async_get(hass) entity_registry.async_get_or_create( UPDATE_DOMAIN, @@ -19,18 +19,9 @@ async def test_block_update(hass: HomeAssistant, coap_wrapper, monkeypatch): suggested_object_id="test_name_firmware_update", disabled_by=None, ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, UPDATE_DOMAIN) - ) - await hass.async_block_till_done() + await init_integration(hass, 1) - # update entity - await async_update_entity(hass, "update.test_name_firmware_update") - await hass.async_block_till_done() - state = hass.states.get("update.test_name_firmware_update") - - assert state - assert state.state == STATE_ON + assert hass.states.get("update.test_name_firmware_update").state == STATE_ON await hass.services.async_call( UPDATE_DOMAIN, @@ -38,46 +29,30 @@ async def test_block_update(hass: HomeAssistant, coap_wrapper, monkeypatch): {ATTR_ENTITY_ID: "update.test_name_firmware_update"}, blocking=True, ) - await hass.async_block_till_done() - assert coap_wrapper.device.trigger_ota_update.call_count == 1 + assert mock_block_device.trigger_ota_update.call_count == 1 - monkeypatch.setitem(coap_wrapper.device.status["update"], "old_version", None) - monkeypatch.setitem(coap_wrapper.device.status["update"], "new_version", None) + monkeypatch.setitem(mock_block_device.status["update"], "old_version", None) + monkeypatch.setitem(mock_block_device.status["update"], "new_version", None) # update entity await async_update_entity(hass, "update.test_name_firmware_update") - await hass.async_block_till_done() - state = hass.states.get("update.test_name_firmware_update") - assert state - assert state.state == STATE_UNKNOWN + assert hass.states.get("update.test_name_firmware_update").state == STATE_UNKNOWN -async def test_rpc_update(hass: HomeAssistant, rpc_wrapper, monkeypatch): +async def test_rpc_update(hass: HomeAssistant, mock_rpc_device, monkeypatch): """Test rpc device update entity.""" - assert rpc_wrapper - entity_registry = async_get(hass) entity_registry.async_get_or_create( UPDATE_DOMAIN, DOMAIN, - "12345678-sys-fwupdate", + "shelly-sys-fwupdate", suggested_object_id="test_name_firmware_update", disabled_by=None, ) + await init_integration(hass, 2) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, UPDATE_DOMAIN) - ) - await hass.async_block_till_done() - - # update entity - await async_update_entity(hass, "update.test_name_firmware_update") - await hass.async_block_till_done() - state = hass.states.get("update.test_name_firmware_update") - - assert state - assert state.state == STATE_ON + assert hass.states.get("update.test_name_firmware_update").state == STATE_ON await hass.services.async_call( UPDATE_DOMAIN, @@ -86,15 +61,12 @@ async def test_rpc_update(hass: HomeAssistant, rpc_wrapper, monkeypatch): blocking=True, ) await hass.async_block_till_done() - assert rpc_wrapper.device.trigger_ota_update.call_count == 1 + assert mock_rpc_device.trigger_ota_update.call_count == 1 - monkeypatch.setitem(rpc_wrapper.device.status["sys"], "available_updates", {}) - rpc_wrapper.device.shelly = None + monkeypatch.setitem(mock_rpc_device.status["sys"], "available_updates", {}) + monkeypatch.setattr(mock_rpc_device, "shelly", None) # update entity await async_update_entity(hass, "update.test_name_firmware_update") - await hass.async_block_till_done() - state = hass.states.get("update.test_name_firmware_update") - assert state - assert state.state == STATE_UNKNOWN + assert hass.states.get("update.test_name_firmware_update").state == STATE_UNKNOWN From 59d9d3de69ef521d8e733bb0b04181be3553d924 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 5 Oct 2022 12:24:51 +0200 Subject: [PATCH 179/985] Add at_started helper (#79577) --- homeassistant/helpers/start.py | 62 ++++++++++++-- tests/helpers/test_start.py | 151 ++++++++++++++++++++++++++++++++- 2 files changed, 203 insertions(+), 10 deletions(-) diff --git a/homeassistant/helpers/start.py b/homeassistant/helpers/start.py index f6c9a536a23..fe3bd2b0987 100644 --- a/homeassistant/helpers/start.py +++ b/homeassistant/helpers/start.py @@ -4,21 +4,31 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from typing import Any -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import ( + CALLBACK_TYPE, + CoreState, + Event, + HassJob, + HomeAssistant, + callback, +) @callback -def async_at_start( +def _async_at_core_state( hass: HomeAssistant, at_start_cb: Callable[[HomeAssistant], Coroutine[Any, Any, None] | None], + event_type: str, + check_state: Callable[[HomeAssistant], bool], ) -> CALLBACK_TYPE: - """Execute something when Home Assistant is started. + """Execute a job at_start_cb when Home Assistant has the wanted state. - Will execute it now if Home Assistant is already started. + The job is executed immediately if Home Assistant is in the wanted state. + Will wait for event specified by event_type if it isn't. """ at_start_job = HassJob(at_start_cb) - if hass.is_running: + if check_state(hass): hass.async_run_hass_job(at_start_job, hass) return lambda: None @@ -36,5 +46,43 @@ def async_at_start( if unsub: unsub() - unsub = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _matched_event) + unsub = hass.bus.async_listen_once(event_type, _matched_event) return cancel + + +@callback +def async_at_start( + hass: HomeAssistant, + at_start_cb: Callable[[HomeAssistant], Coroutine[Any, Any, None] | None], +) -> CALLBACK_TYPE: + """Execute a job at_start_cb when Home Assistant is starting. + + The job is executed immediately if Home Assistant is already starting or started. + Will wait for EVENT_HOMEASSISTANT_START if it isn't. + """ + + def _is_running(hass: HomeAssistant) -> bool: + return hass.is_running + + return _async_at_core_state( + hass, at_start_cb, EVENT_HOMEASSISTANT_START, _is_running + ) + + +@callback +def async_at_started( + hass: HomeAssistant, + at_start_cb: Callable[[HomeAssistant], Coroutine[Any, Any, None] | None], +) -> CALLBACK_TYPE: + """Execute a job at_start_cb when Home Assistant has started. + + The job is executed immediately if Home Assistant is already started. + Will wait for EVENT_HOMEASSISTANT_STARTED if it isn't. + """ + + def _is_started(hass: HomeAssistant) -> bool: + return hass.state == CoreState.running + + return _async_at_core_state( + hass, at_start_cb, EVENT_HOMEASSISTANT_STARTED, _is_started + ) diff --git a/tests/helpers/test_start.py b/tests/helpers/test_start.py index bc32ffa35fd..bccf99a4274 100644 --- a/tests/helpers/test_start.py +++ b/tests/helpers/test_start.py @@ -1,6 +1,6 @@ """Test starting HA helpers.""" from homeassistant import core -from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED from homeassistant.helpers import start @@ -100,7 +100,7 @@ async def test_at_start_when_starting_callback(hass, caplog): assert record.levelname in ("DEBUG", "INFO") -async def test_cancelling_when_running(hass, caplog): +async def test_cancelling_at_start_when_running(hass, caplog): """Test cancelling at start when already running.""" assert hass.state == core.CoreState.running assert hass.is_running @@ -120,7 +120,7 @@ async def test_cancelling_when_running(hass, caplog): assert record.levelname in ("DEBUG", "INFO") -async def test_cancelling_when_starting(hass): +async def test_cancelling_at_start_when_starting(hass): """Test cancelling at start when yet to start.""" hass.state = core.CoreState.not_running assert not hass.is_running @@ -139,3 +139,148 @@ async def test_cancelling_when_starting(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() assert len(calls) == 0 + + +async def test_at_started_when_running_awaitable(hass): + """Test at started when already started.""" + assert hass.state == core.CoreState.running + + calls = [] + + async def cb_at_start(hass): + """Home Assistant is started.""" + calls.append(1) + + start.async_at_started(hass, cb_at_start) + await hass.async_block_till_done() + assert len(calls) == 1 + + # Test the job is not run if state is CoreState.starting + hass.state = core.CoreState.starting + + start.async_at_started(hass, cb_at_start) + await hass.async_block_till_done() + assert len(calls) == 1 + + +async def test_at_started_when_running_callback(hass, caplog): + """Test at started when already running.""" + assert hass.state == core.CoreState.running + + calls = [] + + @core.callback + def cb_at_start(hass): + """Home Assistant is started.""" + calls.append(1) + + start.async_at_started(hass, cb_at_start)() + assert len(calls) == 1 + + # Test the job is not run if state is CoreState.starting + hass.state = core.CoreState.starting + + start.async_at_started(hass, cb_at_start)() + assert len(calls) == 1 + + # Check the unnecessary cancel did not generate warnings or errors + for record in caplog.records: + assert record.levelname in ("DEBUG", "INFO") + + +async def test_at_started_when_starting_awaitable(hass): + """Test at started when yet to start.""" + hass.state = core.CoreState.not_running + + calls = [] + + async def cb_at_start(hass): + """Home Assistant is started.""" + calls.append(1) + + start.async_at_started(hass, cb_at_start) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert len(calls) == 1 + + +async def test_at_started_when_starting_callback(hass, caplog): + """Test at started when yet to start.""" + hass.state = core.CoreState.not_running + + calls = [] + + @core.callback + def cb_at_start(hass): + """Home Assistant is started.""" + calls.append(1) + + cancel = start.async_at_started(hass, cb_at_start) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert len(calls) == 1 + + cancel() + + # Check the unnecessary cancel did not generate warnings or errors + for record in caplog.records: + assert record.levelname in ("DEBUG", "INFO") + + +async def test_cancelling_at_started_when_running(hass, caplog): + """Test cancelling at start when already running.""" + assert hass.state == core.CoreState.running + assert hass.is_running + + calls = [] + + async def cb_at_start(hass): + """Home Assistant is started.""" + calls.append(1) + + start.async_at_started(hass, cb_at_start)() + await hass.async_block_till_done() + assert len(calls) == 1 + + # Check the unnecessary cancel did not generate warnings or errors + for record in caplog.records: + assert record.levelname in ("DEBUG", "INFO") + + +async def test_cancelling_at_started_when_starting(hass): + """Test cancelling at start when yet to start.""" + hass.state = core.CoreState.not_running + assert not hass.is_running + + calls = [] + + @core.callback + def cb_at_start(hass): + """Home Assistant is started.""" + calls.append(1) + + start.async_at_started(hass, cb_at_start)() + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert len(calls) == 0 From 4d3d22320fa0f3bd6abd109e0727e3bba9eab7c7 Mon Sep 17 00:00:00 2001 From: Jafar Atili Date: Wed, 5 Oct 2022 14:19:03 +0300 Subject: [PATCH 180/985] Enhanced switchbee device naming (#79641) --- homeassistant/components/switchbee/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switchbee/entity.py b/homeassistant/components/switchbee/entity.py index 5129446a204..28248667c50 100644 --- a/homeassistant/components/switchbee/entity.py +++ b/homeassistant/components/switchbee/entity.py @@ -50,7 +50,7 @@ class SwitchBeeDeviceEntity(SwitchBeeEntity[_DeviceTypeT]): device.id if device.type == DeviceType.Thermostat else device.unit_id ) self._attr_device_info = DeviceInfo( - name=f"SwitchBee {identifier}", + name=device.zone, identifiers={ ( DOMAIN, From 22c68b95bf7047615e4aeac7a0cf4a9bd12f82cc Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 5 Oct 2022 15:39:58 +0300 Subject: [PATCH 181/985] Refactor Shelly wrapper to coordinator (#79628) --- homeassistant/components/shelly/__init__.py | 54 ++--- homeassistant/components/shelly/button.py | 49 ++--- homeassistant/components/shelly/climate.py | 69 ++++--- .../components/shelly/coordinator.py | 49 ++--- homeassistant/components/shelly/cover.py | 24 +-- .../components/shelly/device_trigger.py | 34 ++-- .../components/shelly/diagnostics.py | 30 +-- homeassistant/components/shelly/entity.py | 188 +++++++++--------- homeassistant/components/shelly/light.py | 54 ++--- homeassistant/components/shelly/logbook.py | 14 +- homeassistant/components/shelly/number.py | 2 +- homeassistant/components/shelly/sensor.py | 16 +- homeassistant/components/shelly/switch.py | 38 ++-- homeassistant/components/shelly/update.py | 36 ++-- 14 files changed, 336 insertions(+), 321 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index e46d5a81c0e..1d59816c661 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -36,10 +36,10 @@ from .const import ( RPC_POLL, ) from .coordinator import ( - BlockDeviceWrapper, - RpcDeviceWrapper, - RpcPollingWrapper, - ShellyDeviceRestWrapper, + ShellyBlockCoordinator, + ShellyRestCoordinator, + ShellyRpcCoordinator, + ShellyRpcPollingCoordinator, ) from .utils import get_block_device_sleep_period, get_coap_context, get_device_entry_gen @@ -200,17 +200,17 @@ def async_block_device_setup( hass: HomeAssistant, entry: ConfigEntry, device: BlockDevice ) -> None: """Set up a block based device that is online.""" - device_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ + block_coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ BLOCK - ] = BlockDeviceWrapper(hass, entry, device) - device_wrapper.async_setup() + ] = ShellyBlockCoordinator(hass, entry, device) + block_coordinator.async_setup() platforms = BLOCK_SLEEPING_PLATFORMS if not entry.data.get(CONF_SLEEP_PERIOD): hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ REST - ] = ShellyDeviceRestWrapper(hass, device, entry) + ] = ShellyRestCoordinator(hass, device, entry) platforms = BLOCK_PLATFORMS hass.config_entries.async_setup_platforms(entry, platforms) @@ -237,14 +237,14 @@ async def async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool except (AuthRequired, InvalidAuthError) as err: raise ConfigEntryAuthFailed from err - device_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ + rpc_coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ RPC - ] = RpcDeviceWrapper(hass, entry, device) - device_wrapper.async_setup() + ] = ShellyRpcCoordinator(hass, entry, device) + rpc_coordinator.async_setup() - hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][RPC_POLL] = RpcPollingWrapper( - hass, entry, device - ) + hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ + RPC_POLL + ] = ShellyRpcPollingCoordinator(hass, entry, device) hass.config_entries.async_setup_platforms(entry, RPC_PLATFORMS) @@ -265,7 +265,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id].get(DEVICE) if device is not None: - # If device is present, device wrapper is not setup yet + # If device is present, block coordinator is not setup yet device.shutdown() return True @@ -283,10 +283,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -def get_block_device_wrapper( +def get_block_device_coordinator( hass: HomeAssistant, device_id: str -) -> BlockDeviceWrapper | None: - """Get a Shelly block device wrapper for the given device id.""" +) -> ShellyBlockCoordinator | None: + """Get a Shelly block device coordinator for the given device id.""" if not hass.data.get(DOMAIN): return None @@ -296,16 +296,18 @@ def get_block_device_wrapper( if not hass.data[DOMAIN][DATA_CONFIG_ENTRY].get(config_entry): continue - if wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get(BLOCK): - return cast(BlockDeviceWrapper, wrapper) + if coordinator := hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get( + BLOCK + ): + return cast(ShellyBlockCoordinator, coordinator) return None -def get_rpc_device_wrapper( +def get_rpc_device_coordinator( hass: HomeAssistant, device_id: str -) -> RpcDeviceWrapper | None: - """Get a Shelly RPC device wrapper for the given device id.""" +) -> ShellyRpcCoordinator | None: + """Get a Shelly RPC device coordinator for the given device id.""" if not hass.data.get(DOMAIN): return None @@ -315,7 +317,9 @@ def get_rpc_device_wrapper( if not hass.data[DOMAIN][DATA_CONFIG_ENTRY].get(config_entry): continue - if wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get(RPC): - return cast(RpcDeviceWrapper, wrapper) + if coordinator := hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get( + RPC + ): + return cast(ShellyRpcCoordinator, coordinator) return None diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 144b100e8eb..48213d706ef 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -15,10 +15,11 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import slugify -from . import BlockDeviceWrapper, RpcDeviceWrapper from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC, SHELLY_GAS_MODELS +from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator from .utils import get_block_device_name, get_device_entry_gen, get_rpc_device_name @@ -42,31 +43,31 @@ BUTTONS: Final = [ name="Reboot", device_class=ButtonDeviceClass.RESTART, entity_category=EntityCategory.CONFIG, - press_action=lambda wrapper: wrapper.device.trigger_reboot(), + press_action=lambda coordinator: coordinator.device.trigger_reboot(), ), ShellyButtonDescription( key="self_test", name="Self Test", icon="mdi:progress-wrench", entity_category=EntityCategory.DIAGNOSTIC, - press_action=lambda wrapper: wrapper.device.trigger_shelly_gas_self_test(), - supported=lambda wrapper: wrapper.device.model in SHELLY_GAS_MODELS, + press_action=lambda coordinator: coordinator.device.trigger_shelly_gas_self_test(), + supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS, ), ShellyButtonDescription( key="mute", name="Mute", icon="mdi:volume-mute", entity_category=EntityCategory.CONFIG, - press_action=lambda wrapper: wrapper.device.trigger_shelly_gas_mute(), - supported=lambda wrapper: wrapper.device.model in SHELLY_GAS_MODELS, + press_action=lambda coordinator: coordinator.device.trigger_shelly_gas_mute(), + supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS, ), ShellyButtonDescription( key="unmute", name="Unmute", icon="mdi:volume-high", entity_category=EntityCategory.CONFIG, - press_action=lambda wrapper: wrapper.device.trigger_shelly_gas_unmute(), - supported=lambda wrapper: wrapper.device.model in SHELLY_GAS_MODELS, + press_action=lambda coordinator: coordinator.device.trigger_shelly_gas_unmute(), + supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS, ), ] @@ -77,54 +78,54 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set buttons for device.""" - wrapper: RpcDeviceWrapper | BlockDeviceWrapper | None = None + coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator | None = None if get_device_entry_gen(config_entry) == 2: - if rpc_wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY][ + if rpc_coordinator := hass.data[DOMAIN][DATA_CONFIG_ENTRY][ config_entry.entry_id ].get(RPC): - wrapper = cast(RpcDeviceWrapper, rpc_wrapper) + coordinator = cast(ShellyRpcCoordinator, rpc_coordinator) else: - if block_wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY][ + if block_coordinator := hass.data[DOMAIN][DATA_CONFIG_ENTRY][ config_entry.entry_id ].get(BLOCK): - wrapper = cast(BlockDeviceWrapper, block_wrapper) + coordinator = cast(ShellyBlockCoordinator, block_coordinator) - if wrapper is not None: + if coordinator is not None: entities = [] for button in BUTTONS: - if not button.supported(wrapper): + if not button.supported(coordinator): continue - entities.append(ShellyButton(wrapper, button)) + entities.append(ShellyButton(coordinator, button)) async_add_entities(entities) -class ShellyButton(ButtonEntity): +class ShellyButton(CoordinatorEntity, ButtonEntity): """Defines a Shelly base button.""" entity_description: ShellyButtonDescription def __init__( self, - wrapper: RpcDeviceWrapper | BlockDeviceWrapper, + coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator, description: ShellyButtonDescription, ) -> None: """Initialize Shelly button.""" + super().__init__(coordinator) self.entity_description = description - self.wrapper = wrapper - if isinstance(wrapper, RpcDeviceWrapper): - device_name = get_rpc_device_name(wrapper.device) + if isinstance(coordinator, ShellyRpcCoordinator): + device_name = get_rpc_device_name(coordinator.device) else: - device_name = get_block_device_name(wrapper.device) + device_name = get_block_device_name(coordinator.device) self._attr_name = f"{device_name} {description.name}" self._attr_unique_id = slugify(self._attr_name) self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, wrapper.mac)} + connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} ) async def async_press(self) -> None: """Triggers the Shelly button press service.""" - await self.entity_description.press_action(self.wrapper) + await self.entity_description.press_action(self.coordinator) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index f98c048d569..5ab5b728aa4 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -20,12 +20,12 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant, State, callback -from homeassistant.helpers import device_registry, entity_registry, update_coordinator +from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import BlockDeviceWrapper from .const import ( AIOSHELLY_DEVICE_TIMEOUT_SEC, BLOCK, @@ -34,6 +34,7 @@ from .const import ( LOGGER, SHTRV_01_TEMPERATURE_SETTINGS, ) +from .coordinator import ShellyBlockCoordinator from .utils import get_device_entry_gen @@ -47,37 +48,41 @@ async def async_setup_entry( if get_device_entry_gen(config_entry) == 2: return - wrapper: BlockDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ + coordinator: ShellyBlockCoordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ config_entry.entry_id ][BLOCK] - if wrapper.device.initialized: - async_setup_climate_entities(async_add_entities, wrapper) + if coordinator.device.initialized: + async_setup_climate_entities(async_add_entities, coordinator) else: - async_restore_climate_entities(hass, config_entry, async_add_entities, wrapper) + async_restore_climate_entities( + hass, config_entry, async_add_entities, coordinator + ) @callback def async_setup_climate_entities( async_add_entities: AddEntitiesCallback, - wrapper: BlockDeviceWrapper, + coordinator: ShellyBlockCoordinator, ) -> None: """Set up online climate devices.""" device_block: Block | None = None sensor_block: Block | None = None - assert wrapper.device.blocks + assert coordinator.device.blocks - for block in wrapper.device.blocks: + for block in coordinator.device.blocks: if block.type == "device": device_block = block if hasattr(block, "targetTemp"): sensor_block = block if sensor_block and device_block: - LOGGER.debug("Setup online climate device %s", wrapper.name) - async_add_entities([BlockSleepingClimate(wrapper, sensor_block, device_block)]) + LOGGER.debug("Setup online climate device %s", coordinator.name) + async_add_entities( + [BlockSleepingClimate(coordinator, sensor_block, device_block)] + ) @callback @@ -85,7 +90,7 @@ def async_restore_climate_entities( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - wrapper: BlockDeviceWrapper, + coordinator: ShellyBlockCoordinator, ) -> None: """Restore sleeping climate devices.""" @@ -99,16 +104,14 @@ def async_restore_climate_entities( if entry.domain != CLIMATE_DOMAIN: continue - LOGGER.debug("Setup sleeping climate device %s", wrapper.name) + LOGGER.debug("Setup sleeping climate device %s", coordinator.name) LOGGER.debug("Found entry %s [%s]", entry.original_name, entry.domain) - async_add_entities([BlockSleepingClimate(wrapper, None, None, entry)]) + async_add_entities([BlockSleepingClimate(coordinator, None, None, entry)]) break class BlockSleepingClimate( - update_coordinator.CoordinatorEntity, - RestoreEntity, - ClimateEntity, + CoordinatorEntity[ShellyBlockCoordinator], RestoreEntity, ClimateEntity ): """Representation of a Shelly climate device.""" @@ -124,16 +127,14 @@ class BlockSleepingClimate( def __init__( self, - wrapper: BlockDeviceWrapper, + coordinator: ShellyBlockCoordinator, sensor_block: Block | None, device_block: Block | None, entry: entity_registry.RegistryEntry | None = None, ) -> None: """Initialize climate.""" + super().__init__(coordinator) - super().__init__(wrapper) - - self.wrapper = wrapper self.block: Block | None = sensor_block self.control_result: dict[str, Any] | None = None self.device_block: Block | None = device_block @@ -142,11 +143,11 @@ class BlockSleepingClimate( self._preset_modes: list[str] = [] if self.block is not None and self.device_block is not None: - self._unique_id = f"{self.wrapper.mac}-{self.block.description}" + self._unique_id = f"{self.coordinator.mac}-{self.block.description}" assert self.block.channel self._preset_modes = [ PRESET_NONE, - *wrapper.device.settings["thermostats"][int(self.block.channel)][ + *coordinator.device.settings["thermostats"][int(self.block.channel)][ "schedule_profile_names" ], ] @@ -163,7 +164,7 @@ class BlockSleepingClimate( @property def name(self) -> str: """Name of entity.""" - return self.wrapper.name + return self.coordinator.name @property def target_temperature(self) -> float | None: @@ -184,7 +185,7 @@ class BlockSleepingClimate( """Device availability.""" if self.device_block is not None: return not cast(bool, self.device_block.valveError) - return self.wrapper.last_update_success + return self.coordinator.last_update_success @property def hvac_mode(self) -> HVACMode: @@ -229,7 +230,9 @@ class BlockSleepingClimate( def device_info(self) -> DeviceInfo: """Device info.""" return { - "connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)} + "connections": { + (device_registry.CONNECTION_NETWORK_MAC, self.coordinator.mac) + } } def _check_is_off(self) -> bool: @@ -244,7 +247,7 @@ class BlockSleepingClimate( LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs) try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - return await self.wrapper.device.http_request( + return await self.coordinator.device.http_request( "get", f"thermostat/{self._channel}", kwargs ) except (asyncio.TimeoutError, OSError) as err: @@ -254,7 +257,7 @@ class BlockSleepingClimate( kwargs, repr(err), ) - self.wrapper.last_update_success = False + self.coordinator.last_update_success = False return None async def async_set_temperature(self, **kwargs: Any) -> None: @@ -302,13 +305,13 @@ class BlockSleepingClimate( @callback def _handle_coordinator_update(self) -> None: """Handle device update.""" - if not self.wrapper.device.initialized: + if not self.coordinator.device.initialized: self.async_write_ha_state() return - assert self.wrapper.device.blocks + assert self.coordinator.device.blocks - for block in self.wrapper.device.blocks: + for block in self.coordinator.device.blocks: if block.type == "device": self.device_block = block if hasattr(block, "targetTemp"): @@ -322,11 +325,11 @@ class BlockSleepingClimate( try: self._preset_modes = [ PRESET_NONE, - *self.wrapper.device.settings["thermostats"][ + *self.coordinator.device.settings["thermostats"][ int(self.block.channel) ]["schedule_profile_names"], ] except AuthRequired: - self.wrapper.entry.async_start_reauth(self.hass) + self.coordinator.entry.async_start_reauth(self.hass) else: self.async_write_ha_state() diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 02a4e6ffba1..db485167fe3 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -14,8 +14,9 @@ import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.helpers import device_registry, update_coordinator +from homeassistant.helpers import device_registry from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( AIOSHELLY_DEVICE_TIMEOUT_SEC, @@ -44,13 +45,13 @@ from .const import ( from .utils import device_update_info, get_block_device_name, get_rpc_device_name -class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): - """Wrapper for a Shelly block based device with Home Assistant specific functions.""" +class ShellyBlockCoordinator(DataUpdateCoordinator): + """Coordinator for a Shelly block based device.""" def __init__( self, hass: HomeAssistant, entry: ConfigEntry, device: BlockDevice ) -> None: - """Initialize the Shelly device wrapper.""" + """Initialize the Shelly block device coordinator.""" self.device_id: str | None = None if sleep_period := entry.data[CONF_SLEEP_PERIOD]: @@ -186,7 +187,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): """Fetch data.""" if sleep_period := self.entry.data.get(CONF_SLEEP_PERIOD): # Sleeping device, no point polling it, just mark it unavailable - raise update_coordinator.UpdateFailed( + raise UpdateFailed( f"Sleeping device did not update within {sleep_period} seconds interval" ) @@ -196,7 +197,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): await self.device.update() device_update_info(self.hass, self.device, self.entry) except OSError as err: - raise update_coordinator.UpdateFailed("Error fetching data") from err + raise UpdateFailed("Error fetching data") from err @property def model(self) -> str: @@ -214,7 +215,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): return self.device.firmware_version if self.device.initialized else "" def async_setup(self) -> None: - """Set up the wrapper.""" + """Set up the coordinator.""" dev_reg = device_registry.async_get(self.hass) entry = dev_reg.async_get_or_create( config_entry_id=self.entry.entry_id, @@ -265,23 +266,23 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): LOGGER.debug("Result of OTA update call: %s", result) def shutdown(self) -> None: - """Shutdown the wrapper.""" + """Shutdown the coordinator.""" self.device.shutdown() @callback def _handle_ha_stop(self, _event: Event) -> None: """Handle Home Assistant stopping.""" - LOGGER.debug("Stopping BlockDeviceWrapper for %s", self.name) + LOGGER.debug("Stopping block device coordinator for %s", self.name) self.shutdown() -class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): - """Rest Wrapper for a Shelly device with Home Assistant specific functions.""" +class ShellyRestCoordinator(DataUpdateCoordinator): + """Coordinator for a Shelly REST device.""" def __init__( self, hass: HomeAssistant, device: BlockDevice, entry: ConfigEntry ) -> None: - """Initialize the Shelly device wrapper.""" + """Initialize the Shelly REST device coordinator.""" if ( device.settings["device"]["type"] in BATTERY_DEVICES_WITH_PERMANENT_CONNECTION @@ -316,7 +317,7 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): return device_update_info(self.hass, self.device, self.entry) except OSError as err: - raise update_coordinator.UpdateFailed("Error fetching data") from err + raise UpdateFailed("Error fetching data") from err @property def mac(self) -> str: @@ -324,13 +325,13 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): return cast(str, self.device.settings["device"]["mac"]) -class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): - """Wrapper for a Shelly RPC based device with Home Assistant specific functions.""" +class ShellyRpcCoordinator(DataUpdateCoordinator): + """Coordinator for a Shelly RPC based device.""" def __init__( self, hass: HomeAssistant, entry: ConfigEntry, device: RpcDevice ) -> None: - """Initialize the Shelly device wrapper.""" + """Initialize the Shelly RPC device coordinator.""" self.device_id: str | None = None device_name = get_rpc_device_name(device) if device.initialized else entry.title @@ -413,7 +414,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): await self.device.initialize() device_update_info(self.hass, self.device, self.entry) except OSError as err: - raise update_coordinator.UpdateFailed("Device disconnected") from err + raise UpdateFailed("Device disconnected") from err @property def model(self) -> str: @@ -431,7 +432,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): return self.device.firmware_version if self.device.initialized else "" def async_setup(self) -> None: - """Set up the wrapper.""" + """Set up the coordinator.""" dev_reg = device_registry.async_get(self.hass) entry = dev_reg.async_get_or_create( config_entry_id=self.entry.entry_id, @@ -482,17 +483,17 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): LOGGER.debug("OTA update call successful") async def shutdown(self) -> None: - """Shutdown the wrapper.""" + """Shutdown the coordinator.""" await self.device.shutdown() async def _handle_ha_stop(self, _event: Event) -> None: """Handle Home Assistant stopping.""" - LOGGER.debug("Stopping RpcDeviceWrapper for %s", self.name) + LOGGER.debug("Stopping RPC device coordinator for %s", self.name) await self.shutdown() -class RpcPollingWrapper(update_coordinator.DataUpdateCoordinator): - """Polling Wrapper for a Shelly RPC based device.""" +class ShellyRpcPollingCoordinator(DataUpdateCoordinator): + """Polling coordinator for a Shelly RPC based device.""" def __init__( self, hass: HomeAssistant, entry: ConfigEntry, device: RpcDevice @@ -513,14 +514,14 @@ class RpcPollingWrapper(update_coordinator.DataUpdateCoordinator): async def _async_update_data(self) -> None: """Fetch data.""" if not self.device.connected: - raise update_coordinator.UpdateFailed("Device disconnected") + raise UpdateFailed("Device disconnected") try: LOGGER.debug("Polling Shelly RPC Device - %s", self.name) async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): await self.device.update_status() except (OSError, aioshelly.exceptions.RPCTimeout) as err: - raise update_coordinator.UpdateFailed("Device disconnected") from err + raise UpdateFailed("Device disconnected") from err @property def model(self) -> str: diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py index e28fe22a528..e94cd6a9e86 100644 --- a/homeassistant/components/shelly/cover.py +++ b/homeassistant/components/shelly/cover.py @@ -15,8 +15,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BlockDeviceWrapper, RpcDeviceWrapper from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC +from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator from .entity import ShellyBlockEntity, ShellyRpcEntity from .utils import get_device_entry_gen, get_rpc_key_ids @@ -40,13 +40,13 @@ def async_setup_block_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up cover for device.""" - wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK] - blocks = [block for block in wrapper.device.blocks if block.type == "roller"] + coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK] + blocks = [block for block in coordinator.device.blocks if block.type == "roller"] if not blocks: return - async_add_entities(BlockShellyCover(wrapper, block) for block in blocks) + async_add_entities(BlockShellyCover(coordinator, block) for block in blocks) @callback @@ -56,14 +56,14 @@ def async_setup_rpc_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for RPC device.""" - wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC] + coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC] - cover_key_ids = get_rpc_key_ids(wrapper.device.status, "cover") + cover_key_ids = get_rpc_key_ids(coordinator.device.status, "cover") if not cover_key_ids: return - async_add_entities(RpcShellyCover(wrapper, id_) for id_ in cover_key_ids) + async_add_entities(RpcShellyCover(coordinator, id_) for id_ in cover_key_ids) class BlockShellyCover(ShellyBlockEntity, CoverEntity): @@ -71,14 +71,14 @@ class BlockShellyCover(ShellyBlockEntity, CoverEntity): _attr_device_class = CoverDeviceClass.SHUTTER - def __init__(self, wrapper: BlockDeviceWrapper, block: Block) -> None: + def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None: """Initialize block cover.""" - super().__init__(wrapper, block) + super().__init__(coordinator, block) self.control_result: dict[str, Any] | None = None self._attr_supported_features: int = ( CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP ) - if self.wrapper.device.settings["rollers"][0]["positioning"]: + if self.coordinator.device.settings["rollers"][0]["positioning"]: self._attr_supported_features |= CoverEntityFeature.SET_POSITION @property @@ -147,9 +147,9 @@ class RpcShellyCover(ShellyRpcEntity, CoverEntity): _attr_device_class = CoverDeviceClass.SHUTTER - def __init__(self, wrapper: RpcDeviceWrapper, id_: int) -> None: + def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None: """Initialize rpc cover.""" - super().__init__(wrapper, f"cover:{id_}") + super().__init__(coordinator, f"cover:{id_}") self._id = id_ self._attr_supported_features: int = ( CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py index fe253ebacb6..32b3432b0aa 100644 --- a/homeassistant/components/shelly/device_trigger.py +++ b/homeassistant/components/shelly/device_trigger.py @@ -22,7 +22,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType -from . import get_block_device_wrapper, get_rpc_device_wrapper +from . import get_block_device_coordinator, get_rpc_device_coordinator from .const import ( ATTR_CHANNEL, ATTR_CLICK_TYPE, @@ -78,23 +78,23 @@ async def async_validate_trigger_config( trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) if config[CONF_TYPE] in RPC_INPUTS_EVENTS_TYPES: - rpc_wrapper = get_rpc_device_wrapper(hass, config[CONF_DEVICE_ID]) - if not rpc_wrapper or not rpc_wrapper.device.initialized: + rpc_coordinator = get_rpc_device_coordinator(hass, config[CONF_DEVICE_ID]) + if not rpc_coordinator or not rpc_coordinator.device.initialized: return config - input_triggers = get_rpc_input_triggers(rpc_wrapper.device) + input_triggers = get_rpc_input_triggers(rpc_coordinator.device) if trigger in input_triggers: return config elif config[CONF_TYPE] in BLOCK_INPUTS_EVENTS_TYPES: - block_wrapper = get_block_device_wrapper(hass, config[CONF_DEVICE_ID]) - if not block_wrapper or not block_wrapper.device.initialized: + block_coordinator = get_block_device_coordinator(hass, config[CONF_DEVICE_ID]) + if not block_coordinator or not block_coordinator.device.initialized: return config - assert block_wrapper.device.blocks + assert block_coordinator.device.blocks - for block in block_wrapper.device.blocks: - input_triggers = get_block_input_triggers(block_wrapper.device, block) + for block in block_coordinator.device.blocks: + input_triggers = get_block_input_triggers(block_coordinator.device, block) if trigger in input_triggers: return config @@ -109,24 +109,24 @@ async def async_get_triggers( """List device triggers for Shelly devices.""" triggers: list[dict[str, str]] = [] - if rpc_wrapper := get_rpc_device_wrapper(hass, device_id): - input_triggers = get_rpc_input_triggers(rpc_wrapper.device) + if rpc_coordinator := get_rpc_device_coordinator(hass, device_id): + input_triggers = get_rpc_input_triggers(rpc_coordinator.device) append_input_triggers(triggers, input_triggers, device_id) return triggers - if block_wrapper := get_block_device_wrapper(hass, device_id): - if block_wrapper.model in SHBTN_MODELS: + if block_coordinator := get_block_device_coordinator(hass, device_id): + if block_coordinator.model in SHBTN_MODELS: input_triggers = get_shbtn_input_triggers() append_input_triggers(triggers, input_triggers, device_id) return triggers - if not block_wrapper.device.initialized: + if not block_coordinator.device.initialized: return triggers - assert block_wrapper.device.blocks + assert block_coordinator.device.blocks - for block in block_wrapper.device.blocks: - input_triggers = get_block_input_triggers(block_wrapper.device, block) + for block in block_coordinator.device.blocks: + input_triggers = get_block_input_triggers(block_coordinator.device, block) append_input_triggers(triggers, input_triggers, device_id) return triggers diff --git a/homeassistant/components/shelly/diagnostics.py b/homeassistant/components/shelly/diagnostics.py index 47dc18d377b..114522e31ac 100644 --- a/homeassistant/components/shelly/diagnostics.py +++ b/homeassistant/components/shelly/diagnostics.py @@ -6,8 +6,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from . import BlockDeviceWrapper, RpcDeviceWrapper from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC +from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator TO_REDACT = {CONF_USERNAME, CONF_PASSWORD} @@ -21,21 +21,21 @@ async def async_get_config_entry_diagnostics( device_settings: str | dict = "not initialized" device_status: str | dict = "not initialized" if BLOCK in data: - block_wrapper: BlockDeviceWrapper = data[BLOCK] + block_coordinator: ShellyBlockCoordinator = data[BLOCK] device_info = { - "name": block_wrapper.name, - "model": block_wrapper.model, - "sw_version": block_wrapper.sw_version, + "name": block_coordinator.name, + "model": block_coordinator.model, + "sw_version": block_coordinator.sw_version, } - if block_wrapper.device.initialized: + if block_coordinator.device.initialized: device_settings = { k: v - for k, v in block_wrapper.device.settings.items() + for k, v in block_coordinator.device.settings.items() if k in ["cloud", "coiot"] } device_status = { k: v - for k, v in block_wrapper.device.status.items() + for k, v in block_coordinator.device.status.items() if k in [ "update", @@ -51,19 +51,19 @@ async def async_get_config_entry_diagnostics( ] } else: - rpc_wrapper: RpcDeviceWrapper = data[RPC] + rpc_coordinator: ShellyRpcCoordinator = data[RPC] device_info = { - "name": rpc_wrapper.name, - "model": rpc_wrapper.model, - "sw_version": rpc_wrapper.sw_version, + "name": rpc_coordinator.name, + "model": rpc_coordinator.model, + "sw_version": rpc_coordinator.sw_version, } - if rpc_wrapper.device.initialized: + if rpc_coordinator.device.initialized: device_settings = { - k: v for k, v in rpc_wrapper.device.config.items() if k in ["cloud"] + k: v for k, v in rpc_coordinator.device.config.items() if k in ["cloud"] } device_status = { k: v - for k, v in rpc_wrapper.device.status.items() + for k, v in rpc_coordinator.device.status.items() if k in ["sys", "wifi"] } diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index a38bef54bea..21512238cd4 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -11,23 +11,13 @@ import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import ( - device_registry, - entity, - entity_registry, - update_coordinator, -) +from homeassistant.helpers import device_registry, entity, entity_registry from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import ( - BlockDeviceWrapper, - RpcDeviceWrapper, - RpcPollingWrapper, - ShellyDeviceRestWrapper, -) from .const import ( AIOSHELLY_DEVICE_TIMEOUT_SEC, BLOCK, @@ -38,6 +28,12 @@ from .const import ( RPC, RPC_POLL, ) +from .coordinator import ( + ShellyBlockCoordinator, + ShellyRestCoordinator, + ShellyRpcCoordinator, + ShellyRpcPollingCoordinator, +) from .utils import ( async_remove_shelly_entity, get_block_entity_name, @@ -58,20 +54,20 @@ def async_setup_entry_attribute_entities( ], ) -> None: """Set up entities for attributes.""" - wrapper: BlockDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ + coordinator: ShellyBlockCoordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ config_entry.entry_id ][BLOCK] - if wrapper.device.initialized: + if coordinator.device.initialized: async_setup_block_attribute_entities( - hass, async_add_entities, wrapper, sensors, sensor_class + hass, async_add_entities, coordinator, sensors, sensor_class ) else: async_restore_block_attribute_entities( hass, config_entry, async_add_entities, - wrapper, + coordinator, sensors, sensor_class, description_class, @@ -82,16 +78,16 @@ def async_setup_entry_attribute_entities( def async_setup_block_attribute_entities( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, - wrapper: BlockDeviceWrapper, + coordinator: ShellyBlockCoordinator, sensors: Mapping[tuple[str, str], BlockEntityDescription], sensor_class: Callable, ) -> None: """Set up entities for block attributes.""" blocks = [] - assert wrapper.device.blocks + assert coordinator.device.blocks - for block in wrapper.device.blocks: + for block in coordinator.device.blocks: for sensor_id in block.sensor_ids: description = sensors.get((block.type, sensor_id)) if description is None: @@ -103,10 +99,10 @@ def async_setup_block_attribute_entities( # Filter and remove entities that according to settings should not create an entity if description.removal_condition and description.removal_condition( - wrapper.device.settings, block + coordinator.device.settings, block ): domain = sensor_class.__module__.split(".")[-1] - unique_id = f"{wrapper.mac}-{block.description}-{sensor_id}" + unique_id = f"{coordinator.mac}-{block.description}-{sensor_id}" async_remove_shelly_entity(hass, domain, unique_id) else: blocks.append((block, sensor_id, description)) @@ -116,7 +112,7 @@ def async_setup_block_attribute_entities( async_add_entities( [ - sensor_class(wrapper, block, sensor_id, description) + sensor_class(coordinator, block, sensor_id, description) for block, sensor_id, description in blocks ] ) @@ -127,7 +123,7 @@ def async_restore_block_attribute_entities( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - wrapper: BlockDeviceWrapper, + coordinator: ShellyBlockCoordinator, sensors: Mapping[tuple[str, str], BlockEntityDescription], sensor_class: Callable, description_class: Callable[ @@ -152,7 +148,7 @@ def async_restore_block_attribute_entities( description = description_class(entry) entities.append( - sensor_class(wrapper, None, attribute, description, entry, sensors) + sensor_class(coordinator, None, attribute, description, entry, sensors) ) if not entities: @@ -170,40 +166,44 @@ def async_setup_entry_rpc( sensor_class: Callable, ) -> None: """Set up entities for REST sensors.""" - wrapper: RpcDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ + coordinator: ShellyRpcCoordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ config_entry.entry_id ][RPC] - polling_wrapper: RpcPollingWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ - config_entry.entry_id - ][RPC_POLL] + polling_coordinator: ShellyRpcPollingCoordinator = hass.data[DOMAIN][ + DATA_CONFIG_ENTRY + ][config_entry.entry_id][RPC_POLL] entities = [] for sensor_id in sensors: description = sensors[sensor_id] - key_instances = get_rpc_key_instances(wrapper.device.status, description.key) + key_instances = get_rpc_key_instances( + coordinator.device.status, description.key + ) for key in key_instances: # Filter non-existing sensors - if description.sub_key not in wrapper.device.status[ + if description.sub_key not in coordinator.device.status[ key - ] and not description.supported(wrapper.device.status[key]): + ] and not description.supported(coordinator.device.status[key]): continue # Filter and remove entities that according to settings/status should not create an entity if description.removal_condition and description.removal_condition( - wrapper.device.config, wrapper.device.status, key + coordinator.device.config, coordinator.device.status, key ): domain = sensor_class.__module__.split(".")[-1] - unique_id = f"{wrapper.mac}-{key}-{sensor_id}" + unique_id = f"{coordinator.mac}-{key}-{sensor_id}" async_remove_shelly_entity(hass, domain, unique_id) else: - if description.use_polling_wrapper: + if description.use_polling_coordinator: entities.append( - sensor_class(polling_wrapper, key, sensor_id, description) + sensor_class(polling_coordinator, key, sensor_id, description) ) else: - entities.append(sensor_class(wrapper, key, sensor_id, description)) + entities.append( + sensor_class(coordinator, key, sensor_id, description) + ) if not entities: return @@ -220,7 +220,7 @@ def async_setup_entry_rest( sensor_class: Callable, ) -> None: """Set up entities for REST sensors.""" - wrapper: ShellyDeviceRestWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ + coordinator: ShellyRestCoordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ config_entry.entry_id ][REST] @@ -228,7 +228,7 @@ def async_setup_entry_rest( for sensor_id in sensors: description = sensors.get(sensor_id) - if not wrapper.device.settings.get("sleep_mode"): + if not coordinator.device.settings.get("sleep_mode"): entities.append((sensor_id, description)) if not entities: @@ -236,7 +236,7 @@ def async_setup_entry_rest( async_add_entities( [ - sensor_class(wrapper, sensor_id, description) + sensor_class(coordinator, sensor_id, description) for sensor_id, description in entities ] ) @@ -270,7 +270,7 @@ class RpcEntityDescription(EntityDescription, RpcEntityRequiredKeysMixin): available: Callable[[dict], bool] | None = None removal_condition: Callable[[dict, dict, str], bool] | None = None extra_state_attributes: Callable[[dict, dict], dict | None] | None = None - use_polling_wrapper: bool = False + use_polling_coordinator: bool = False supported: Callable = lambda _: False @@ -282,32 +282,32 @@ class RestEntityDescription(EntityDescription): extra_state_attributes: Callable[[dict], dict | None] | None = None -class ShellyBlockEntity(entity.Entity): +class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): """Helper class to represent a block entity.""" - def __init__(self, wrapper: BlockDeviceWrapper, block: Block) -> None: + def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None: """Initialize Shelly entity.""" - self.wrapper = wrapper + super().__init__(coordinator) self.block = block - self._attr_name = get_block_entity_name(wrapper.device, block) + self._attr_name = get_block_entity_name(coordinator.device, block) self._attr_should_poll = False self._attr_device_info = DeviceInfo( - connections={(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)} + connections={(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)} ) - self._attr_unique_id = f"{wrapper.mac}-{block.description}" + self._attr_unique_id = f"{coordinator.mac}-{block.description}" @property def available(self) -> bool: """Available.""" - return self.wrapper.last_update_success + return self.coordinator.last_update_success async def async_added_to_hass(self) -> None: """When entity is added to HASS.""" - self.async_on_remove(self.wrapper.async_add_listener(self._update_callback)) + self.async_on_remove(self.coordinator.async_add_listener(self._update_callback)) async def async_update(self) -> None: """Update entity with latest info.""" - await self.wrapper.async_request_refresh() + await self.coordinator.async_request_refresh() @callback def _update_callback(self) -> None: @@ -327,7 +327,7 @@ class ShellyBlockEntity(entity.Entity): kwargs, repr(err), ) - self.wrapper.last_update_success = False + self.coordinator.last_update_success = False return None @@ -336,36 +336,36 @@ class ShellyRpcEntity(entity.Entity): def __init__( self, - wrapper: RpcDeviceWrapper | RpcPollingWrapper, + coordinator: ShellyRpcCoordinator | ShellyRpcPollingCoordinator, key: str, ) -> None: """Initialize Shelly entity.""" - self.wrapper = wrapper + self.coordinator = coordinator self.key = key self._attr_should_poll = False self._attr_device_info = { - "connections": {(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)} + "connections": {(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)} } - self._attr_unique_id = f"{wrapper.mac}-{key}" - self._attr_name = get_rpc_entity_name(wrapper.device, key) + self._attr_unique_id = f"{coordinator.mac}-{key}" + self._attr_name = get_rpc_entity_name(coordinator.device, key) @property def available(self) -> bool: """Available.""" - return self.wrapper.device.connected + return self.coordinator.device.connected @property def status(self) -> dict: """Device status by entity key.""" - return cast(dict, self.wrapper.device.status[self.key]) + return cast(dict, self.coordinator.device.status[self.key]) async def async_added_to_hass(self) -> None: """When entity is added to HASS.""" - self.async_on_remove(self.wrapper.async_add_listener(self._update_callback)) + self.async_on_remove(self.coordinator.async_add_listener(self._update_callback)) async def async_update(self) -> None: """Update entity with latest info.""" - await self.wrapper.async_request_refresh() + await self.coordinator.async_request_refresh() @callback def _update_callback(self) -> None: @@ -382,7 +382,7 @@ class ShellyRpcEntity(entity.Entity): ) try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - return await self.wrapper.device.call_rpc(method, params) + return await self.coordinator.device.call_rpc(method, params) except asyncio.TimeoutError as err: LOGGER.error( "Call RPC for entity %s failed, method: %s, params: %s, error: %s", @@ -391,7 +391,7 @@ class ShellyRpcEntity(entity.Entity): params, repr(err), ) - self.wrapper.last_update_success = False + self.coordinator.last_update_success = False return None @@ -402,18 +402,20 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): def __init__( self, - wrapper: BlockDeviceWrapper, + coordinator: ShellyBlockCoordinator, block: Block, attribute: str, description: BlockEntityDescription, ) -> None: """Initialize sensor.""" - super().__init__(wrapper, block) + super().__init__(coordinator, block) self.attribute = attribute self.entity_description = description self._attr_unique_id: str = f"{super().unique_id}-{self.attribute}" - self._attr_name = get_block_entity_name(wrapper.device, block, description.name) + self._attr_name = get_block_entity_name( + coordinator.device, block, description.name + ) @property def attribute_value(self) -> StateType: @@ -442,40 +444,42 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): return self.entity_description.extra_state_attributes(self.block) -class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): +class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]): """Class to load info from REST.""" entity_description: RestEntityDescription def __init__( self, - wrapper: BlockDeviceWrapper, + coordinator: ShellyBlockCoordinator, attribute: str, description: RestEntityDescription, ) -> None: """Initialize sensor.""" - super().__init__(wrapper) - self.wrapper = wrapper + super().__init__(coordinator) + self.block_coordinator = coordinator self.attribute = attribute self.entity_description = description - self._attr_name = get_block_entity_name(wrapper.device, None, description.name) - self._attr_unique_id = f"{wrapper.mac}-{attribute}" + self._attr_name = get_block_entity_name( + coordinator.device, None, description.name + ) + self._attr_unique_id = f"{coordinator.mac}-{attribute}" self._attr_device_info = DeviceInfo( - connections={(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)} + connections={(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)} ) self._last_value = None @property def available(self) -> bool: """Available.""" - return self.wrapper.last_update_success + return self.block_coordinator.last_update_success @property def attribute_value(self) -> StateType: """Value of sensor.""" if callable(self.entity_description.value): self._last_value = self.entity_description.value( - self.wrapper.device.status, self._last_value + self.block_coordinator.device.status, self._last_value ) return self._last_value @@ -486,7 +490,7 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): return None return self.entity_description.extra_state_attributes( - self.wrapper.device.status + self.block_coordinator.device.status ) @@ -497,18 +501,18 @@ class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity): def __init__( self, - wrapper: RpcDeviceWrapper, + coordinator: ShellyRpcCoordinator, key: str, attribute: str, description: RpcEntityDescription, ) -> None: """Initialize sensor.""" - super().__init__(wrapper, key) + super().__init__(coordinator, key) self.attribute = attribute self.entity_description = description self._attr_unique_id = f"{super().unique_id}-{attribute}" - self._attr_name = get_rpc_entity_name(wrapper.device, key, description.name) + self._attr_name = get_rpc_entity_name(coordinator.device, key, description.name) self._last_value = None @property @@ -516,13 +520,13 @@ class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity): """Value of sensor.""" if callable(self.entity_description.value): self._last_value = self.entity_description.value( - self.wrapper.device.status[self.key].get( + self.coordinator.device.status[self.key].get( self.entity_description.sub_key ), self._last_value, ) else: - self._last_value = self.wrapper.device.status[self.key][ + self._last_value = self.coordinator.device.status[self.key][ self.entity_description.sub_key ] @@ -537,7 +541,7 @@ class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity): return available return self.entity_description.available( - self.wrapper.device.status[self.key][self.entity_description.sub_key] + self.coordinator.device.status[self.key][self.entity_description.sub_key] ) @property @@ -546,11 +550,11 @@ class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity): if self.entity_description.extra_state_attributes is None: return None - assert self.wrapper.device.shelly + assert self.coordinator.device.shelly return self.entity_description.extra_state_attributes( - self.wrapper.device.status[self.key][self.entity_description.sub_key], - self.wrapper.device.shelly, + self.coordinator.device.status[self.key][self.entity_description.sub_key], + self.coordinator.device.shelly, ) @@ -560,7 +564,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti # pylint: disable=super-init-not-called def __init__( self, - wrapper: BlockDeviceWrapper, + coordinator: ShellyBlockCoordinator, block: Block | None, attribute: str, description: BlockEntityDescription, @@ -570,20 +574,22 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti """Initialize the sleeping sensor.""" self.sensors = sensors self.last_state: StateType = None - self.wrapper = wrapper + self.coordinator = coordinator self.attribute = attribute self.block: Block | None = block # type: ignore[assignment] self.entity_description = description self._attr_should_poll = False self._attr_device_info = DeviceInfo( - connections={(device_registry.CONNECTION_NETWORK_MAC, wrapper.mac)} + connections={(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)} ) if block is not None: - self._attr_unique_id = f"{self.wrapper.mac}-{block.description}-{attribute}" + self._attr_unique_id = ( + f"{self.coordinator.mac}-{block.description}-{attribute}" + ) self._attr_name = get_block_entity_name( - self.wrapper.device, block, self.entity_description.name + self.coordinator.device, block, self.entity_description.name ) elif entry is not None: self._attr_unique_id = entry.unique_id @@ -603,7 +609,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti """Handle device update.""" if ( self.block is not None - or not self.wrapper.device.initialized + or not self.coordinator.device.initialized or self.sensors is None ): super()._update_callback() @@ -611,9 +617,9 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti _, entity_block, entity_sensor = self._attr_unique_id.split("-") - assert self.wrapper.device.blocks + assert self.coordinator.device.blocks - for block in self.wrapper.device.blocks: + for block in self.coordinator.device.blocks: if block.description != entity_block: continue diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index b75e1ad2377..0d0ab5dd029 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -25,7 +25,6 @@ from homeassistant.util.color import ( color_temperature_mired_to_kelvin, ) -from . import BlockDeviceWrapper, RpcDeviceWrapper from .const import ( BLOCK, DATA_CONFIG_ENTRY, @@ -44,6 +43,7 @@ from .const import ( SHBLB_1_RGB_EFFECTS, STANDARD_RGB_EFFECTS, ) +from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator from .entity import ShellyBlockEntity, ShellyRpcEntity from .utils import ( async_remove_shelly_entity, @@ -77,28 +77,28 @@ def async_setup_block_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for block device.""" - wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK] + coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK] blocks = [] - assert wrapper.device.blocks - for block in wrapper.device.blocks: + assert coordinator.device.blocks + for block in coordinator.device.blocks: if block.type == "light": blocks.append(block) elif block.type == "relay": if not is_block_channel_type_light( - wrapper.device.settings, int(block.channel) + coordinator.device.settings, int(block.channel) ): continue blocks.append(block) - assert wrapper.device.shelly - unique_id = f"{wrapper.mac}-{block.type}_{block.channel}" + assert coordinator.device.shelly + unique_id = f"{coordinator.mac}-{block.type}_{block.channel}" async_remove_shelly_entity(hass, "switch", unique_id) if not blocks: return - async_add_entities(BlockShellyLight(wrapper, block) for block in blocks) + async_add_entities(BlockShellyLight(coordinator, block) for block in blocks) @callback @@ -108,22 +108,22 @@ def async_setup_rpc_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for RPC device.""" - wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC] - switch_key_ids = get_rpc_key_ids(wrapper.device.status, "switch") + coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC] + switch_key_ids = get_rpc_key_ids(coordinator.device.status, "switch") switch_ids = [] for id_ in switch_key_ids: - if not is_rpc_channel_type_light(wrapper.device.config, id_): + if not is_rpc_channel_type_light(coordinator.device.config, id_): continue switch_ids.append(id_) - unique_id = f"{wrapper.mac}-switch:{id_}" + unique_id = f"{coordinator.mac}-switch:{id_}" async_remove_shelly_entity(hass, "switch", unique_id) if not switch_ids: return - async_add_entities(RpcShellyLight(wrapper, id_) for id_ in switch_ids) + async_add_entities(RpcShellyLight(coordinator, id_) for id_ in switch_ids) class BlockShellyLight(ShellyBlockEntity, LightEntity): @@ -131,9 +131,9 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): _attr_supported_color_modes: set[str] - def __init__(self, wrapper: BlockDeviceWrapper, block: Block) -> None: + def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None: """Initialize light.""" - super().__init__(wrapper, block) + super().__init__(coordinator, block) self.control_result: dict[str, Any] | None = None self._attr_supported_color_modes = set() self._attr_min_mireds = MIRED_MIN_VALUE @@ -144,7 +144,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"): self._attr_max_mireds = MIRED_MAX_VALUE_COLOR self._min_kelvin = KELVIN_MIN_VALUE_COLOR - if wrapper.model in RGBW_MODELS: + if coordinator.model in RGBW_MODELS: self._attr_supported_color_modes.add(ColorMode.RGBW) else: self._attr_supported_color_modes.add(ColorMode.RGB) @@ -161,8 +161,8 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): if hasattr(block, "effect"): self._attr_supported_features |= LightEntityFeature.EFFECT - if wrapper.model in MODELS_SUPPORTING_LIGHT_TRANSITION: - match = FIRMWARE_PATTERN.search(wrapper.device.settings.get("fw", "")) + if coordinator.model in MODELS_SUPPORTING_LIGHT_TRANSITION: + match = FIRMWARE_PATTERN.search(coordinator.device.settings.get("fw", "")) if ( match is not None and int(match[0]) >= LIGHT_TRANSITION_MIN_FIRMWARE_DATE @@ -215,7 +215,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): def color_mode(self) -> ColorMode: """Return the color mode of the light.""" if self.mode == "color": - if self.wrapper.model in RGBW_MODELS: + if self.coordinator.model in RGBW_MODELS: return ColorMode.RGBW return ColorMode.RGB @@ -268,7 +268,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): if not self.supported_features & LightEntityFeature.EFFECT: return None - if self.wrapper.model == "SHBLB-1": + if self.coordinator.model == "SHBLB-1": return list(SHBLB_1_RGB_EFFECTS.values()) return list(STANDARD_RGB_EFFECTS.values()) @@ -284,7 +284,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): else: effect_index = self.block.effect - if self.wrapper.model == "SHBLB-1": + if self.coordinator.model == "SHBLB-1": return SHBLB_1_RGB_EFFECTS[effect_index] return STANDARD_RGB_EFFECTS[effect_index] @@ -334,7 +334,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): if ATTR_EFFECT in kwargs and ATTR_COLOR_TEMP not in kwargs: # Color effect change - used only in color mode, switch device mode to color set_mode = "color" - if self.wrapper.model == "SHBLB-1": + if self.coordinator.model == "SHBLB-1": effect_dict = SHBLB_1_RGB_EFFECTS else: effect_dict = STANDARD_RGB_EFFECTS @@ -346,13 +346,13 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): LOGGER.error( "Effect '%s' not supported by device %s", kwargs[ATTR_EFFECT], - self.wrapper.model, + self.coordinator.model, ) if ( set_mode and set_mode != self.mode - and self.wrapper.model in DUAL_MODE_LIGHT_MODELS + and self.coordinator.model in DUAL_MODE_LIGHT_MODELS ): params["mode"] = set_mode @@ -385,15 +385,15 @@ class RpcShellyLight(ShellyRpcEntity, LightEntity): _attr_color_mode = ColorMode.ONOFF _attr_supported_color_modes = {ColorMode.ONOFF} - def __init__(self, wrapper: RpcDeviceWrapper, id_: int) -> None: + def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None: """Initialize light.""" - super().__init__(wrapper, f"switch:{id_}") + super().__init__(coordinator, f"switch:{id_}") self._id = id_ @property def is_on(self) -> bool: """If light is on.""" - return bool(self.wrapper.device.status[self.key]["output"]) + return bool(self.coordinator.device.status[self.key]["output"]) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on light.""" diff --git a/homeassistant/components/shelly/logbook.py b/homeassistant/components/shelly/logbook.py index 337b40fff04..c5da9d579f8 100644 --- a/homeassistant/components/shelly/logbook.py +++ b/homeassistant/components/shelly/logbook.py @@ -8,7 +8,7 @@ from homeassistant.const import ATTR_DEVICE_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.typing import EventType -from . import get_block_device_wrapper, get_rpc_device_wrapper +from . import get_block_device_coordinator, get_rpc_device_coordinator from .const import ( ATTR_CHANNEL, ATTR_CLICK_TYPE, @@ -37,15 +37,15 @@ def async_describe_events( input_name = f"{event.data[ATTR_DEVICE]} channel {channel}" if click_type in RPC_INPUTS_EVENTS_TYPES: - rpc_wrapper = get_rpc_device_wrapper(hass, device_id) - if rpc_wrapper and rpc_wrapper.device.initialized: + rpc_coordinator = get_rpc_device_coordinator(hass, device_id) + if rpc_coordinator and rpc_coordinator.device.initialized: key = f"input:{channel-1}" - input_name = get_rpc_entity_name(rpc_wrapper.device, key) + input_name = get_rpc_entity_name(rpc_coordinator.device, key) elif click_type in BLOCK_INPUTS_EVENTS_TYPES: - block_wrapper = get_block_device_wrapper(hass, device_id) - if block_wrapper and block_wrapper.device.initialized: - device_name = get_block_device_name(block_wrapper.device) + block_coordinator = get_block_device_coordinator(hass, device_id) + if block_coordinator and block_coordinator.device.initialized: + device_name = get_block_device_name(block_coordinator.device) input_name = f"{device_name} channel {channel}" return { diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index 6658daf674f..eb61fccb9ef 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -119,7 +119,7 @@ class BlockSleepingNumber(ShellySleepingBlockAttributeEntity, NumberEntity): LOGGER.debug("Setting state for entity %s, state: %s", self.name, params) try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - return await self.wrapper.device.http_request("get", path, params) + return await self.coordinator.device.http_request("get", path, params) except (asyncio.TimeoutError, OSError) as err: LOGGER.error( "Setting state for entity %s failed, state: %s, error: %s", diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index bc55aa3e865..0e507b59431 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -32,8 +32,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.typing import StateType -from . import BlockDeviceWrapper from .const import CONF_SLEEP_PERIOD, SHAIR_MAX_WORK_HOURS +from .coordinator import ShellyBlockCoordinator from .entity import ( BlockEntityDescription, RestEntityDescription, @@ -355,7 +355,7 @@ RPC_SENSORS: Final = { state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, - use_polling_wrapper=True, + use_polling_coordinator=True, ), "temperature_0": RpcSensorDescription( key="temperature:0", @@ -376,7 +376,7 @@ RPC_SENSORS: Final = { state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, - use_polling_wrapper=True, + use_polling_coordinator=True, ), "uptime": RpcSensorDescription( key="sys", @@ -386,7 +386,7 @@ RPC_SENSORS: Final = { device_class=SensorDeviceClass.TIMESTAMP, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, - use_polling_wrapper=True, + use_polling_coordinator=True, ), "humidity_0": RpcSensorDescription( key="humidity:0", @@ -465,13 +465,13 @@ class BlockSensor(ShellyBlockAttributeEntity, SensorEntity): def __init__( self, - wrapper: BlockDeviceWrapper, + coordinator: ShellyBlockCoordinator, block: Block, attribute: str, description: BlockSensorDescription, ) -> None: """Initialize sensor.""" - super().__init__(wrapper, block, attribute, description) + super().__init__(coordinator, block, attribute, description) self._attr_native_unit_of_measurement = description.native_unit_of_measurement if unit_fn := description.unit_fn: @@ -512,7 +512,7 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity): def __init__( self, - wrapper: BlockDeviceWrapper, + coordinator: ShellyBlockCoordinator, block: Block | None, attribute: str, description: BlockSensorDescription, @@ -520,7 +520,7 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity): sensors: Mapping[tuple[str, str], BlockSensorDescription] | None = None, ) -> None: """Initialize the sleeping sensor.""" - super().__init__(wrapper, block, attribute, description, entry, sensors) + super().__init__(coordinator, block, attribute, description, entry, sensors) self._attr_native_unit_of_measurement = description.native_unit_of_measurement if block and (unit_fn := description.unit_fn): diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index d65568d0a2a..16f3ca9c163 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -10,8 +10,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BlockDeviceWrapper, RpcDeviceWrapper from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC +from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator from .entity import ShellyBlockEntity, ShellyRpcEntity from .utils import ( async_remove_shelly_entity, @@ -41,31 +41,31 @@ def async_setup_block_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for block device.""" - wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK] + coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK] # In roller mode the relay blocks exist but do not contain required info if ( - wrapper.model in ["SHSW-21", "SHSW-25"] - and wrapper.device.settings["mode"] != "relay" + coordinator.model in ["SHSW-21", "SHSW-25"] + and coordinator.device.settings["mode"] != "relay" ): return relay_blocks = [] - assert wrapper.device.blocks - for block in wrapper.device.blocks: + assert coordinator.device.blocks + for block in coordinator.device.blocks: if block.type != "relay" or is_block_channel_type_light( - wrapper.device.settings, int(block.channel) + coordinator.device.settings, int(block.channel) ): continue relay_blocks.append(block) - unique_id = f"{wrapper.mac}-{block.type}_{block.channel}" + unique_id = f"{coordinator.mac}-{block.type}_{block.channel}" async_remove_shelly_entity(hass, "light", unique_id) if not relay_blocks: return - async_add_entities(BlockRelaySwitch(wrapper, block) for block in relay_blocks) + async_add_entities(BlockRelaySwitch(coordinator, block) for block in relay_blocks) @callback @@ -75,31 +75,31 @@ def async_setup_rpc_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for RPC device.""" - wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC] + coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC] - switch_key_ids = get_rpc_key_ids(wrapper.device.status, "switch") + switch_key_ids = get_rpc_key_ids(coordinator.device.status, "switch") switch_ids = [] for id_ in switch_key_ids: - if is_rpc_channel_type_light(wrapper.device.config, id_): + if is_rpc_channel_type_light(coordinator.device.config, id_): continue switch_ids.append(id_) - unique_id = f"{wrapper.mac}-switch:{id_}" + unique_id = f"{coordinator.mac}-switch:{id_}" async_remove_shelly_entity(hass, "light", unique_id) if not switch_ids: return - async_add_entities(RpcRelaySwitch(wrapper, id_) for id_ in switch_ids) + async_add_entities(RpcRelaySwitch(coordinator, id_) for id_ in switch_ids) class BlockRelaySwitch(ShellyBlockEntity, SwitchEntity): """Entity that controls a relay on Block based Shelly devices.""" - def __init__(self, wrapper: BlockDeviceWrapper, block: Block) -> None: + def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None: """Initialize relay switch.""" - super().__init__(wrapper, block) + super().__init__(coordinator, block) self.control_result: dict[str, Any] | None = None @property @@ -130,15 +130,15 @@ class BlockRelaySwitch(ShellyBlockEntity, SwitchEntity): class RpcRelaySwitch(ShellyRpcEntity, SwitchEntity): """Entity that controls a relay on RPC based Shelly devices.""" - def __init__(self, wrapper: RpcDeviceWrapper, id_: int) -> None: + def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None: """Initialize relay switch.""" - super().__init__(wrapper, f"switch:{id_}") + super().__init__(coordinator, f"switch:{id_}") self._id = id_ @property def is_on(self) -> bool: """If switch is on.""" - return bool(self.wrapper.device.status[self.key]["output"]) + return bool(self.coordinator.device.status[self.key]["output"]) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on relay.""" diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index ac4b737a2cc..d9048594a04 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -17,8 +17,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BlockDeviceWrapper, RpcDeviceWrapper from .const import BLOCK, CONF_SLEEP_PERIOD, DATA_CONFIG_ENTRY, DOMAIN +from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator from .entity import ( RestEntityDescription, RpcEntityDescription, @@ -67,7 +67,7 @@ REST_UPDATES: Final = { name="Firmware Update", key="fwupdate", latest_version=lambda status: status["update"]["new_version"], - install=lambda wrapper: wrapper.async_trigger_ota_update(), + install=lambda coordinator: coordinator.async_trigger_ota_update(), device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, @@ -76,7 +76,7 @@ REST_UPDATES: Final = { name="Beta Firmware Update", key="fwupdate", latest_version=lambda status: status["update"].get("beta_version"), - install=lambda wrapper: wrapper.async_trigger_ota_update(beta=True), + install=lambda coordinator: coordinator.async_trigger_ota_update(beta=True), device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, @@ -91,7 +91,7 @@ RPC_UPDATES: Final = { latest_version=lambda status: status.get("stable", {"version": None})[ "version" ], - install=lambda wrapper: wrapper.async_trigger_ota_update(), + install=lambda coordinator: coordinator.async_trigger_ota_update(), device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, @@ -101,7 +101,7 @@ RPC_UPDATES: Final = { key="sys", sub_key="available_updates", latest_version=lambda status: status.get("beta", {"version": None})["version"], - install=lambda wrapper: wrapper.async_trigger_ota_update(beta=True), + install=lambda coordinator: coordinator.async_trigger_ota_update(beta=True), device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, @@ -140,18 +140,18 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity): def __init__( self, - wrapper: BlockDeviceWrapper, + block_coordinator: ShellyBlockCoordinator, attribute: str, description: RestEntityDescription, ) -> None: """Initialize update entity.""" - super().__init__(wrapper, attribute, description) + super().__init__(block_coordinator, attribute, description) self._in_progress_old_version: str | None = None @property def installed_version(self) -> str | None: """Version currently in use.""" - version = self.wrapper.device.status["update"]["old_version"] + version = self.block_coordinator.device.status["update"]["old_version"] if version is None: return None @@ -161,7 +161,7 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity): def latest_version(self) -> str | None: """Latest version available for install.""" new_version = self.entity_description.latest_version( - self.wrapper.device.status, + self.block_coordinator.device.status, ) if new_version not in (None, ""): return cast(str, new_version) @@ -177,12 +177,12 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity): self, version: str | None, backup: bool, **kwargs: Any ) -> None: """Install the latest firmware version.""" - config_entry = self.wrapper.entry - block_wrapper = self.hass.data[DOMAIN][DATA_CONFIG_ENTRY][ + config_entry = self.block_coordinator.entry + block_coordinator = self.hass.data[DOMAIN][DATA_CONFIG_ENTRY][ config_entry.entry_id ].get(BLOCK) self._in_progress_old_version = self.installed_version - await self.entity_description.install(block_wrapper) + await self.entity_description.install(block_coordinator) class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): @@ -195,28 +195,28 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): def __init__( self, - wrapper: RpcDeviceWrapper, + coordinator: ShellyRpcCoordinator, key: str, attribute: str, description: RpcEntityDescription, ) -> None: """Initialize update entity.""" - super().__init__(wrapper, key, attribute, description) + super().__init__(coordinator, key, attribute, description) self._in_progress_old_version: str | None = None @property def installed_version(self) -> str | None: """Version currently in use.""" - if self.wrapper.device.shelly is None: + if self.coordinator.device.shelly is None: return None - return cast(str, self.wrapper.device.shelly["ver"]) + return cast(str, self.coordinator.device.shelly["ver"]) @property def latest_version(self) -> str | None: """Latest version available for install.""" new_version = self.entity_description.latest_version( - self.wrapper.device.status[self.key][self.entity_description.sub_key], + self.coordinator.device.status[self.key][self.entity_description.sub_key], ) if new_version is not None: return cast(str, new_version) @@ -233,4 +233,4 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): ) -> None: """Install the latest firmware version.""" self._in_progress_old_version = self.installed_version - await self.entity_description.install(self.wrapper) + await self.entity_description.install(self.coordinator) From 312770dbac9565cfac4edbb5e715c70cecfab18f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 5 Oct 2022 08:57:36 -0400 Subject: [PATCH 182/985] Change Lidarr device name to entry title (#79630) --- homeassistant/components/lidarr/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lidarr/__init__.py b/homeassistant/components/lidarr/__init__.py index 6410e520b42..7fd3e799d88 100644 --- a/homeassistant/components/lidarr/__init__.py +++ b/homeassistant/components/lidarr/__init__.py @@ -80,6 +80,6 @@ class LidarrEntity(CoordinatorEntity[LidarrDataUpdateCoordinator]): entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, manufacturer=DEFAULT_NAME, - name=DEFAULT_NAME, + name=coordinator.config_entry.title, sw_version=coordinator.system_version, ) From 5d7756885be0fd044d86e60ec0d2639f9d114ea3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 5 Oct 2022 16:27:08 +0200 Subject: [PATCH 183/985] Normalize to kWh when handling WS energy/fossil_energy_consumption (#79649) * Normalize to kWh when handling WS energy/fossil_energy_consumption * Improve test --- homeassistant/components/energy/websocket_api.py | 2 ++ tests/components/energy/test_websocket_api.py | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index ad77308b410..7ba83cf15c9 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -13,6 +13,7 @@ from typing import Any, cast import voluptuous as vol from homeassistant.components import recorder, websocket_api +from homeassistant.const import ENERGY_KILO_WATT_HOUR from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, @@ -268,6 +269,7 @@ async def ws_get_fossil_energy_consumption( statistic_ids, "hour", True, + {"energy": ENERGY_KILO_WATT_HOUR}, ) def _combine_sum_statistics( diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index ab785291f91..343e814f3a8 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -814,25 +814,25 @@ async def test_fossil_energy_consumption(hass, hass_ws_client, recorder_mock): "start": period1, "last_reset": None, "state": 0, - "sum": 20, + "sum": 20000, }, { "start": period2, "last_reset": None, "state": 1, - "sum": 30, + "sum": 30000, }, { "start": period3, "last_reset": None, "state": 2, - "sum": 40, + "sum": 40000, }, { "start": period4, "last_reset": None, "state": 3, - "sum": 50, + "sum": 50000, }, ) external_energy_metadata_2 = { @@ -841,7 +841,7 @@ async def test_fossil_energy_consumption(hass, hass_ws_client, recorder_mock): "name": "Total imported energy", "source": "test", "statistic_id": "test:total_energy_import_tariff_2", - "unit_of_measurement": "kWh", + "unit_of_measurement": "Wh", } external_co2_statistics = ( { From 0eb1101de84b6042380a0d2220da182493b2d206 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Wed, 5 Oct 2022 17:03:23 +0200 Subject: [PATCH 184/985] Velbus split of entity in its own file (#79653) * Velbus split of entity in its own file * Update coveragerc --- .coveragerc | 1 + homeassistant/components/velbus/__init__.py | 31 ---------------- .../components/velbus/binary_sensor.py | 2 +- homeassistant/components/velbus/button.py | 2 +- homeassistant/components/velbus/climate.py | 2 +- homeassistant/components/velbus/cover.py | 2 +- homeassistant/components/velbus/entity.py | 37 +++++++++++++++++++ homeassistant/components/velbus/light.py | 2 +- homeassistant/components/velbus/sensor.py | 2 +- homeassistant/components/velbus/switch.py | 2 +- 10 files changed, 45 insertions(+), 38 deletions(-) create mode 100644 homeassistant/components/velbus/entity.py diff --git a/.coveragerc b/.coveragerc index 79ecbb3ece5..dd01fa11e84 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1426,6 +1426,7 @@ omit = homeassistant/components/velbus/const.py homeassistant/components/velbus/cover.py homeassistant/components/velbus/diagnostics.py + homeassistant/components/velbus/entity.py homeassistant/components/velbus/light.py homeassistant/components/velbus/sensor.py homeassistant/components/velbus/switch.py diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index eeeee2f9716..67a4652d5e5 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging -from velbusaio.channels import Channel as VelbusChannel from velbusaio.controller import Velbus import voluptuous as vol @@ -13,7 +12,6 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntry -from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( CONF_INTERFACE, @@ -146,32 +144,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.services.async_remove(DOMAIN, SERVICE_SYNC) hass.services.async_remove(DOMAIN, SERVICE_SET_MEMO_TEXT) return unload_ok - - -class VelbusEntity(Entity): - """Representation of a Velbus entity.""" - - _attr_should_poll: bool = False - - def __init__(self, channel: VelbusChannel) -> None: - """Initialize a Velbus entity.""" - self._channel = channel - self._attr_name = channel.get_name() - self._attr_device_info = DeviceInfo( - identifiers={ - (DOMAIN, str(channel.get_module_address())), - }, - manufacturer="Velleman", - model=channel.get_module_type_name(), - name=channel.get_full_name(), - sw_version=channel.get_module_sw_version(), - ) - serial = channel.get_module_serial() or str(channel.get_module_address()) - self._attr_unique_id = f"{serial}-{channel.get_channel_number()}" - - async def async_added_to_hass(self) -> None: - """Add listener for state changes.""" - self._channel.on_status_update(self._on_update) - - async def _on_update(self) -> None: - self.async_write_ha_state() diff --git a/homeassistant/components/velbus/binary_sensor.py b/homeassistant/components/velbus/binary_sensor.py index 8c67520dd9a..ef0cef938b1 100644 --- a/homeassistant/components/velbus/binary_sensor.py +++ b/homeassistant/components/velbus/binary_sensor.py @@ -6,8 +6,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import VelbusEntity from .const import DOMAIN +from .entity import VelbusEntity async def async_setup_entry( diff --git a/homeassistant/components/velbus/button.py b/homeassistant/components/velbus/button.py index 189cfb495e4..5f76f7bba98 100644 --- a/homeassistant/components/velbus/button.py +++ b/homeassistant/components/velbus/button.py @@ -12,8 +12,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import VelbusEntity from .const import DOMAIN +from .entity import VelbusEntity async def async_setup_entry( diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index a6549f0262c..76eb3e30fa0 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -15,8 +15,8 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import VelbusEntity from .const import DOMAIN, PRESET_MODES +from .entity import VelbusEntity async def async_setup_entry( diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py index 6bd1629d3a3..3ac66147bb6 100644 --- a/homeassistant/components/velbus/cover.py +++ b/homeassistant/components/velbus/cover.py @@ -14,8 +14,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import VelbusEntity from .const import DOMAIN +from .entity import VelbusEntity async def async_setup_entry( diff --git a/homeassistant/components/velbus/entity.py b/homeassistant/components/velbus/entity.py new file mode 100644 index 00000000000..13ecb7febab --- /dev/null +++ b/homeassistant/components/velbus/entity.py @@ -0,0 +1,37 @@ +"""Support for Velbus devices.""" +from __future__ import annotations + +from velbusaio.channels import Channel as VelbusChannel + +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import DOMAIN + + +class VelbusEntity(Entity): + """Representation of a Velbus entity.""" + + _attr_should_poll: bool = False + + def __init__(self, channel: VelbusChannel) -> None: + """Initialize a Velbus entity.""" + self._channel = channel + self._attr_name = channel.get_name() + self._attr_device_info = DeviceInfo( + identifiers={ + (DOMAIN, str(channel.get_module_address())), + }, + manufacturer="Velleman", + model=channel.get_module_type_name(), + name=channel.get_full_name(), + sw_version=channel.get_module_sw_version(), + ) + serial = channel.get_module_serial() or str(channel.get_module_address()) + self._attr_unique_id = f"{serial}-{channel.get_channel_number()}" + + async def async_added_to_hass(self) -> None: + """Add listener for state changes.""" + self._channel.on_status_update(self._on_update) + + async def _on_update(self) -> None: + self.async_write_ha_state() diff --git a/homeassistant/components/velbus/light.py b/homeassistant/components/velbus/light.py index f562e250892..b5b106b6f9f 100644 --- a/homeassistant/components/velbus/light.py +++ b/homeassistant/components/velbus/light.py @@ -24,8 +24,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import VelbusEntity from .const import DOMAIN +from .entity import VelbusEntity async def async_setup_entry( diff --git a/homeassistant/components/velbus/sensor.py b/homeassistant/components/velbus/sensor.py index a0bd9b6c173..0805ae2699a 100644 --- a/homeassistant/components/velbus/sensor.py +++ b/homeassistant/components/velbus/sensor.py @@ -12,8 +12,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import VelbusEntity from .const import DOMAIN +from .entity import VelbusEntity async def async_setup_entry( diff --git a/homeassistant/components/velbus/switch.py b/homeassistant/components/velbus/switch.py index c3c4c8a5863..6de8373d3fc 100644 --- a/homeassistant/components/velbus/switch.py +++ b/homeassistant/components/velbus/switch.py @@ -8,8 +8,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import VelbusEntity from .const import DOMAIN +from .entity import VelbusEntity async def async_setup_entry( From 41d2ab5b375564ccbc836736fa8896a758cffc2e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 5 Oct 2022 17:38:32 +0200 Subject: [PATCH 185/985] Update frontend to 20221005.0 (#79656) --- 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 61fc1629793..e6d5f63272d 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==20221004.0"], + "requirements": ["home-assistant-frontend==20221005.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index afda4684b34..2f637ba61f1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.24.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20221004.0 +home-assistant-frontend==20221005.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index a6c341b41f7..715f4e5f7a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,7 +868,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221004.0 +home-assistant-frontend==20221005.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af1e4372d52..44b3487850a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,7 +648,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221004.0 +home-assistant-frontend==20221005.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 5674295b3cd50536a18f08c6baa72d651b5dbd46 Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Wed, 5 Oct 2022 23:18:41 +0300 Subject: [PATCH 186/985] Add clicksend to strict typing (#79544) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- .strict-typing | 1 + homeassistant/components/clicksend/notify.py | 27 +++++++++++++------- mypy.ini | 10 ++++++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/.strict-typing b/.strict-typing index 2390ab8373d..635356f4950 100644 --- a/.strict-typing +++ b/.strict-typing @@ -78,6 +78,7 @@ homeassistant.components.camera.* homeassistant.components.canary.* homeassistant.components.cover.* homeassistant.components.clickatell.* +homeassistant.components.clicksend.* homeassistant.components.cpuspeed.* homeassistant.components.crownstone.* homeassistant.components.deconz.* diff --git a/homeassistant/components/clicksend/notify.py b/homeassistant/components/clicksend/notify.py index ec6bed3c55d..36ac21d8dd3 100644 --- a/homeassistant/components/clicksend/notify.py +++ b/homeassistant/components/clicksend/notify.py @@ -1,7 +1,10 @@ """Clicksend platform for notify component.""" +from __future__ import annotations + from http import HTTPStatus import json import logging +from typing import Any import requests import voluptuous as vol @@ -14,7 +17,9 @@ from homeassistant.const import ( CONF_USERNAME, CONTENT_TYPE_JSON, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -41,7 +46,11 @@ PLATFORM_SCHEMA = vol.Schema( ) -def get_service(hass, config, discovery_info=None): +def get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> ClicksendNotificationService | None: """Get the ClickSend notification service.""" if not _authenticate(config): _LOGGER.error("You are not authorized to access ClickSend") @@ -52,16 +61,16 @@ def get_service(hass, config, discovery_info=None): class ClicksendNotificationService(BaseNotificationService): """Implementation of a notification service for the ClickSend service.""" - def __init__(self, config): + def __init__(self, config: ConfigType) -> None: """Initialize the service.""" - self.username = config[CONF_USERNAME] - self.api_key = config[CONF_API_KEY] - self.recipients = config[CONF_RECIPIENT] - self.sender = config[CONF_SENDER] + self.username: str = config[CONF_USERNAME] + self.api_key: str = config[CONF_API_KEY] + self.recipients: list[str] = config[CONF_RECIPIENT] + self.sender: str = config[CONF_SENDER] - def send_message(self, message="", **kwargs): + def send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message to a user.""" - data = {"messages": []} + data: dict[str, Any] = {"messages": []} for recipient in self.recipients: data["messages"].append( { @@ -91,7 +100,7 @@ class ClicksendNotificationService(BaseNotificationService): ) -def _authenticate(config): +def _authenticate(config: ConfigType) -> bool: """Authenticate with ClickSend.""" api_url = f"{BASE_API_URL}/account" resp = requests.get( diff --git a/mypy.ini b/mypy.ini index 4aa0b5cbeb3..267e856aa09 100644 --- a/mypy.ini +++ b/mypy.ini @@ -532,6 +532,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.clicksend.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.cpuspeed.*] check_untyped_defs = true disallow_incomplete_defs = true From 558b327928d5ab08d8e97262dfbdbc13dc73056f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 6 Oct 2022 00:31:54 +0000 Subject: [PATCH 187/985] [ci skip] Translation update --- .../accuweather/translations/sensor.pl.json | 6 ++--- .../airthings_ble/translations/pl.json | 7 ++--- .../airthings_ble/translations/sv.json | 23 ++++++++++++++++ .../components/apcupsd/translations/sv.json | 26 +++++++++++++++++++ .../components/awair/translations/pl.json | 2 +- .../components/bayesian/translations/sv.json | 12 +++++++++ .../components/braviatv/translations/el.json | 11 +++++++- .../components/braviatv/translations/id.json | 11 +++++++- .../components/braviatv/translations/pl.json | 11 +++++++- .../components/braviatv/translations/sv.json | 11 +++++++- .../translations/sensor.pl.json | 19 +++++++++++--- .../components/hue/translations/pl.json | 14 +++++----- .../components/lidarr/translations/en.json | 10 +++++++ .../components/mikrotik/translations/pl.json | 4 +-- .../components/mikrotik/translations/sv.json | 10 ++++++- .../components/nest/translations/id.json | 2 +- .../components/nest/translations/pl.json | 2 +- .../components/octoprint/translations/pl.json | 2 +- .../components/octoprint/translations/sv.json | 6 +++++ .../rtsp_to_webrtc/translations/el.json | 9 +++++++ .../rtsp_to_webrtc/translations/fr.json | 9 +++++++ .../rtsp_to_webrtc/translations/id.json | 9 +++++++ .../rtsp_to_webrtc/translations/pl.json | 9 +++++++ .../rtsp_to_webrtc/translations/sv.json | 9 +++++++ .../components/sensor/translations/sv.json | 12 +++++++-- .../water_heater/translations/bg.json | 5 ++++ .../components/weather/translations/pl.json | 6 ++--- .../components/zha/translations/sv.json | 2 ++ 28 files changed, 226 insertions(+), 33 deletions(-) create mode 100644 homeassistant/components/airthings_ble/translations/sv.json create mode 100644 homeassistant/components/apcupsd/translations/sv.json create mode 100644 homeassistant/components/bayesian/translations/sv.json diff --git a/homeassistant/components/accuweather/translations/sensor.pl.json b/homeassistant/components/accuweather/translations/sensor.pl.json index cc7ba9b873c..68d7f8ac8ee 100644 --- a/homeassistant/components/accuweather/translations/sensor.pl.json +++ b/homeassistant/components/accuweather/translations/sensor.pl.json @@ -1,9 +1,9 @@ { "state": { "accuweather__pressure_tendency": { - "falling": "spada", - "rising": "ro\u015bnie", - "steady": "bez zmian" + "falling": "Spada", + "rising": "Ro\u015bnie", + "steady": "Bez zmian" } } } \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/pl.json b/homeassistant/components/airthings_ble/translations/pl.json index 2efd17b5aaf..550f9127635 100644 --- a/homeassistant/components/airthings_ble/translations/pl.json +++ b/homeassistant/components/airthings_ble/translations/pl.json @@ -2,14 +2,15 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "cannot_connect": "B\u0142\u0105d po\u0142\u0105czenia", - "no_devices_found": "Nie znaleziono \u017cadnych urz\u0105dze\u0144", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "Czy chcesz instalowa\u0107 {name}?" + "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, "user": { "data": { diff --git a/homeassistant/components/airthings_ble/translations/sv.json b/homeassistant/components/airthings_ble/translations/sv.json new file mode 100644 index 00000000000..fb65ad157f8 --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta.", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du konfigurera {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att konfigurera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/sv.json b/homeassistant/components/apcupsd/translations/sv.json new file mode 100644 index 00000000000..3cfc7b96260 --- /dev/null +++ b/homeassistant/components/apcupsd/translations/sv.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "no_status": "Ingen status rapporteras fr\u00e5n v\u00e4rd" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + }, + "description": "Ange den v\u00e4rd och port som apcupsd NIS anv\u00e4nds p\u00e5." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av APC UPS Daemon med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort APC UPS Daemon YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "APC UPS Daemon YAML-konfigurationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/pl.json b/homeassistant/components/awair/translations/pl.json index 7c9dba59e06..c42f9863b1c 100644 --- a/homeassistant/components/awair/translations/pl.json +++ b/homeassistant/components/awair/translations/pl.json @@ -29,7 +29,7 @@ "data": { "host": "Adres IP" }, - "description": "Post\u0119puj zgodnie z [tymi instrukcjami]( {url} ), aby dowiedzie\u0107 si\u0119, jak w\u0142\u0105czy\u0107 lokalny interfejs API Awair. \n\n Po zako\u0144czeniu kliknij Prze\u015blij." + "description": "Post\u0119puj zgodnie z [tymi instrukcjami]({url}), aby dowiedzie\u0107 si\u0119, jak w\u0142\u0105czy\u0107 lokalny interfejs API Awair. \n\n Po zako\u0144czeniu kliknij Zatwierd\u017a." }, "local_pick": { "data": { diff --git a/homeassistant/components/bayesian/translations/sv.json b/homeassistant/components/bayesian/translations/sv.json new file mode 100644 index 00000000000..59038d408d9 --- /dev/null +++ b/homeassistant/components/bayesian/translations/sv.json @@ -0,0 +1,12 @@ +{ + "issues": { + "manual_migration": { + "description": "Den Bayesianska integrationen uppdaterar nu ocks\u00e5 sannolikheten om den observerade `till_tillst\u00e5nd`, `\u00f6ver`, `under` eller `v\u00e4rde_mall` utv\u00e4rderas till `False` snarare \u00e4n bara `Sant`. S\u00e5 det \u00e4r inte l\u00e4ngre n\u00f6dv\u00e4ndigt att ha dubbla, kompletterande poster f\u00f6r varje bin\u00e4rt tillst\u00e5nd. Ta bort den speglade posten f\u00f6r ` {entity} `.", + "title": "Manuell YAML-korrigering kr\u00e4vs f\u00f6r Bayesian" + }, + "no_prob_given_false": { + "description": "I den Bayesianska integrationen \u00e4r 'prob_given_false' nu en obligatorisk konfigurationsvariabel eftersom det inte fanns n\u00e5gon matematisk motivering f\u00f6r det tidigare standardv\u00e4rdet. V\u00e4nligen l\u00e4gg till detta i din `configuration.yml` f\u00f6r `bayesian/ {entity} `. Dessa observationer kommer att ignoreras tills du g\u00f6r det.", + "title": "Manuellt YAML-till\u00e4gg kr\u00e4vs f\u00f6r Bayesian" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/el.json b/homeassistant/components/braviatv/translations/el.json index 4e479ddad74..fc3ba88c57e 100644 --- a/homeassistant/components/braviatv/translations/el.json +++ b/homeassistant/components/braviatv/translations/el.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "no_ip_control": "\u039f \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 IP \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03ae \u03b7 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9.", - "not_bravia_device": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 Bravia." + "not_bravia_device": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 Bravia.", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "reauth_unsuccessful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b1\u03bd\u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03be\u03b1\u03bd\u03ac." }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", @@ -23,6 +25,13 @@ "confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7;" }, + "reauth_confirm": { + "data": { + "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", + "use_psk": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 PSK" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc PIN \u03c0\u03bf\u03c5 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 Sony Bravia. \n\n \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae \u03c4\u03bf\u03c5 Home Assistant \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf: \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 - > \u0394\u03af\u03ba\u03c4\u03c5\u03bf - > \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 - > \u039a\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2. \n\n \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 PSK (Pre-Shared-Key) \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 PIN. \u03a4\u03bf PSK \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1 \u03bc\u03c5\u03c3\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c0\u03bf\u03c5 \u03bf\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2. \u0391\u03c5\u03c4\u03ae \u03b7 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9 \u03c9\u03c2 \u03c0\u03b9\u03bf \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf PSK \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b9\u03c2 \u03b5\u03be\u03ae\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2: \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 - > \u0394\u03af\u03ba\u03c4\u03c5\u03bf - > \u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03bf\u03b9\u03ba\u03b9\u03b1\u03ba\u03bf\u03cd \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 - > \u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 IP. \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03c0\u03bb\u03b1\u03af\u03c3\u03b9\u03bf \u00ab\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 PSK\u00bb \u03ba\u03b1\u03b9 \u03c0\u03bb\u03b7\u03ba\u03c4\u03c1\u03bf\u03bb\u03bf\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf PSK \u03c3\u03b1\u03c2 \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03c4\u03bf PIN." + }, "user": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" diff --git a/homeassistant/components/braviatv/translations/id.json b/homeassistant/components/braviatv/translations/id.json index 63b4353aefc..853dd7da29e 100644 --- a/homeassistant/components/braviatv/translations/id.json +++ b/homeassistant/components/braviatv/translations/id.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Perangkat sudah dikonfigurasi", "no_ip_control": "Kontrol IP dinonaktifkan di TV Anda atau TV tidak didukung.", - "not_bravia_device": "Perangkat ini bukan TV Bravia." + "not_bravia_device": "Perangkat ini bukan TV Bravia.", + "reauth_successful": "Autentikasi ulang berhasil", + "reauth_unsuccessful": "Autentikasi ulang tidak berhasil, hapus integrasi dan siapkan kembali." }, "error": { "cannot_connect": "Gagal terhubung", @@ -23,6 +25,13 @@ "confirm": { "description": "Ingin memulai penyiapan?" }, + "reauth_confirm": { + "data": { + "pin": "Kode PIN", + "use_psk": "Gunakan autentikasi PSK" + }, + "description": "Masukkan kode PIN yang ditampilkan di TV Sony Bravia. \n\nJika kode PIN tidak ditampilkan, Anda harus membatalkan pendaftaran Home Assistant di TV, buka: Pengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Batalkan pendaftaran perangkat jarak jauh.\n\nAnda bisa menggunakan PSK (Pre-Shared-Key) alih-alih menggunakan PIN. PSK merupakan kunci rahasia yang ditentukan pengguna untuk mengakses kontrol. Metode autentikasi ini disarankan karena lebih stabil. Untuk mengaktifkan PSK di TV Anda, buka Pengaturan -> Jaringan -> Penyiapan Jaringan Rumah -> Kontrol IP, lalu centang \u00abGunakan autentikasi PSK\u00bb dan masukkan PSK Anda, bukan PIN." + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/pl.json b/homeassistant/components/braviatv/translations/pl.json index 3795bea349e..adc3a67e603 100644 --- a/homeassistant/components/braviatv/translations/pl.json +++ b/homeassistant/components/braviatv/translations/pl.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "no_ip_control": "Sterowanie IP jest wy\u0142\u0105czone w telewizorze lub telewizor nie jest obs\u0142ugiwany", - "not_bravia_device": "Urz\u0105dzenie nie jest telewizorem Bravia" + "not_bravia_device": "Urz\u0105dzenie nie jest telewizorem Bravia", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "reauth_unsuccessful": "B\u0142\u0105d ponownego uwierzytelnienia, usu\u0144 integracj\u0119 i skonfiguruj j\u0105 ponownie" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -23,6 +25,13 @@ "confirm": { "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" }, + "reauth_confirm": { + "data": { + "pin": "Kod PIN", + "use_psk": "U\u017cyj uwierzytelniania PSK" + }, + "description": "Wprowad\u017a kod PIN wy\u015bwietlany na telewizorze Sony Bravia. \n\nJe\u015bli kod PIN nie jest wy\u015bwietlany, musisz wyrejestrowa\u0107 Home Assistanta na telewizorze. Przejd\u017a do: Ustawienia - > Sie\u0107 - > Ustawienia urz\u0105dzenia zdalnego - > Wyrejestruj zdalne urz\u0105dzenie. \n\nMo\u017cesz u\u017cy\u0107 PSK (Pre-Shared-Key) zamiast kodu PIN. PSK to zdefiniowany przez u\u017cytkownika tajny klucz u\u017cywany do kontroli dost\u0119pu. Ta metoda uwierzytelniania jest zalecana jako bardziej stabilna. Aby w\u0142\u0105czy\u0107 PSK na telewizorze, przejd\u017a do: Ustawienia - > Sie\u0107 - > Konfiguracja sieci domowej - > Sterowanie IP. Nast\u0119pnie zaznacz pole \u201eU\u017cyj uwierzytelniania PSK\u201d i wprowad\u017a sw\u00f3j PSK zamiast kodu PIN." + }, "user": { "data": { "host": "Nazwa hosta lub adres IP" diff --git a/homeassistant/components/braviatv/translations/sv.json b/homeassistant/components/braviatv/translations/sv.json index 3467af8e997..9b218f562d2 100644 --- a/homeassistant/components/braviatv/translations/sv.json +++ b/homeassistant/components/braviatv/translations/sv.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Den h\u00e4r TV:n \u00e4r redan konfigurerad", "no_ip_control": "IP-kontroll \u00e4r inaktiverat p\u00e5 din TV eller s\u00e5 st\u00f6ds inte TV:n.", - "not_bravia_device": "Enheten \u00e4r inte en Bravia TV." + "not_bravia_device": "Enheten \u00e4r inte en Bravia TV.", + "reauth_successful": "\u00c5terautentisering lyckades", + "reauth_unsuccessful": "\u00c5terautentiseringen misslyckades. Ta bort integrationen och konfigurera den igen." }, "error": { "cannot_connect": "Det gick inte att ansluta.", @@ -23,6 +25,13 @@ "confirm": { "description": "Vill du starta konfigurationen?" }, + "reauth_confirm": { + "data": { + "pin": "Pin-kod", + "use_psk": "Anv\u00e4nd PSK-autentisering" + }, + "description": "Ange PIN-koden som visas p\u00e5 Sony Bravia TV. \n\n Om PIN-koden inte visas m\u00e5ste du avregistrera Home Assistant p\u00e5 din TV, g\u00e5 till: Inst\u00e4llningar - > N\u00e4tverk - > Inst\u00e4llningar f\u00f6r fj\u00e4rrenhet - > Avregistrera fj\u00e4rrenhet. \n\n Du kan anv\u00e4nda PSK (Pre-Shared-Key) ist\u00e4llet f\u00f6r PIN. PSK \u00e4r en anv\u00e4ndardefinierad hemlig nyckel som anv\u00e4nds f\u00f6r \u00e5tkomstkontroll. Denna autentiseringsmetod rekommenderas eftersom den \u00e4r mer stabil. F\u00f6r att aktivera PSK p\u00e5 din TV, g\u00e5 till: Inst\u00e4llningar - > N\u00e4tverk - > Hemn\u00e4tverksinst\u00e4llningar - > IP-kontroll. Markera sedan rutan \u00abAnv\u00e4nd PSK-autentisering\u00bb och ange din PSK ist\u00e4llet f\u00f6r PIN-kod." + }, "user": { "data": { "host": "V\u00e4rdnamn eller IP-adress f\u00f6r TV" diff --git a/homeassistant/components/homekit_controller/translations/sensor.pl.json b/homeassistant/components/homekit_controller/translations/sensor.pl.json index a3a251cbc6f..a11105cfc15 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.pl.json +++ b/homeassistant/components/homekit_controller/translations/sensor.pl.json @@ -1,10 +1,21 @@ { "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "funkcje routera granicznego", + "full": "pe\u0142ne urz\u0105dzenie ko\u0144cowe", + "minimal": "podstawowe urz\u0105dzenie ko\u0144cowe", + "none": "brak", + "router_eligible": "urz\u0105dzenie ko\u0144cowe kwalifikuj\u0105ce si\u0119 jako router", + "sleepy": "u\u015bpione urz\u0105dzenie ko\u0144cowe" + }, "homekit_controller__thread_status": { - "child": "Dziecko", - "detached": "Od\u0142\u0105czony", - "joining": "Do\u0142\u0105czanie", - "router": "Router" + "border_router": "router graniczny", + "child": "dziecko", + "detached": "od\u0142\u0105czony", + "disabled": "wy\u0142\u0105czony", + "joining": "do\u0142\u0105czanie", + "leader": "lider", + "router": "router" } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/pl.json b/homeassistant/components/hue/translations/pl.json index ff8ab182dd4..9cbe70d868b 100644 --- a/homeassistant/components/hue/translations/pl.json +++ b/homeassistant/components/hue/translations/pl.json @@ -55,15 +55,15 @@ }, "trigger_type": { "double_short_release": "przycisk \"{subtype}\" zostanie zwolniony", - "initial_press": "przycisk \"{subtype}\" zostanie lekko naci\u015bni\u0119ty", - "long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", - "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", + "initial_press": "\"{subtype}\" zostanie lekko naci\u015bni\u0119ty", + "long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty", + "remote_button_short_release": "\"{subtype}\" zostanie zwolniony", "remote_double_button_long_press": "oba przyciski \"{subtype}\" zostan\u0105 zwolnione po d\u0142ugim naci\u015bni\u0119ciu", "remote_double_button_short_press": "oba przyciski \"{subtype}\" zostan\u0105 zwolnione", - "repeat": "przycisk \"{subtype}\" zostanie przytrzymany", - "short_release": "przycisk \"{subtype}\" zostanie zwolniony po kr\u00f3tkim naci\u015bni\u0119ciu", + "repeat": "\"{subtype}\" zostanie przytrzymany", + "short_release": "\"{subtype}\" zostanie zwolniony po kr\u00f3tkim naci\u015bni\u0119ciu", "start": "\"{subtype}\" zostanie lekko naci\u015bni\u0119ty" } }, diff --git a/homeassistant/components/lidarr/translations/en.json b/homeassistant/components/lidarr/translations/en.json index cdb21be7fb2..0e0475d25cd 100644 --- a/homeassistant/components/lidarr/translations/en.json +++ b/homeassistant/components/lidarr/translations/en.json @@ -28,5 +28,15 @@ "description": "API key can be retrieved automatically if login credentials were not set in application.\nYour API key can be found in Settings > General in the Lidarr Web UI." } } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "Number of maximum records to display on wanted and queue", + "upcoming_days": "Number of upcoming days to display on calendar" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/pl.json b/homeassistant/components/mikrotik/translations/pl.json index a8bf06df15f..f4231bb767f 100644 --- a/homeassistant/components/mikrotik/translations/pl.json +++ b/homeassistant/components/mikrotik/translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "reauth_successful": "Ponowna autoryzacja przebieg\u0142a pomy\u015blnie" + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -15,7 +15,7 @@ "password": "Has\u0142o" }, "description": "Has\u0142o u\u017cytkownika {username} jest nieprawid\u0142owe.", - "title": "Ponownie autoryzuj integracje" + "title": "Ponownie uwierzytelnij integracj\u0119" }, "user": { "data": { diff --git a/homeassistant/components/mikrotik/translations/sv.json b/homeassistant/components/mikrotik/translations/sv.json index bc93490db14..1dd1d00c0c0 100644 --- a/homeassistant/components/mikrotik/translations/sv.json +++ b/homeassistant/components/mikrotik/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Mikrotik \u00e4r redan konfigurerad" + "already_configured": "Mikrotik \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Anslutningen misslyckades", @@ -9,6 +10,13 @@ "name_exists": "Namnet finns" }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "L\u00f6senordet f\u00f6r {username} \u00e4r ogiltigt.", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "host": "V\u00e4rd", diff --git a/homeassistant/components/nest/translations/id.json b/homeassistant/components/nest/translations/id.json index d9ed2039800..2d07df4dc62 100644 --- a/homeassistant/components/nest/translations/id.json +++ b/homeassistant/components/nest/translations/id.json @@ -52,7 +52,7 @@ "data": { "project_id": "ID Proyek Akses Perangkat" }, - "description": "Buat proyek Akses Perangkat Nest yang **membutuhkan biaya USD5** untuk menyiapkannya.\n1. Buka [Konsol Akses Perangkat]({device_access_console_url}), dan ikuti alur pembayaran.\n1. Klik **Buat proyek**\n1. Beri nama proyek Akses Perangkat Anda dan klik **Berikutnya**.\n1. Masukkan ID Klien OAuth Anda\n1. Aktifkan acara dengan mengklik **Aktifkan** dan **Buat proyek**. \n\n Masukkan ID Proyek Akses Perangkat Anda di bawah ini ([more info]({more_info_url})).\n", + "description": "Buat proyek Akses Perangkat Nest yang **membutuhkan biaya USD5 untuk Google** untuk menyiapkannya.\n1. Buka [Konsol Akses Perangkat]({device_access_console_url}), dan ikuti alur pembayaran.\n1. Klik **Buat proyek**\n1. Beri nama proyek Akses Perangkat Anda dan klik **Berikutnya**.\n1. Masukkan ID Klien OAuth Anda\n1. Aktifkan acara dengan mengklik **Aktifkan** dan **Buat proyek**. \n\n Masukkan ID Proyek Akses Perangkat Anda di bawah ini ([more info]({more_info_url})).\n", "title": "Nest: Buat Proyek Akses Perangkat" }, "device_project_upgrade": { diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json index a0b879ccc90..11da4335614 100644 --- a/homeassistant/components/nest/translations/pl.json +++ b/homeassistant/components/nest/translations/pl.json @@ -52,7 +52,7 @@ "data": { "project_id": "Identyfikator projektu dost\u0119pu do urz\u0105dzenia" }, - "description": "Utw\u00f3rz projekt dost\u0119pu do urz\u0105dzenia Nest, kt\u00f3rego konfiguracja **wymaga op\u0142aty w wysoko\u015bci 5 dolar\u00f3w**.\n1. Przejd\u017a do [Konsoli dost\u0119pu do urz\u0105dzenia]({device_access_console_url}) i przejd\u017a przez proces p\u0142atno\u015bci.\n2. Kliknij **Utw\u00f3rz projekt**.\n3. Nadaj projektowi dost\u0119pu do urz\u0105dzenia nazw\u0119 i kliknij **Dalej**.\n4. Wprowad\u017a sw\u00f3j identyfikator klienta OAuth\n5. W\u0142\u0105cz wydarzenia, klikaj\u0105c **W\u0142\u0105cz** i **Utw\u00f3rz projekt**. \n\nPod ([wi\u0119cej informacji]({more_info_url})) wpisz sw\u00f3j identyfikator projektu dost\u0119pu do urz\u0105dzenia.\n", + "description": "Utw\u00f3rz projekt dost\u0119pu do urz\u0105dzenia Nest, kt\u00f3rego konfiguracja **wymaga op\u0142aty w Google w wysoko\u015bci 5 dolar\u00f3w**.\n1. Przejd\u017a do [Konsoli dost\u0119pu do urz\u0105dzenia]({device_access_console_url}) i przejd\u017a przez proces p\u0142atno\u015bci.\n2. Kliknij **Utw\u00f3rz projekt**.\n3. Nadaj projektowi dost\u0119pu do urz\u0105dzenia nazw\u0119 i kliknij **Dalej**.\n4. Wprowad\u017a sw\u00f3j identyfikator klienta OAuth\n5. W\u0142\u0105cz wydarzenia, klikaj\u0105c **W\u0142\u0105cz** i **Utw\u00f3rz projekt**. \n\nPod ([wi\u0119cej informacji]({more_info_url})) wpisz sw\u00f3j identyfikator projektu dost\u0119pu do urz\u0105dzenia.\n", "title": "Nest: Utw\u00f3rz projekt dost\u0119pu do urz\u0105dzenia" }, "device_project_upgrade": { diff --git a/homeassistant/components/octoprint/translations/pl.json b/homeassistant/components/octoprint/translations/pl.json index fcb8c5fbbb0..a2ccd78204a 100644 --- a/homeassistant/components/octoprint/translations/pl.json +++ b/homeassistant/components/octoprint/translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "auth_failed": "Nie uda\u0142o si\u0119 pobra\u0107 klucza API aplikacji", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "reauth_successful": "Ponowna autoryzacja przebieg\u0142a pomy\u015blnie", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { diff --git a/homeassistant/components/octoprint/translations/sv.json b/homeassistant/components/octoprint/translations/sv.json index f3b9760aca2..a349fb9c973 100644 --- a/homeassistant/components/octoprint/translations/sv.json +++ b/homeassistant/components/octoprint/translations/sv.json @@ -4,6 +4,7 @@ "already_configured": "Enheten \u00e4r redan konfigurerad", "auth_failed": "Det gick inte att h\u00e4mta applikationens API-nyckel", "cannot_connect": "Det gick inte att ansluta.", + "reauth_successful": "\u00c5terautentisering lyckades", "unknown": "Ov\u00e4ntat fel" }, "error": { @@ -15,6 +16,11 @@ "get_api_key": "\u00d6ppna OctoPrint UI och klicka p\u00e5 \"Till\u00e5t\" p\u00e5 \u00e5tkomstbeg\u00e4ran f\u00f6r \"Home Assistant\"." }, "step": { + "reauth_confirm": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + }, "user": { "data": { "host": "V\u00e4rd", diff --git a/homeassistant/components/rtsp_to_webrtc/translations/el.json b/homeassistant/components/rtsp_to_webrtc/translations/el.json index 0e4c6baa287..19c29763deb 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/el.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/el.json @@ -23,5 +23,14 @@ "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 RTSPtoWebRTC" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03b1\u03bd\u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 (host:port)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rtsp_to_webrtc/translations/fr.json b/homeassistant/components/rtsp_to_webrtc/translations/fr.json index e51207a6254..e5ea9ef1eb7 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/fr.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/fr.json @@ -23,5 +23,14 @@ "title": "Configurer RTSPtoWebRTC" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Adresse du serveur STUN (h\u00f4te:port)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rtsp_to_webrtc/translations/id.json b/homeassistant/components/rtsp_to_webrtc/translations/id.json index 105e4072300..c9841a7612b 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/id.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/id.json @@ -23,5 +23,14 @@ "title": "Konfigurasikan RTSPtoWebrTC" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Alamat server stun (host:port)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rtsp_to_webrtc/translations/pl.json b/homeassistant/components/rtsp_to_webrtc/translations/pl.json index 3ed933d098f..8d42c0efcc7 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/pl.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/pl.json @@ -23,5 +23,14 @@ "title": "Konfiguracja RTSPtoWebRTC" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Adres serwera STUN (host:port)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rtsp_to_webrtc/translations/sv.json b/homeassistant/components/rtsp_to_webrtc/translations/sv.json index c748d2feb9c..d6e8400571d 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/sv.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/sv.json @@ -23,5 +23,14 @@ "title": "Konfigurera RTSPtoWebRTC" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Stun serveradress (v\u00e4rd:port)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/sv.json b/homeassistant/components/sensor/translations/sv.json index 544bf563bcc..31139005375 100644 --- a/homeassistant/components/sensor/translations/sv.json +++ b/homeassistant/components/sensor/translations/sv.json @@ -6,6 +6,7 @@ "is_carbon_dioxide": "Nuvarande {entity_name} koncentration av koldioxid", "is_carbon_monoxide": "Nuvarande {entity_name} koncentration av kolmonoxid", "is_current": "Nuvarande", + "is_distance": "Aktuellt avst\u00e5nd {entity_name}", "is_energy": "Nuvarande {entity_name} energi", "is_frequency": "Nuvarande frekvens", "is_gas": "Nuvarande {entity_name} gas", @@ -24,11 +25,14 @@ "is_pressure": "Aktuellt {entity_name} tryck", "is_reactive_power": "Nuvarande {entity_name} reaktiv effekt", "is_signal_strength": "Nuvarande {entity_name} signalstyrka", + "is_speed": "Aktuell hastighet {entity_name}", "is_sulphur_dioxide": "Nuvarande koncentration av svaveldioxid i {entity_name}.", "is_temperature": "Aktuell {entity_name} temperatur", "is_value": "Nuvarande {entity_name} v\u00e4rde", "is_volatile_organic_compounds": "Nuvarande {entity_name} koncentration av flyktiga organiska \u00e4mnen", - "is_voltage": "Nuvarande {entity_name} sp\u00e4nning" + "is_voltage": "Nuvarande {entity_name} sp\u00e4nning", + "is_volume": "Aktuell volym {entity_name}", + "is_weight": "Nuvarande vikt {entity_name}" }, "trigger_type": { "apparent_power": "{entity_name} uppenbara effektf\u00f6r\u00e4ndringar", @@ -36,6 +40,7 @@ "carbon_dioxide": "{entity_name} f\u00f6r\u00e4ndringar av koldioxidkoncentrationen", "carbon_monoxide": "{entity_name} f\u00f6r\u00e4ndringar av kolmonoxidkoncentrationen", "current": "{entity_name} aktuella \u00e4ndringar", + "distance": "{entity_name} avst\u00e5ndsf\u00f6r\u00e4ndringar", "energy": "Energif\u00f6r\u00e4ndringar", "frequency": "{entity_name} frekvens\u00e4ndringar", "gas": "{entity_name} gasf\u00f6r\u00e4ndringar", @@ -54,11 +59,14 @@ "pressure": "{entity_name} tryckf\u00f6r\u00e4ndringar", "reactive_power": "{entity_name} reaktiv effekt\u00e4ndring", "signal_strength": "{entity_name} signalstyrka \u00e4ndras", + "speed": "{entity_name} hastighets\u00e4ndringar", "sulphur_dioxide": "{entity_name} f\u00f6r\u00e4ndringar av koncentrationen av svaveldioxid", "temperature": "{entity_name} temperaturf\u00f6r\u00e4ndringar", "value": "{entity_name} v\u00e4rde \u00e4ndras", "volatile_organic_compounds": "{entity_name} koncentrations\u00e4ndringar av flyktiga organiska \u00e4mnen", - "voltage": "{entity_name} sp\u00e4nningsf\u00f6r\u00e4ndringar" + "voltage": "{entity_name} sp\u00e4nningsf\u00f6r\u00e4ndringar", + "volume": "{entity_name} volymf\u00f6r\u00e4ndringar", + "weight": "{entity_name} viktf\u00f6r\u00e4ndringar" } }, "state": { diff --git a/homeassistant/components/water_heater/translations/bg.json b/homeassistant/components/water_heater/translations/bg.json index b751234eaea..c80c861f5dd 100644 --- a/homeassistant/components/water_heater/translations/bg.json +++ b/homeassistant/components/water_heater/translations/bg.json @@ -4,5 +4,10 @@ "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}", "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" } + }, + "state": { + "_": { + "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d" + } } } \ No newline at end of file diff --git a/homeassistant/components/weather/translations/pl.json b/homeassistant/components/weather/translations/pl.json index 43cc15533eb..c284c361623 100644 --- a/homeassistant/components/weather/translations/pl.json +++ b/homeassistant/components/weather/translations/pl.json @@ -1,15 +1,15 @@ { "state": { "_": { - "clear-night": "pogodna noc", - "cloudy": "pochmurno", + "clear-night": "Pogodna noc", + "cloudy": "Pochmurno", "exceptional": "warunki nadzwyczajne", "fog": "mg\u0142a", "hail": "grad", "lightning": "b\u0142yskawice", "lightning-rainy": "burza", "partlycloudy": "cz\u0119\u015bciowe zachmurzenie", - "pouring": "ulewa", + "pouring": "Ulewa", "rainy": "deszczowo", "snowy": "opady \u015bniegu", "snowy-rainy": "\u015bnieg z deszczem", diff --git a/homeassistant/components/zha/translations/sv.json b/homeassistant/components/zha/translations/sv.json index ca9fc90f5e9..8c0bd645a65 100644 --- a/homeassistant/components/zha/translations/sv.json +++ b/homeassistant/components/zha/translations/sv.json @@ -116,6 +116,8 @@ }, "device_automation": { "action_type": { + "issue_all_led_effect": "Effekt f\u00f6r alla lysdioder", + "issue_individual_led_effect": "Effekt f\u00f6r enskilda lysdioder", "squawk": "Kraxa", "warn": "Varna" }, From c798723c2744a7b386c56de9627a31c349309bbf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Oct 2022 16:32:29 -1000 Subject: [PATCH 188/985] Fix bluetooth diagnostics on macos (#79680) * Fix bluetooth diagnostics on macos The pyobjc objects cannot be pickled which cases dataclasses asdict to raise an exception when trying to do the deepcopy We now implement our own as_dict to avoid this problem * add cover --- homeassistant/components/bluetooth/manager.py | 5 +-- homeassistant/components/bluetooth/models.py | 19 +++++++++ .../components/bluetooth/test_diagnostics.py | 40 +++++++++++++++++-- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 37c24423231..f0152f5ae5e 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Iterable -from dataclasses import asdict from datetime import datetime, timedelta import itertools import logging @@ -185,11 +184,11 @@ class BluetoothManager: "adapters": self._adapters, "scanners": scanner_diagnostics, "connectable_history": [ - asdict(service_info) + service_info.as_dict() for service_info in self._connectable_history.values() ], "history": [ - asdict(service_info) for service_info in self._history.values() + service_info.as_dict() for service_info in self._history.values() ], } diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index d93f8efc1e2..9e93ea4d142 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -53,6 +53,25 @@ class BluetoothServiceInfoBleak(BluetoothServiceInfo): connectable: bool time: float + def as_dict(self) -> dict[str, Any]: + """Return as dict. + + The dataclass asdict method is not used because + it will try to deepcopy pyobjc data which will fail. + """ + return { + "name": self.name, + "address": self.address, + "rssi": self.rssi, + "manufacturer_data": self.manufacturer_data, + "service_data": self.service_data, + "service_uuids": self.service_uuids, + "source": self.source, + "advertisement": self.advertisement, + "connectable": self.connectable, + "time": self.time, + } + class BluetoothScanningMode(Enum): """The mode of scanning for bluetooth devices.""" diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index d641cae9c7c..1da071a76ab 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -3,11 +3,13 @@ from unittest.mock import ANY, patch -from bleak.backends.scanner import BLEDevice +from bleak.backends.scanner import AdvertisementData, BLEDevice from homeassistant.components import bluetooth from homeassistant.components.bluetooth.const import DEFAULT_ADDRESS +from . import inject_advertisement + from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -158,6 +160,10 @@ async def test_diagnostics_macos( # because we cannot import the scanner class directly without it throwing an # error if the test is not running on linux since we won't have the correct # deps installed when testing on MacOS. + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"} + ) with patch( "homeassistant.components.bluetooth.scanner.HaScanner.discovered_devices", @@ -180,6 +186,8 @@ async def test_diagnostics_macos( assert await hass.config_entries.async_setup(entry1.entry_id) await hass.async_block_till_done() + inject_advertisement(hass, switchbot_device, switchbot_adv) + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry1) assert diag == { "adapters": { @@ -197,8 +205,34 @@ async def test_diagnostics_macos( "sw_version": ANY, } }, - "connectable_history": [], - "history": [], + "connectable_history": [ + { + "address": "44:44:33:11:23:45", + "advertisement": ANY, + "connectable": True, + "manufacturer_data": ANY, + "name": "wohand", + "rssi": 0, + "service_data": {}, + "service_uuids": [], + "source": "local", + "time": ANY, + } + ], + "history": [ + { + "address": "44:44:33:11:23:45", + "advertisement": ANY, + "connectable": True, + "manufacturer_data": ANY, + "name": "wohand", + "rssi": 0, + "service_data": {}, + "service_uuids": [], + "source": "local", + "time": ANY, + } + ], "scanners": [ { "adapter": "Core Bluetooth", From ce6ccffd9c39c9c3cc9fad5751abd440fd84a82d Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 6 Oct 2022 08:10:04 +0300 Subject: [PATCH 189/985] Fix Switcher breeze fan mode and swing control (#79676) --- .../components/switcher_kis/climate.py | 6 ++--- tests/components/switcher_kis/test_climate.py | 26 ++++++++++++------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/switcher_kis/climate.py b/homeassistant/components/switcher_kis/climate.py index 75ce386bd39..99b9208e4ad 100644 --- a/homeassistant/components/switcher_kis/climate.py +++ b/homeassistant/components/switcher_kis/climate.py @@ -196,7 +196,7 @@ class SwitcherClimateEntity( if not self._remote.modes_features[self.coordinator.data.mode]["fan_levels"]: raise HomeAssistantError("Current mode doesn't support setting Fan Mode") - await self._async_control_breeze_device(fan_mode=HA_TO_DEVICE_FAN[fan_mode]) + await self._async_control_breeze_device(fan_level=HA_TO_DEVICE_FAN[fan_mode]) async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target operation mode.""" @@ -213,6 +213,6 @@ class SwitcherClimateEntity( raise HomeAssistantError("Current mode doesn't support setting Swing Mode") if swing_mode == SWING_VERTICAL: - await self._async_control_breeze_device(swing_mode=ThermostatSwing.ON) + await self._async_control_breeze_device(swing=ThermostatSwing.ON) else: - await self._async_control_breeze_device(swing_mode=ThermostatSwing.OFF) + await self._async_control_breeze_device(swing=ThermostatSwing.OFF) diff --git a/tests/components/switcher_kis/test_climate.py b/tests/components/switcher_kis/test_climate.py index 212ab88746b..56fbbe61ef9 100644 --- a/tests/components/switcher_kis/test_climate.py +++ b/tests/components/switcher_kis/test_climate.py @@ -1,5 +1,5 @@ """Test the Switcher climate platform.""" -from unittest.mock import patch +from unittest.mock import ANY, patch from aioswitcher.api import SwitcherBaseResponse from aioswitcher.device import ( @@ -59,7 +59,9 @@ async def test_climate_hvac_mode(hass, mock_bridge, mock_api, monkeypatch): await hass.async_block_till_done() assert mock_api.call_count == 2 - mock_control_device.assert_called_once() + mock_control_device.assert_called_once_with( + ANY, state=DeviceState.ON, mode=ThermostatMode.HEAT + ) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.HEAT @@ -79,7 +81,7 @@ async def test_climate_hvac_mode(hass, mock_bridge, mock_api, monkeypatch): await hass.async_block_till_done() assert mock_api.call_count == 4 - mock_control_device.assert_called_once() + mock_control_device.assert_called_once_with(ANY, state=DeviceState.OFF) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.OFF @@ -110,7 +112,7 @@ async def test_climate_temperature(hass, mock_bridge, mock_api, monkeypatch): await hass.async_block_till_done() assert mock_api.call_count == 2 - mock_control_device.assert_called_once() + mock_control_device.assert_called_once_with(ANY, target_temp=22) state = hass.states.get(ENTITY_ID) assert state.attributes["temperature"] == 22 @@ -160,7 +162,9 @@ async def test_climate_fan_level(hass, mock_bridge, mock_api, monkeypatch): await hass.async_block_till_done() assert mock_api.call_count == 2 - mock_control_device.assert_called_once() + mock_control_device.assert_called_once_with( + ANY, fan_level=ThermostatFanLevel.HIGH + ) state = hass.states.get(ENTITY_ID) assert state.attributes["fan_mode"] == "high" @@ -194,7 +198,7 @@ async def test_climate_swing(hass, mock_bridge, mock_api, monkeypatch): await hass.async_block_till_done() assert mock_api.call_count == 2 - mock_control_device.assert_called_once() + mock_control_device.assert_called_once_with(ANY, swing=ThermostatSwing.ON) state = hass.states.get(ENTITY_ID) assert state.attributes["swing_mode"] == "vertical" @@ -214,7 +218,7 @@ async def test_climate_swing(hass, mock_bridge, mock_api, monkeypatch): await hass.async_block_till_done() assert mock_api.call_count == 4 - mock_control_device.assert_called_once() + mock_control_device.assert_called_once_with(ANY, swing=ThermostatSwing.OFF) state = hass.states.get(ENTITY_ID) assert state.attributes["swing_mode"] == "off" @@ -243,7 +247,9 @@ async def test_control_device_fail(hass, mock_bridge, mock_api, monkeypatch): ) assert mock_api.call_count == 2 - mock_control_device.assert_called_once() + mock_control_device.assert_called_once_with( + ANY, state=DeviceState.ON, mode=ThermostatMode.HEAT + ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_UNAVAILABLE @@ -268,7 +274,9 @@ async def test_control_device_fail(hass, mock_bridge, mock_api, monkeypatch): ) assert mock_api.call_count == 4 - mock_control_device.assert_called_once() + mock_control_device.assert_called_once_with( + ANY, state=DeviceState.ON, mode=ThermostatMode.HEAT + ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_UNAVAILABLE From 9b4c7f5dc576a23469a556fadfa1567d3f0abfba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Oct 2022 19:17:10 -1000 Subject: [PATCH 190/985] Bump dbus-fast to 1.26.0 (#79684) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index f81e1324da4..53a4125625a 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.24.0" + "dbus-fast==1.26.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2f637ba61f1..cc6d716a43b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.24.0 +dbus-fast==1.26.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 715f4e5f7a2..cb2d10ad5a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -543,7 +543,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.24.0 +dbus-fast==1.26.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44b3487850a..5bbc88760a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.24.0 +dbus-fast==1.26.0 # homeassistant.components.debugpy debugpy==1.6.3 From 93b2a6cc267da1b78233d75586263765c8161158 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 6 Oct 2022 10:10:58 +0300 Subject: [PATCH 191/985] Refactor Shelly to use data class for ConfigEntry data (#79671) * Refactor Shelly to use data class for ConfigEntry data * Apply suggestions from code review Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Update homeassistant/components/shelly/__init__.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Optimize usage of shelly_entry_data in _async_setup_block_entry Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/shelly/__init__.py | 146 ++++++------------ homeassistant/components/shelly/button.py | 16 +- homeassistant/components/shelly/climate.py | 17 +- homeassistant/components/shelly/const.py | 6 - .../components/shelly/coordinator.py | 57 +++++++ homeassistant/components/shelly/cover.py | 10 +- .../components/shelly/device_trigger.py | 15 +- .../components/shelly/diagnostics.py | 13 +- homeassistant/components/shelly/entity.py | 36 ++--- homeassistant/components/shelly/light.py | 13 +- homeassistant/components/shelly/logbook.py | 9 +- homeassistant/components/shelly/switch.py | 10 +- homeassistant/components/shelly/update.py | 10 +- 13 files changed, 164 insertions(+), 194 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 1d59816c661..b2b927e48b5 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from http import HTTPStatus -from typing import Any, Final, cast +from typing import Any, Final from aiohttp import ClientResponseError import aioshelly @@ -23,23 +23,20 @@ from homeassistant.helpers.typing import ConfigType from .const import ( AIOSHELLY_DEVICE_TIMEOUT_SEC, - BLOCK, CONF_COAP_PORT, CONF_SLEEP_PERIOD, DATA_CONFIG_ENTRY, DEFAULT_COAP_PORT, - DEVICE, DOMAIN, LOGGER, - REST, - RPC, - RPC_POLL, ) from .coordinator import ( ShellyBlockCoordinator, + ShellyEntryData, ShellyRestCoordinator, ShellyRpcCoordinator, ShellyRpcPollingCoordinator, + get_entry_data, ) from .utils import get_block_device_sleep_period, get_coap_context, get_device_entry_gen @@ -101,16 +98,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False - hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = {} - hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = None + get_entry_data(hass)[entry.entry_id] = ShellyEntryData() if get_device_entry_gen(entry) == 2: - return await async_setup_rpc_entry(hass, entry) + return await _async_setup_rpc_entry(hass, entry) - return await async_setup_block_entry(hass, entry) + return await _async_setup_block_entry(hass, entry) -async def async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Shelly block based device from a config entry.""" temperature_unit = "C" if hass.config.units.is_metric else "F" @@ -146,11 +142,26 @@ async def async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bo device_entry = None sleep_period = entry.data.get(CONF_SLEEP_PERIOD) + shelly_entry_data = get_entry_data(hass)[entry.entry_id] + + @callback + def _async_block_device_setup() -> None: + """Set up a block based device that is online.""" + shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) + shelly_entry_data.block.async_setup() + + platforms = BLOCK_SLEEPING_PLATFORMS + + if not entry.data.get(CONF_SLEEP_PERIOD): + shelly_entry_data.rest = ShellyRestCoordinator(hass, device, entry) + platforms = BLOCK_PLATFORMS + + hass.config_entries.async_setup_platforms(entry, platforms) @callback def _async_device_online(_: Any) -> None: LOGGER.debug("Device %s is online, resuming setup", entry.title) - hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = None + shelly_entry_data.device = None if sleep_period is None: data = {**entry.data} @@ -158,7 +169,7 @@ async def async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bo data["model"] = device.settings["device"]["type"] hass.config_entries.async_update_entry(entry, data=data) - async_block_device_setup(hass, entry, device) + _async_block_device_setup() if sleep_period == 0: # Not a sleeping device, finish setup @@ -179,10 +190,10 @@ async def async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bo if err.status == HTTPStatus.UNAUTHORIZED: raise ConfigEntryAuthFailed from err - async_block_device_setup(hass, entry, device) + _async_block_device_setup() elif sleep_period is None or device_entry is None: # Need to get sleep info or first time sleeping device setup, wait for device - hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = device + shelly_entry_data.device = device LOGGER.debug( "Setup for device %s will resume when device is online", entry.title ) @@ -190,33 +201,12 @@ async def async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bo else: # Restore sensors for sleeping device LOGGER.debug("Setting up offline block device %s", entry.title) - async_block_device_setup(hass, entry, device) + _async_block_device_setup() return True -@callback -def async_block_device_setup( - hass: HomeAssistant, entry: ConfigEntry, device: BlockDevice -) -> None: - """Set up a block based device that is online.""" - block_coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ - BLOCK - ] = ShellyBlockCoordinator(hass, entry, device) - block_coordinator.async_setup() - - platforms = BLOCK_SLEEPING_PLATFORMS - - if not entry.data.get(CONF_SLEEP_PERIOD): - hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ - REST - ] = ShellyRestCoordinator(hass, device, entry) - platforms = BLOCK_PLATFORMS - - hass.config_entries.async_setup_platforms(entry, platforms) - - -async def async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Shelly RPC based device from a config entry.""" options = aioshelly.common.ConnectionOptions( entry.data[CONF_HOST], @@ -237,14 +227,11 @@ async def async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool except (AuthRequired, InvalidAuthError) as err: raise ConfigEntryAuthFailed from err - rpc_coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ - RPC - ] = ShellyRpcCoordinator(hass, entry, device) - rpc_coordinator.async_setup() + shelly_entry_data = get_entry_data(hass)[entry.entry_id] + shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device) + shelly_entry_data.rpc.async_setup() - hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][ - RPC_POLL - ] = ShellyRpcPollingCoordinator(hass, entry, device) + shelly_entry_data.rpc_poll = ShellyRpcPollingCoordinator(hass, entry, device) hass.config_entries.async_setup_platforms(entry, RPC_PLATFORMS) @@ -253,73 +240,32 @@ async def async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + shelly_entry_data = get_entry_data(hass)[entry.entry_id] + if get_device_entry_gen(entry) == 2: - unload_ok = await hass.config_entries.async_unload_platforms( + if unload_ok := await hass.config_entries.async_unload_platforms( entry, RPC_PLATFORMS - ) - if unload_ok: - await hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][RPC].shutdown() - hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id) + ): + if shelly_entry_data.rpc: + await shelly_entry_data.rpc.shutdown() + get_entry_data(hass).pop(entry.entry_id) return unload_ok - device = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id].get(DEVICE) - if device is not None: + if shelly_entry_data.device is not None: # If device is present, block coordinator is not setup yet - device.shutdown() + shelly_entry_data.device.shutdown() return True platforms = BLOCK_SLEEPING_PLATFORMS if not entry.data.get(CONF_SLEEP_PERIOD): - hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][REST] = None + shelly_entry_data.rest = None platforms = BLOCK_PLATFORMS - unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms) - if unload_ok: - hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][BLOCK].shutdown() - hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id) + if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms): + if shelly_entry_data.block: + shelly_entry_data.block.shutdown() + get_entry_data(hass).pop(entry.entry_id) return unload_ok - - -def get_block_device_coordinator( - hass: HomeAssistant, device_id: str -) -> ShellyBlockCoordinator | None: - """Get a Shelly block device coordinator for the given device id.""" - if not hass.data.get(DOMAIN): - return None - - dev_reg = device_registry.async_get(hass) - if device := dev_reg.async_get(device_id): - for config_entry in device.config_entries: - if not hass.data[DOMAIN][DATA_CONFIG_ENTRY].get(config_entry): - continue - - if coordinator := hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get( - BLOCK - ): - return cast(ShellyBlockCoordinator, coordinator) - - return None - - -def get_rpc_device_coordinator( - hass: HomeAssistant, device_id: str -) -> ShellyRpcCoordinator | None: - """Get a Shelly RPC device coordinator for the given device id.""" - if not hass.data.get(DOMAIN): - return None - - dev_reg = device_registry.async_get(hass) - if device := dev_reg.async_get(device_id): - for config_entry in device.config_entries: - if not hass.data[DOMAIN][DATA_CONFIG_ENTRY].get(config_entry): - continue - - if coordinator := hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry].get( - RPC - ): - return cast(ShellyRpcCoordinator, coordinator) - - return None diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 48213d706ef..e7989dd9417 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Final, cast +from typing import Final from homeassistant.components.button import ( ButtonDeviceClass, @@ -18,8 +18,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import slugify -from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC, SHELLY_GAS_MODELS -from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator +from .const import SHELLY_GAS_MODELS +from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data from .utils import get_block_device_name, get_device_entry_gen, get_rpc_device_name @@ -80,15 +80,9 @@ async def async_setup_entry( """Set buttons for device.""" coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator | None = None if get_device_entry_gen(config_entry) == 2: - if rpc_coordinator := hass.data[DOMAIN][DATA_CONFIG_ENTRY][ - config_entry.entry_id - ].get(RPC): - coordinator = cast(ShellyRpcCoordinator, rpc_coordinator) + coordinator = get_entry_data(hass)[config_entry.entry_id].rpc else: - if block_coordinator := hass.data[DOMAIN][DATA_CONFIG_ENTRY][ - config_entry.entry_id - ].get(BLOCK): - coordinator = cast(ShellyBlockCoordinator, block_coordinator) + coordinator = get_entry_data(hass)[config_entry.entry_id].block if coordinator is not None: entities = [] diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 5ab5b728aa4..7bf43ce33cb 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -26,15 +26,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - AIOSHELLY_DEVICE_TIMEOUT_SEC, - BLOCK, - DATA_CONFIG_ENTRY, - DOMAIN, - LOGGER, - SHTRV_01_TEMPERATURE_SETTINGS, -) -from .coordinator import ShellyBlockCoordinator +from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, LOGGER, SHTRV_01_TEMPERATURE_SETTINGS +from .coordinator import ShellyBlockCoordinator, get_entry_data from .utils import get_device_entry_gen @@ -48,10 +41,8 @@ async def async_setup_entry( if get_device_entry_gen(config_entry) == 2: return - coordinator: ShellyBlockCoordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ - config_entry.entry_id - ][BLOCK] - + coordinator = get_entry_data(hass)[config_entry.entry_id].block + assert coordinator if coordinator.device.initialized: async_setup_climate_entities(async_add_entities, coordinator) else: diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 3dacf2bfd6a..cb89ecc4ea1 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -9,13 +9,7 @@ DOMAIN: Final = "shelly" LOGGER: Logger = getLogger(__package__) -BLOCK: Final = "block" DATA_CONFIG_ENTRY: Final = "config_entry" -DEVICE: Final = "device" -REST: Final = "rest" -RPC: Final = "rpc" -RPC_POLL: Final = "rpc_poll" - CONF_COAP_PORT: Final = "coap_port" DEFAULT_COAP_PORT: Final = 5683 FIRMWARE_PATTERN: Final = re.compile(r"^(\d{8})") diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index db485167fe3..6259571d078 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Coroutine +from dataclasses import dataclass from datetime import timedelta from typing import Any, cast @@ -27,6 +28,8 @@ from .const import ( ATTR_GENERATION, BATTERY_DEVICES_WITH_PERMANENT_CONNECTION, CONF_SLEEP_PERIOD, + DATA_CONFIG_ENTRY, + DOMAIN, DUAL_MODE_LIGHT_MODELS, ENTRY_RELOAD_COOLDOWN, EVENT_SHELLY_CLICK, @@ -45,6 +48,22 @@ from .const import ( from .utils import device_update_info, get_block_device_name, get_rpc_device_name +@dataclass +class ShellyEntryData: + """Class for sharing data within a given config entry.""" + + block: ShellyBlockCoordinator | None = None + device: BlockDevice | None = None + rest: ShellyRestCoordinator | None = None + rpc: ShellyRpcCoordinator | None = None + rpc_poll: ShellyRpcPollingCoordinator | None = None + + +def get_entry_data(hass: HomeAssistant) -> dict[str, ShellyEntryData]: + """Return Shelly entry data for a given config entry.""" + return cast(dict[str, ShellyEntryData], hass.data[DOMAIN][DATA_CONFIG_ENTRY]) + + class ShellyBlockCoordinator(DataUpdateCoordinator): """Coordinator for a Shelly block based device.""" @@ -532,3 +551,41 @@ class ShellyRpcPollingCoordinator(DataUpdateCoordinator): def mac(self) -> str: """Mac address of the device.""" return cast(str, self.entry.unique_id) + + +def get_block_coordinator_by_device_id( + hass: HomeAssistant, device_id: str +) -> ShellyBlockCoordinator | None: + """Get a Shelly block device coordinator for the given device id.""" + if not hass.data.get(DOMAIN): + return None + + dev_reg = device_registry.async_get(hass) + if device := dev_reg.async_get(device_id): + for config_entry in device.config_entries: + if not (entry_data := get_entry_data(hass).get(config_entry)): + continue + + if coordinator := entry_data.block: + return coordinator + + return None + + +def get_rpc_coordinator_by_device_id( + hass: HomeAssistant, device_id: str +) -> ShellyRpcCoordinator | None: + """Get a Shelly RPC device coordinator for the given device id.""" + if not hass.data.get(DOMAIN): + return None + + dev_reg = device_registry.async_get(hass) + if device := dev_reg.async_get(device_id): + for config_entry in device.config_entries: + if not (entry_data := get_entry_data(hass).get(config_entry)): + continue + + if coordinator := entry_data.rpc: + return coordinator + + return None diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py index e94cd6a9e86..66b95a7a7fd 100644 --- a/homeassistant/components/shelly/cover.py +++ b/homeassistant/components/shelly/cover.py @@ -15,8 +15,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC -from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator +from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data from .entity import ShellyBlockEntity, ShellyRpcEntity from .utils import get_device_entry_gen, get_rpc_key_ids @@ -40,7 +39,8 @@ def async_setup_block_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up cover for device.""" - coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK] + coordinator = get_entry_data(hass)[config_entry.entry_id].block + assert coordinator and coordinator.device.blocks blocks = [block for block in coordinator.device.blocks if block.type == "roller"] if not blocks: @@ -56,8 +56,8 @@ def async_setup_rpc_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for RPC device.""" - coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC] - + coordinator = get_entry_data(hass)[config_entry.entry_id].rpc + assert coordinator cover_key_ids = get_rpc_key_ids(coordinator.device.status, "cover") if not cover_key_ids: diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py index 32b3432b0aa..1f41483efc0 100644 --- a/homeassistant/components/shelly/device_trigger.py +++ b/homeassistant/components/shelly/device_trigger.py @@ -22,7 +22,6 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType -from . import get_block_device_coordinator, get_rpc_device_coordinator from .const import ( ATTR_CHANNEL, ATTR_CLICK_TYPE, @@ -34,6 +33,10 @@ from .const import ( RPC_INPUTS_EVENTS_TYPES, SHBTN_MODELS, ) +from .coordinator import ( + get_block_coordinator_by_device_id, + get_rpc_coordinator_by_device_id, +) from .utils import ( get_block_input_triggers, get_rpc_input_triggers, @@ -78,7 +81,7 @@ async def async_validate_trigger_config( trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) if config[CONF_TYPE] in RPC_INPUTS_EVENTS_TYPES: - rpc_coordinator = get_rpc_device_coordinator(hass, config[CONF_DEVICE_ID]) + rpc_coordinator = get_rpc_coordinator_by_device_id(hass, config[CONF_DEVICE_ID]) if not rpc_coordinator or not rpc_coordinator.device.initialized: return config @@ -87,7 +90,9 @@ async def async_validate_trigger_config( return config elif config[CONF_TYPE] in BLOCK_INPUTS_EVENTS_TYPES: - block_coordinator = get_block_device_coordinator(hass, config[CONF_DEVICE_ID]) + block_coordinator = get_block_coordinator_by_device_id( + hass, config[CONF_DEVICE_ID] + ) if not block_coordinator or not block_coordinator.device.initialized: return config @@ -109,12 +114,12 @@ async def async_get_triggers( """List device triggers for Shelly devices.""" triggers: list[dict[str, str]] = [] - if rpc_coordinator := get_rpc_device_coordinator(hass, device_id): + if rpc_coordinator := get_rpc_coordinator_by_device_id(hass, device_id): input_triggers = get_rpc_input_triggers(rpc_coordinator.device) append_input_triggers(triggers, input_triggers, device_id) return triggers - if block_coordinator := get_block_device_coordinator(hass, device_id): + if block_coordinator := get_block_coordinator_by_device_id(hass, device_id): if block_coordinator.model in SHBTN_MODELS: input_triggers = get_shbtn_input_triggers() append_input_triggers(triggers, input_triggers, device_id) diff --git a/homeassistant/components/shelly/diagnostics.py b/homeassistant/components/shelly/diagnostics.py index 114522e31ac..6e5f8d139a2 100644 --- a/homeassistant/components/shelly/diagnostics.py +++ b/homeassistant/components/shelly/diagnostics.py @@ -6,8 +6,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC -from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator +from .coordinator import get_entry_data TO_REDACT = {CONF_USERNAME, CONF_PASSWORD} @@ -16,12 +15,13 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict: """Return diagnostics for a config entry.""" - data: dict = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] + shelly_entry_data = get_entry_data(hass)[entry.entry_id] device_settings: str | dict = "not initialized" device_status: str | dict = "not initialized" - if BLOCK in data: - block_coordinator: ShellyBlockCoordinator = data[BLOCK] + if shelly_entry_data.block: + block_coordinator = shelly_entry_data.block + assert block_coordinator device_info = { "name": block_coordinator.name, "model": block_coordinator.model, @@ -51,7 +51,8 @@ async def async_get_config_entry_diagnostics( ] } else: - rpc_coordinator: ShellyRpcCoordinator = data[RPC] + rpc_coordinator = shelly_entry_data.rpc + assert rpc_coordinator device_info = { "name": rpc_coordinator.name, "model": rpc_coordinator.model, diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 21512238cd4..c27f0210e6a 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -18,21 +18,12 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - AIOSHELLY_DEVICE_TIMEOUT_SEC, - BLOCK, - DATA_CONFIG_ENTRY, - DOMAIN, - LOGGER, - REST, - RPC, - RPC_POLL, -) +from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, LOGGER from .coordinator import ( ShellyBlockCoordinator, - ShellyRestCoordinator, ShellyRpcCoordinator, ShellyRpcPollingCoordinator, + get_entry_data, ) from .utils import ( async_remove_shelly_entity, @@ -54,10 +45,8 @@ def async_setup_entry_attribute_entities( ], ) -> None: """Set up entities for attributes.""" - coordinator: ShellyBlockCoordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ - config_entry.entry_id - ][BLOCK] - + coordinator = get_entry_data(hass)[config_entry.entry_id].block + assert coordinator if coordinator.device.initialized: async_setup_block_attribute_entities( hass, async_add_entities, coordinator, sensors, sensor_class @@ -166,13 +155,10 @@ def async_setup_entry_rpc( sensor_class: Callable, ) -> None: """Set up entities for REST sensors.""" - coordinator: ShellyRpcCoordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ - config_entry.entry_id - ][RPC] - - polling_coordinator: ShellyRpcPollingCoordinator = hass.data[DOMAIN][ - DATA_CONFIG_ENTRY - ][config_entry.entry_id][RPC_POLL] + coordinator = get_entry_data(hass)[config_entry.entry_id].rpc + assert coordinator + polling_coordinator = get_entry_data(hass)[config_entry.entry_id].rpc_poll + assert polling_coordinator entities = [] for sensor_id in sensors: @@ -220,10 +206,8 @@ def async_setup_entry_rest( sensor_class: Callable, ) -> None: """Set up entities for REST sensors.""" - coordinator: ShellyRestCoordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ - config_entry.entry_id - ][REST] - + coordinator = get_entry_data(hass)[config_entry.entry_id].rest + assert coordinator entities = [] for sensor_id in sensors: description = sensors.get(sensor_id) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 0d0ab5dd029..6c479ebc63f 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -26,9 +26,6 @@ from homeassistant.util.color import ( ) from .const import ( - BLOCK, - DATA_CONFIG_ENTRY, - DOMAIN, DUAL_MODE_LIGHT_MODELS, FIRMWARE_PATTERN, KELVIN_MAX_VALUE, @@ -39,11 +36,10 @@ from .const import ( MAX_TRANSITION_TIME, MODELS_SUPPORTING_LIGHT_TRANSITION, RGBW_MODELS, - RPC, SHBLB_1_RGB_EFFECTS, STANDARD_RGB_EFFECTS, ) -from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator +from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data from .entity import ShellyBlockEntity, ShellyRpcEntity from .utils import ( async_remove_shelly_entity, @@ -77,8 +73,8 @@ def async_setup_block_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for block device.""" - coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK] - + coordinator = get_entry_data(hass)[config_entry.entry_id].block + assert coordinator blocks = [] assert coordinator.device.blocks for block in coordinator.device.blocks: @@ -108,7 +104,8 @@ def async_setup_rpc_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for RPC device.""" - coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC] + coordinator = get_entry_data(hass)[config_entry.entry_id].rpc + assert coordinator switch_key_ids = get_rpc_key_ids(coordinator.device.status, "switch") switch_ids = [] diff --git a/homeassistant/components/shelly/logbook.py b/homeassistant/components/shelly/logbook.py index c5da9d579f8..38465112345 100644 --- a/homeassistant/components/shelly/logbook.py +++ b/homeassistant/components/shelly/logbook.py @@ -8,7 +8,6 @@ from homeassistant.const import ATTR_DEVICE_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.typing import EventType -from . import get_block_device_coordinator, get_rpc_device_coordinator from .const import ( ATTR_CHANNEL, ATTR_CLICK_TYPE, @@ -18,6 +17,10 @@ from .const import ( EVENT_SHELLY_CLICK, RPC_INPUTS_EVENTS_TYPES, ) +from .coordinator import ( + get_block_coordinator_by_device_id, + get_rpc_coordinator_by_device_id, +) from .utils import get_block_device_name, get_rpc_entity_name @@ -37,13 +40,13 @@ def async_describe_events( input_name = f"{event.data[ATTR_DEVICE]} channel {channel}" if click_type in RPC_INPUTS_EVENTS_TYPES: - rpc_coordinator = get_rpc_device_coordinator(hass, device_id) + rpc_coordinator = get_rpc_coordinator_by_device_id(hass, device_id) if rpc_coordinator and rpc_coordinator.device.initialized: key = f"input:{channel-1}" input_name = get_rpc_entity_name(rpc_coordinator.device, key) elif click_type in BLOCK_INPUTS_EVENTS_TYPES: - block_coordinator = get_block_device_coordinator(hass, device_id) + block_coordinator = get_block_coordinator_by_device_id(hass, device_id) if block_coordinator and block_coordinator.device.initialized: device_name = get_block_device_name(block_coordinator.device) input_name = f"{device_name} channel {channel}" diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 16f3ca9c163..39e754eaf86 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -10,8 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC -from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator +from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data from .entity import ShellyBlockEntity, ShellyRpcEntity from .utils import ( async_remove_shelly_entity, @@ -41,7 +40,8 @@ def async_setup_block_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for block device.""" - coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK] + coordinator = get_entry_data(hass)[config_entry.entry_id].block + assert coordinator # In roller mode the relay blocks exist but do not contain required info if ( @@ -75,8 +75,8 @@ def async_setup_rpc_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entities for RPC device.""" - coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC] - + coordinator = get_entry_data(hass)[config_entry.entry_id].rpc + assert coordinator switch_key_ids = get_rpc_key_ids(coordinator.device.status, "switch") switch_ids = [] diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index d9048594a04..faceea62a59 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -17,8 +17,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import BLOCK, CONF_SLEEP_PERIOD, DATA_CONFIG_ENTRY, DOMAIN -from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator +from .const import CONF_SLEEP_PERIOD +from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data from .entity import ( RestEntityDescription, RpcEntityDescription, @@ -178,11 +178,9 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity): ) -> None: """Install the latest firmware version.""" config_entry = self.block_coordinator.entry - block_coordinator = self.hass.data[DOMAIN][DATA_CONFIG_ENTRY][ - config_entry.entry_id - ].get(BLOCK) + coordinator = get_entry_data(self.hass)[config_entry.entry_id].block self._in_progress_old_version = self.installed_version - await self.entity_description.install(block_coordinator) + await self.entity_description.install(coordinator) class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): From 1e39f42df5d75e4eeae7a0033beb4c3c0f23ea51 Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Thu, 6 Oct 2022 11:55:17 +0200 Subject: [PATCH 192/985] Add default ports for Nibe heatpump (#79695) --- homeassistant/components/nibe_heatpump/config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nibe_heatpump/config_flow.py b/homeassistant/components/nibe_heatpump/config_flow.py index 28fafdb3a37..f8e4974b79b 100644 --- a/homeassistant/components/nibe_heatpump/config_flow.py +++ b/homeassistant/components/nibe_heatpump/config_flow.py @@ -31,9 +31,9 @@ STEP_USER_DATA_SCHEMA = vol.Schema( { vol.Required(CONF_MODEL): vol.In([e.name for e in Model]), vol.Required(CONF_IP_ADDRESS): str, - vol.Required(CONF_LISTENING_PORT): cv.port, - vol.Required(CONF_REMOTE_READ_PORT): cv.port, - vol.Required(CONF_REMOTE_WRITE_PORT): cv.port, + vol.Required(CONF_LISTENING_PORT, default=9999): cv.port, + vol.Required(CONF_REMOTE_READ_PORT, default=9999): cv.port, + vol.Required(CONF_REMOTE_WRITE_PORT, default=10000): cv.port, } ) From 47d0598e75487f63901931875f69f802a477df13 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 6 Oct 2022 12:22:39 +0200 Subject: [PATCH 193/985] Use Kelvin as the preferred color temperature unit (#79591) * Use Kelvin as the preferred white temperature unit * Update homekit * Adjust tests --- .../components/homekit/type_lights.py | 5 +- homeassistant/components/light/__init__.py | 118 +++++++++---- homeassistant/util/color.py | 45 +++-- .../test_nanoleaf_strip_nl55.py | 2 + tests/components/light/test_init.py | 40 ++++- tests/components/yeelight/test_light.py | 89 +++++++++- tests/util/test_color.py | 161 +++++++++--------- 7 files changed, 325 insertions(+), 135 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 4b433a5dc3a..7ccf3dcc38f 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -193,7 +193,10 @@ class Light(HomeAccessory): params[ATTR_COLOR_TEMP] = temp elif self.rgbww_supported: params[ATTR_RGBWW_COLOR] = color_temperature_to_rgbww( - temp, bright_val, self.min_mireds, self.max_mireds + color_temperature_mired_to_kelvin(temp), + bright_val, + color_temperature_mired_to_kelvin(self.max_mireds), + color_temperature_mired_to_kelvin(self.min_mireds), ) elif self.rgbw_supported: params[ATTR_RGBW_COLOR] = (*(0,) * 3, bright_val) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 7d34c607b1f..866c3338a75 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -196,10 +196,13 @@ ATTR_RGBW_COLOR = "rgbw_color" ATTR_RGBWW_COLOR = "rgbww_color" ATTR_XY_COLOR = "xy_color" ATTR_HS_COLOR = "hs_color" -ATTR_COLOR_TEMP = "color_temp" -ATTR_KELVIN = "kelvin" -ATTR_MIN_MIREDS = "min_mireds" -ATTR_MAX_MIREDS = "max_mireds" +ATTR_COLOR_TEMP = "color_temp" # Deprecated in HA Core 2022.11 +ATTR_KELVIN = "kelvin" # Deprecated in HA Core 2022.11 +ATTR_MIN_MIREDS = "min_mireds" # Deprecated in HA Core 2022.11 +ATTR_MAX_MIREDS = "max_mireds" # Deprecated in HA Core 2022.11 +ATTR_COLOR_TEMP_KELVIN = "color_temp_kelvin" +ATTR_MIN_COLOR_TEMP_KELVIN = "min_color_temp_kelvin" +ATTR_MAX_COLOR_TEMP_KELVIN = "max_color_temp_kelvin" ATTR_COLOR_NAME = "color_name" ATTR_WHITE = "white" @@ -249,6 +252,7 @@ LIGHT_TURN_ON_SCHEMA = { vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All( vol.Coerce(int), vol.Range(min=1) ), + vol.Exclusive(ATTR_COLOR_TEMP_KELVIN, COLOR_GROUP): cv.positive_int, vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): cv.positive_int, vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( vol.Coerce(tuple), @@ -309,9 +313,20 @@ def preprocess_turn_on_alternatives( _LOGGER.warning("Got unknown color %s, falling back to white", color_name) params[ATTR_RGB_COLOR] = (255, 255, 255) + if (mired := params.pop(ATTR_COLOR_TEMP, None)) is not None: + kelvin = color_util.color_temperature_mired_to_kelvin(mired) + params[ATTR_COLOR_TEMP] = int(mired) + params[ATTR_COLOR_TEMP_KELVIN] = int(kelvin) + if (kelvin := params.pop(ATTR_KELVIN, None)) is not None: mired = color_util.color_temperature_kelvin_to_mired(kelvin) params[ATTR_COLOR_TEMP] = int(mired) + params[ATTR_COLOR_TEMP_KELVIN] = int(kelvin) + + if (kelvin := params.pop(ATTR_COLOR_TEMP_KELVIN, None)) is not None: + mired = color_util.color_temperature_kelvin_to_mired(kelvin) + params[ATTR_COLOR_TEMP] = int(mired) + params[ATTR_COLOR_TEMP_KELVIN] = int(kelvin) brightness_pct = params.pop(ATTR_BRIGHTNESS_PCT, None) if brightness_pct is not None: @@ -350,6 +365,7 @@ def filter_turn_on_params(light: LightEntity, params: dict[str, Any]) -> dict[st params.pop(ATTR_BRIGHTNESS, None) if ColorMode.COLOR_TEMP not in supported_color_modes: params.pop(ATTR_COLOR_TEMP, None) + params.pop(ATTR_COLOR_TEMP_KELVIN, None) if ColorMode.HS not in supported_color_modes: params.pop(ATTR_HS_COLOR, None) if ColorMode.RGB not in supported_color_modes: @@ -424,22 +440,28 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: supported_color_modes = light.supported_color_modes # If a color temperature is specified, emulate it if not supported by the light - if ATTR_COLOR_TEMP in params: + if ATTR_COLOR_TEMP_KELVIN in params: if ( supported_color_modes and ColorMode.COLOR_TEMP not in supported_color_modes and ColorMode.RGBWW in supported_color_modes ): - color_temp = params.pop(ATTR_COLOR_TEMP) + params.pop(ATTR_COLOR_TEMP) + color_temp = params.pop(ATTR_COLOR_TEMP_KELVIN) brightness = params.get(ATTR_BRIGHTNESS, light.brightness) params[ATTR_RGBWW_COLOR] = color_util.color_temperature_to_rgbww( - color_temp, brightness, light.min_mireds, light.max_mireds + color_temp, + brightness, + light.min_color_temp_kelvin, + light.max_color_temp_kelvin, ) elif ColorMode.COLOR_TEMP not in legacy_supported_color_modes: - color_temp = params.pop(ATTR_COLOR_TEMP) + params.pop(ATTR_COLOR_TEMP) + color_temp = params.pop(ATTR_COLOR_TEMP_KELVIN) if color_supported(legacy_supported_color_modes): - temp_k = color_util.color_temperature_mired_to_kelvin(color_temp) - params[ATTR_HS_COLOR] = color_util.color_temperature_to_hs(temp_k) + params[ATTR_HS_COLOR] = color_util.color_temperature_to_hs( + color_temp + ) # If a color is specified, convert to the color space supported by the light # Backwards compatibility: Fall back to hs color if light.supported_color_modes @@ -457,7 +479,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: elif (rgbww_color := params.pop(ATTR_RGBWW_COLOR, None)) is not None: # https://github.com/python/mypy/issues/13673 rgb_color = color_util.color_rgbww_to_rgb( # type: ignore[call-arg] - *rgbww_color, light.min_mireds, light.max_mireds + *rgbww_color, + light.min_color_temp_kelvin, + light.max_color_temp_kelvin, ) params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) elif ATTR_HS_COLOR in params and ColorMode.HS not in supported_color_modes: @@ -470,7 +494,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: elif ColorMode.RGBWW in supported_color_modes: rgb_color = color_util.color_hs_to_RGB(*hs_color) params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( - *rgb_color, light.min_mireds, light.max_mireds + *rgb_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin ) elif ColorMode.XY in supported_color_modes: params[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color) @@ -481,7 +505,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: elif ColorMode.RGBWW in supported_color_modes: # https://github.com/python/mypy/issues/13673 params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( # type: ignore[call-arg] - *rgb_color, light.min_mireds, light.max_mireds + *rgb_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin ) elif ColorMode.HS in supported_color_modes: params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) @@ -499,7 +523,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: elif ColorMode.RGBWW in supported_color_modes: rgb_color = color_util.color_xy_to_RGB(*xy_color) params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( - *rgb_color, light.min_mireds, light.max_mireds + *rgb_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin ) elif ATTR_RGBW_COLOR in params and ColorMode.RGBW not in supported_color_modes: rgbw_color = params.pop(ATTR_RGBW_COLOR) @@ -508,7 +532,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: params[ATTR_RGB_COLOR] = rgb_color elif ColorMode.RGBWW in supported_color_modes: params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( - *rgb_color, light.min_mireds, light.max_mireds + *rgb_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin ) elif ColorMode.HS in supported_color_modes: params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) @@ -520,7 +544,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: assert (rgbww_color := params.pop(ATTR_RGBWW_COLOR)) is not None # https://github.com/python/mypy/issues/13673 rgb_color = color_util.color_rgbww_to_rgb( # type: ignore[call-arg] - *rgbww_color, light.min_mireds, light.max_mireds + *rgbww_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin ) if ColorMode.RGB in supported_color_modes: params[ATTR_RGB_COLOR] = rgb_color @@ -755,11 +779,16 @@ class LightEntity(ToggleEntity): _attr_brightness: int | None = None _attr_color_mode: ColorMode | str | None = None _attr_color_temp: int | None = None + _attr_color_temp_kelvin: int | None = None _attr_effect_list: list[str] | None = None _attr_effect: str | None = None _attr_hs_color: tuple[float, float] | None = None - _attr_max_mireds: int = 500 - _attr_min_mireds: int = 153 + # Default to the Philips Hue value that HA has always assumed + # https://developers.meethue.com/documentation/core-concepts + _attr_max_color_temp_kelvin: int | None = None + _attr_min_color_temp_kelvin: int | None = None + _attr_max_mireds: int = 500 # 2000 K + _attr_min_mireds: int = 153 # 6535 K _attr_rgb_color: tuple[int, int, int] | None = None _attr_rgbw_color: tuple[int, int, int, int] | None = None _attr_rgbww_color: tuple[int, int, int, int, int] | None = None @@ -787,7 +816,7 @@ class LightEntity(ToggleEntity): if ColorMode.HS in supported and self.hs_color is not None: return ColorMode.HS - if ColorMode.COLOR_TEMP in supported and self.color_temp is not None: + if ColorMode.COLOR_TEMP in supported and self.color_temp_kelvin is not None: return ColorMode.COLOR_TEMP if ColorMode.BRIGHTNESS in supported and self.brightness is not None: return ColorMode.BRIGHTNESS @@ -833,20 +862,37 @@ class LightEntity(ToggleEntity): """Return the CT color value in mireds.""" return self._attr_color_temp + @property + def color_temp_kelvin(self) -> int | None: + """Return the CT color value in Kelvin.""" + if self._attr_color_temp_kelvin is None and self.color_temp: + return color_util.color_temperature_mired_to_kelvin(self.color_temp) + return self._attr_color_temp_kelvin + @property def min_mireds(self) -> int: """Return the coldest color_temp that this light supports.""" - # Default to the Philips Hue value that HA has always assumed - # https://developers.meethue.com/documentation/core-concepts return self._attr_min_mireds @property def max_mireds(self) -> int: """Return the warmest color_temp that this light supports.""" - # Default to the Philips Hue value that HA has always assumed - # https://developers.meethue.com/documentation/core-concepts return self._attr_max_mireds + @property + def min_color_temp_kelvin(self) -> int: + """Return the warmest color_temp_kelvin that this light supports.""" + if self._attr_min_color_temp_kelvin is None: + return color_util.color_temperature_mired_to_kelvin(self.max_mireds) + return self._attr_min_color_temp_kelvin + + @property + def max_color_temp_kelvin(self) -> int: + """Return the coldest color_temp_kelvin that this light supports.""" + if self._attr_min_color_temp_kelvin is None: + return color_util.color_temperature_mired_to_kelvin(self.min_mireds) + return self._attr_min_color_temp_kelvin + @property def effect_list(self) -> list[str] | None: """Return the list of supported effects.""" @@ -867,6 +913,8 @@ class LightEntity(ToggleEntity): if ColorMode.COLOR_TEMP in supported_color_modes: data[ATTR_MIN_MIREDS] = self.min_mireds data[ATTR_MAX_MIREDS] = self.max_mireds + data[ATTR_MIN_COLOR_TEMP_KELVIN] = self.min_color_temp_kelvin + data[ATTR_MAX_COLOR_TEMP_KELVIN] = self.max_color_temp_kelvin if supported_features & LightEntityFeature.EFFECT: data[ATTR_EFFECT_LIST] = self.effect_list @@ -904,16 +952,14 @@ class LightEntity(ToggleEntity): elif color_mode == ColorMode.RGBWW and self.rgbww_color: rgbww_color = self.rgbww_color rgb_color = color_util.color_rgbww_to_rgb( - *rgbww_color, self.min_mireds, self.max_mireds + *rgbww_color, self.min_color_temp_kelvin, self.max_color_temp_kelvin ) data[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) data[ATTR_RGB_COLOR] = tuple(int(x) for x in rgb_color[0:3]) data[ATTR_RGBWW_COLOR] = tuple(int(x) for x in rgbww_color[0:5]) data[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color) - elif color_mode == ColorMode.COLOR_TEMP and self.color_temp: - hs_color = color_util.color_temperature_to_hs( - color_util.color_temperature_mired_to_kelvin(self.color_temp) - ) + elif color_mode == ColorMode.COLOR_TEMP and self.color_temp_kelvin: + hs_color = color_util.color_temperature_to_hs(self.color_temp_kelvin) data[ATTR_HS_COLOR] = (round(hs_color[0], 3), round(hs_color[1], 3)) data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color) data[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color) @@ -949,7 +995,13 @@ class LightEntity(ToggleEntity): data[ATTR_BRIGHTNESS] = self.brightness if color_mode == ColorMode.COLOR_TEMP: - data[ATTR_COLOR_TEMP] = self.color_temp + data[ATTR_COLOR_TEMP_KELVIN] = self.color_temp_kelvin + if not self.color_temp_kelvin: + data[ATTR_COLOR_TEMP] = None + else: + data[ATTR_COLOR_TEMP] = color_util.color_temperature_kelvin_to_mired( + self.color_temp_kelvin + ) if color_mode in COLOR_MODES_COLOR or color_mode == ColorMode.COLOR_TEMP: data.update(self._light_internal_convert_color(color_mode)) @@ -957,7 +1009,13 @@ class LightEntity(ToggleEntity): if supported_features & SUPPORT_COLOR_TEMP and not self.supported_color_modes: # Backwards compatibility # Add warning in 2021.6, remove in 2021.10 - data[ATTR_COLOR_TEMP] = self.color_temp + data[ATTR_COLOR_TEMP_KELVIN] = self.color_temp_kelvin + if not self.color_temp_kelvin: + data[ATTR_COLOR_TEMP] = None + else: + data[ATTR_COLOR_TEMP] = color_util.color_temperature_kelvin_to_mired( + self.color_temp_kelvin + ) if supported_features & LightEntityFeature.EFFECT: data[ATTR_EFFECT] = self.effect diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 494ee04546c..3823c0e45bd 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -436,10 +436,12 @@ def color_rgbw_to_rgb(r: int, g: int, b: int, w: int) -> tuple[int, int, int]: def color_rgb_to_rgbww( - r: int, g: int, b: int, min_mireds: int, max_mireds: int + r: int, g: int, b: int, min_kelvin: int, max_kelvin: int ) -> tuple[int, int, int, int, int]: """Convert an rgb color to an rgbww representation.""" # Find the color temperature when both white channels have equal brightness + max_mireds = color_temperature_kelvin_to_mired(min_kelvin) + min_mireds = color_temperature_kelvin_to_mired(max_kelvin) mired_range = max_mireds - min_mireds mired_midpoint = min_mireds + mired_range / 2 color_temp_kelvin = color_temperature_mired_to_kelvin(mired_midpoint) @@ -460,10 +462,12 @@ def color_rgb_to_rgbww( def color_rgbww_to_rgb( - r: int, g: int, b: int, cw: int, ww: int, min_mireds: int, max_mireds: int + r: int, g: int, b: int, cw: int, ww: int, min_kelvin: int, max_kelvin: int ) -> tuple[int, int, int]: """Convert an rgbww color to an rgb representation.""" # Calculate color temperature of the white channels + max_mireds = color_temperature_kelvin_to_mired(min_kelvin) + min_mireds = color_temperature_kelvin_to_mired(max_kelvin) mired_range = max_mireds - min_mireds try: ct_ratio = ww / (cw + ww) @@ -530,9 +534,15 @@ def color_temperature_to_rgb( def color_temperature_to_rgbww( - temperature: int, brightness: int, min_mireds: int, max_mireds: int + temperature: int, brightness: int, min_kelvin: int, max_kelvin: int ) -> tuple[int, int, int, int, int]: - """Convert color temperature in mireds to rgbcw.""" + """Convert color temperature in kelvin to rgbcw. + + Returns a (r, g, b, cw, ww) tuple. + """ + max_mireds = color_temperature_kelvin_to_mired(min_kelvin) + min_mireds = color_temperature_kelvin_to_mired(max_kelvin) + temperature = color_temperature_kelvin_to_mired(temperature) mired_range = max_mireds - min_mireds cold = ((max_mireds - temperature) / mired_range) * brightness warm = brightness - cold @@ -540,22 +550,33 @@ def color_temperature_to_rgbww( def rgbww_to_color_temperature( - rgbww: tuple[int, int, int, int, int], min_mireds: int, max_mireds: int + rgbww: tuple[int, int, int, int, int], min_kelvin: int, max_kelvin: int ) -> tuple[int, int]: - """Convert rgbcw to color temperature in mireds.""" + """Convert rgbcw to color temperature in kelvin. + + Returns a tuple (color_temperature, brightness). + """ _, _, _, cold, warm = rgbww - return while_levels_to_color_temperature(cold, warm, min_mireds, max_mireds) + return _white_levels_to_color_temperature(cold, warm, min_kelvin, max_kelvin) -def while_levels_to_color_temperature( - cold: int, warm: int, min_mireds: int, max_mireds: int +def _white_levels_to_color_temperature( + cold: int, warm: int, min_kelvin: int, max_kelvin: int ) -> tuple[int, int]: - """Convert whites to color temperature in mireds.""" + """Convert whites to color temperature in kelvin. + + Returns a tuple (color_temperature, brightness). + """ + max_mireds = color_temperature_kelvin_to_mired(min_kelvin) + min_mireds = color_temperature_kelvin_to_mired(max_kelvin) brightness = warm / 255 + cold / 255 if brightness == 0: - return (max_mireds, 0) + # Return the warmest color if brightness is 0 + return (min_kelvin, 0) return round( - ((cold / 255 / brightness) * (min_mireds - max_mireds)) + max_mireds + color_temperature_mired_to_kelvin( + ((cold / 255 / brightness) * (min_mireds - max_mireds)) + max_mireds + ) ), min(255, round(brightness * 255)) diff --git a/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py index c66ea0d76a9..61d872ccd2a 100644 --- a/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py +++ b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py @@ -37,6 +37,8 @@ async def test_nanoleaf_nl55_setup(hass): unique_id="homekit-AAAA011111111111-19", supported_features=0, capabilities={ + "max_color_temp_kelvin": 6535, + "min_color_temp_kelvin": 2127, "max_mireds": 470, "min_mireds": 153, "supported_color_modes": ["color_temp", "hs"], diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 1f21981340f..3a7f9cfccb8 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -629,11 +629,33 @@ async def test_default_profiles_group( }, { light.ATTR_COLOR_TEMP: 600, + light.ATTR_COLOR_TEMP_KELVIN: 1666, light.ATTR_BRIGHTNESS: 11, light.ATTR_TRANSITION: 1, }, { light.ATTR_COLOR_TEMP: 600, + light.ATTR_COLOR_TEMP_KELVIN: 1666, + light.ATTR_BRIGHTNESS: 11, + light.ATTR_TRANSITION: 1, + }, + ), + ( + # Color temp in turn on params, color from profile ignored + { + light.ATTR_COLOR_TEMP_KELVIN: 6500, + light.ATTR_BRIGHTNESS: 11, + light.ATTR_TRANSITION: 1, + }, + { + light.ATTR_COLOR_TEMP: 153, + light.ATTR_COLOR_TEMP_KELVIN: 6500, + light.ATTR_BRIGHTNESS: 11, + light.ATTR_TRANSITION: 1, + }, + { + light.ATTR_COLOR_TEMP: 153, + light.ATTR_COLOR_TEMP_KELVIN: 6500, light.ATTR_BRIGHTNESS: 11, light.ATTR_TRANSITION: 1, }, @@ -1440,7 +1462,7 @@ async def test_light_service_call_color_conversion(hass, enable_custom_integrati _, data = entity5.last_call("turn_on") assert data == {"brightness": 255, "rgbw_color": (0, 0, 0, 255)} _, data = entity6.last_call("turn_on") - # The midpoint the the white channels is warm, compensated by adding green + blue + # The midpoint of the the white channels is warm, compensated by adding green + blue assert data == {"brightness": 255, "rgbww_color": (0, 76, 141, 255, 255)} await hass.services.async_call( @@ -1843,7 +1865,7 @@ async def test_light_service_call_color_temp_emulation( blocking=True, ) _, data = entity0.last_call("turn_on") - assert data == {"brightness": 255, "color_temp": 200} + assert data == {"brightness": 255, "color_temp": 200, "color_temp_kelvin": 5000} _, data = entity1.last_call("turn_on") assert data == {"brightness": 255, "hs_color": (27.001, 19.243)} _, data = entity2.last_call("turn_on") @@ -1868,6 +1890,10 @@ async def test_light_service_call_color_temp_conversion( entity1 = platform.ENTITIES[1] entity1.supported_color_modes = {light.ColorMode.RGBWW} + assert entity1.min_mireds == 153 + assert entity1.max_mireds == 500 + assert entity1.min_color_temp_kelvin == 2000 + assert entity1.max_color_temp_kelvin == 6535 assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1895,7 +1921,7 @@ async def test_light_service_call_color_temp_conversion( blocking=True, ) _, data = entity0.last_call("turn_on") - assert data == {"brightness": 255, "color_temp": 153} + assert data == {"brightness": 255, "color_temp": 153, "color_temp_kelvin": 6535} _, data = entity1.last_call("turn_on") # Home Assistant uses RGBCW so a mireds of 153 should be maximum cold at 100% brightness so 255 assert data == {"brightness": 255, "rgbww_color": (0, 0, 0, 255, 0)} @@ -1914,7 +1940,7 @@ async def test_light_service_call_color_temp_conversion( blocking=True, ) _, data = entity0.last_call("turn_on") - assert data == {"brightness": 128, "color_temp": 500} + assert data == {"brightness": 128, "color_temp": 500, "color_temp_kelvin": 2000} _, data = entity1.last_call("turn_on") # Home Assistant uses RGBCW so a mireds of 500 should be maximum warm at 50% brightness so 128 assert data == {"brightness": 128, "rgbww_color": (0, 0, 0, 0, 128)} @@ -1933,7 +1959,7 @@ async def test_light_service_call_color_temp_conversion( blocking=True, ) _, data = entity0.last_call("turn_on") - assert data == {"brightness": 255, "color_temp": 327} + assert data == {"brightness": 255, "color_temp": 327, "color_temp_kelvin": 3058} _, data = entity1.last_call("turn_on") # Home Assistant uses RGBCW so a mireds of 328 should be the midway point at 100% brightness so 127 (rounding), 128 assert data == {"brightness": 255, "rgbww_color": (0, 0, 0, 127, 128)} @@ -1952,7 +1978,7 @@ async def test_light_service_call_color_temp_conversion( blocking=True, ) _, data = entity0.last_call("turn_on") - assert data == {"brightness": 255, "color_temp": 240} + assert data == {"brightness": 255, "color_temp": 240, "color_temp_kelvin": 4166} _, data = entity1.last_call("turn_on") assert data == {"brightness": 255, "rgbww_color": (0, 0, 0, 191, 64)} @@ -1970,7 +1996,7 @@ async def test_light_service_call_color_temp_conversion( blocking=True, ) _, data = entity0.last_call("turn_on") - assert data == {"brightness": 255, "color_temp": 410} + assert data == {"brightness": 255, "color_temp": 410, "color_temp_kelvin": 2439} _, data = entity1.last_call("turn_on") assert data == {"brightness": 255, "rgbww_color": (0, 0, 0, 66, 189)} diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index fad39a052d5..f38705c6e9a 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -861,14 +861,16 @@ async def test_device_types(hass: HomeAssistant, caplog): await hass.async_block_till_done() bright = round(255 * int(PROPERTIES["bright"]) / 100) - ct = color_temperature_kelvin_to_mired(int(PROPERTIES["ct"])) + ct = int(PROPERTIES["ct"]) + ct_mired = color_temperature_kelvin_to_mired(int(PROPERTIES["ct"])) hue = int(PROPERTIES["hue"]) sat = int(PROPERTIES["sat"]) rgb = int(PROPERTIES["rgb"]) rgb_color = ((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF) hs_color = (hue, sat) bg_bright = round(255 * int(PROPERTIES["bg_bright"]) / 100) - bg_ct = color_temperature_kelvin_to_mired(int(PROPERTIES["bg_ct"])) + bg_ct = int(PROPERTIES["bg_ct"]) + bg_ct_kelvin = color_temperature_kelvin_to_mired(int(PROPERTIES["bg_ct"])) bg_hue = int(PROPERTIES["bg_hue"]) bg_sat = int(PROPERTIES["bg_sat"]) bg_rgb = int(PROPERTIES["bg_rgb"]) @@ -911,6 +913,10 @@ async def test_device_types(hass: HomeAssistant, caplog): { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, + "min_color_temp_kelvin": model_specs["color_temp"]["min"], + "max_color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) + ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), @@ -918,7 +924,8 @@ async def test_device_types(hass: HomeAssistant, caplog): model_specs["color_temp"]["min"] ), "brightness": bright, - "color_temp": ct, + "color_temp_kelvin": ct, + "color_temp": ct_mired, "color_mode": "color_temp", "supported_color_modes": ["color_temp", "hs", "rgb"], "hs_color": (26.812, 34.87), @@ -936,6 +943,10 @@ async def test_device_types(hass: HomeAssistant, caplog): "hs_color": (28.401, 100.0), "rgb_color": (255, 120, 0), "xy_color": (0.621, 0.367), + "min_color_temp_kelvin": model_specs["color_temp"]["min"], + "max_color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) + ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), @@ -945,6 +956,7 @@ async def test_device_types(hass: HomeAssistant, caplog): "brightness": nl_br, "color_mode": "color_temp", "supported_color_modes": ["color_temp", "hs", "rgb"], + "color_temp_kelvin": model_specs["color_temp"]["min"], "color_temp": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), @@ -960,6 +972,10 @@ async def test_device_types(hass: HomeAssistant, caplog): { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, + "min_color_temp_kelvin": model_specs["color_temp"]["min"], + "max_color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) + ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), @@ -989,6 +1005,10 @@ async def test_device_types(hass: HomeAssistant, caplog): { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, + "min_color_temp_kelvin": model_specs["color_temp"]["min"], + "max_color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) + ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), @@ -1019,6 +1039,10 @@ async def test_device_types(hass: HomeAssistant, caplog): { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, + "min_color_temp_kelvin": model_specs["color_temp"]["min"], + "max_color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) + ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), @@ -1046,6 +1070,10 @@ async def test_device_types(hass: HomeAssistant, caplog): { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, + "min_color_temp_kelvin": model_specs["color_temp"]["min"], + "max_color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) + ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), @@ -1072,6 +1100,10 @@ async def test_device_types(hass: HomeAssistant, caplog): { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, + "min_color_temp_kelvin": model_specs["color_temp"]["min"], + "max_color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) + ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), @@ -1097,6 +1129,12 @@ async def test_device_types(hass: HomeAssistant, caplog): { "effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, + "min_color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(model_specs["color_temp"]["min"]) + ), + "max_color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) + ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), @@ -1104,7 +1142,8 @@ async def test_device_types(hass: HomeAssistant, caplog): model_specs["color_temp"]["min"] ), "brightness": bright, - "color_temp": ct, + "color_temp_kelvin": ct, + "color_temp": ct_mired, "color_mode": "color_temp", "supported_color_modes": ["color_temp"], "hs_color": (26.812, 34.87), @@ -1120,6 +1159,12 @@ async def test_device_types(hass: HomeAssistant, caplog): nightlight_mode_properties={ "effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, + "min_color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(model_specs["color_temp"]["min"]) + ), + "max_color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) + ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), @@ -1127,6 +1172,9 @@ async def test_device_types(hass: HomeAssistant, caplog): model_specs["color_temp"]["min"] ), "brightness": nl_br, + "color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(model_specs["color_temp"]["min"]) + ), "color_temp": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), @@ -1151,6 +1199,12 @@ async def test_device_types(hass: HomeAssistant, caplog): "flowing": False, "night_light": True, "supported_features": SUPPORT_YEELIGHT, + "min_color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(model_specs["color_temp"]["min"]) + ), + "max_color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) + ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), @@ -1158,7 +1212,8 @@ async def test_device_types(hass: HomeAssistant, caplog): model_specs["color_temp"]["min"] ), "brightness": bright, - "color_temp": ct, + "color_temp_kelvin": ct, + "color_temp": ct_mired, "color_mode": "color_temp", "supported_color_modes": ["color_temp"], "hs_color": (26.812, 34.87), @@ -1177,6 +1232,12 @@ async def test_device_types(hass: HomeAssistant, caplog): "flowing": False, "night_light": True, "supported_features": SUPPORT_YEELIGHT, + "min_color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(model_specs["color_temp"]["min"]) + ), + "max_color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(model_specs["color_temp"]["max"]) + ), "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), @@ -1184,6 +1245,9 @@ async def test_device_types(hass: HomeAssistant, caplog): model_specs["color_temp"]["min"] ), "brightness": nl_br, + "color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(model_specs["color_temp"]["min"]) + ), "color_temp": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), @@ -1202,10 +1266,15 @@ async def test_device_types(hass: HomeAssistant, caplog): { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, + "min_color_temp_kelvin": 1700, + "max_color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(6500) + ), "min_mireds": color_temperature_kelvin_to_mired(6500), "max_mireds": color_temperature_kelvin_to_mired(1700), "brightness": bg_bright, - "color_temp": bg_ct, + "color_temp_kelvin": bg_ct, + "color_temp": bg_ct_kelvin, "color_mode": "color_temp", "supported_color_modes": ["color_temp", "hs", "rgb"], "hs_color": (27.001, 19.243), @@ -1224,6 +1293,10 @@ async def test_device_types(hass: HomeAssistant, caplog): { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, + "min_color_temp_kelvin": 1700, + "max_color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(6500) + ), "min_mireds": color_temperature_kelvin_to_mired(6500), "max_mireds": color_temperature_kelvin_to_mired(1700), "brightness": bg_bright, @@ -1245,6 +1318,10 @@ async def test_device_types(hass: HomeAssistant, caplog): { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, + "min_color_temp_kelvin": 1700, + "max_color_temp_kelvin": color_temperature_mired_to_kelvin( + color_temperature_kelvin_to_mired(6500) + ), "min_mireds": color_temperature_kelvin_to_mired(6500), "max_mireds": color_temperature_kelvin_to_mired(1700), "brightness": bg_bright, diff --git a/tests/util/test_color.py b/tests/util/test_color.py index b77540acc2b..95d2ffc0fd7 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -370,82 +370,84 @@ def test_get_color_in_voluptuous(): def test_color_rgb_to_rgbww(): """Test color_rgb_to_rgbww conversions.""" - assert color_util.color_rgb_to_rgbww(255, 255, 255, 154, 370) == ( + # Light with mid point at ~4600K (warm white) -> output compensated by adding blue + assert color_util.color_rgb_to_rgbww(255, 255, 255, 2702, 6493) == ( 0, 54, 98, 255, 255, ) - assert color_util.color_rgb_to_rgbww(255, 255, 255, 100, 1000) == ( + # Light with mid point at ~5500K (less warm white) -> output compensated by adding less blue + assert color_util.color_rgb_to_rgbww(255, 255, 255, 1000, 10000) == ( 255, 255, 255, 0, 0, ) - assert color_util.color_rgb_to_rgbww(255, 255, 255, 1, 1000) == ( + # Light with mid point at ~1MK (unrealistically cold white) -> output compensated by adding red + assert color_util.color_rgb_to_rgbww(255, 255, 255, 1000, 1000000) == ( 0, 118, 241, 255, 255, ) - assert color_util.color_rgb_to_rgbww(128, 128, 128, 154, 370) == ( + assert color_util.color_rgb_to_rgbww(128, 128, 128, 2702, 6493) == ( 0, 27, 49, 128, 128, ) - assert color_util.color_rgb_to_rgbww(64, 64, 64, 154, 370) == (0, 14, 25, 64, 64) - assert color_util.color_rgb_to_rgbww(32, 64, 16, 154, 370) == (9, 64, 0, 38, 38) - assert color_util.color_rgb_to_rgbww(0, 0, 0, 154, 370) == (0, 0, 0, 0, 0) - assert color_util.color_rgb_to_rgbww(0, 0, 0, 0, 100) == (0, 0, 0, 0, 0) - assert color_util.color_rgb_to_rgbww(255, 255, 255, 1, 5) == (103, 69, 0, 255, 255) + assert color_util.color_rgb_to_rgbww(64, 64, 64, 2702, 6493) == (0, 14, 25, 64, 64) + assert color_util.color_rgb_to_rgbww(32, 64, 16, 2702, 6493) == (9, 64, 0, 38, 38) + assert color_util.color_rgb_to_rgbww(0, 0, 0, 2702, 6493) == (0, 0, 0, 0, 0) + assert color_util.color_rgb_to_rgbww(0, 0, 0, 10000, 1000000) == (0, 0, 0, 0, 0) + assert color_util.color_rgb_to_rgbww(255, 255, 255, 200000, 1000000) == ( + 103, + 69, + 0, + 255, + 255, + ) def test_color_rgbww_to_rgb(): """Test color_rgbww_to_rgb conversions.""" - assert color_util.color_rgbww_to_rgb(0, 54, 98, 255, 255, 154, 370) == ( + assert color_util.color_rgbww_to_rgb(0, 54, 98, 255, 255, 2702, 6493) == ( 255, 255, 255, ) - assert color_util.color_rgbww_to_rgb(255, 255, 255, 0, 0, 154, 370) == ( + # rgb fully on, + both white channels turned off -> rgb fully on + assert color_util.color_rgbww_to_rgb(255, 255, 255, 0, 0, 2702, 6493) == ( 255, 255, 255, ) - assert color_util.color_rgbww_to_rgb(0, 118, 241, 255, 255, 154, 370) == ( + # r < g < b + both white channels fully enabled -> r < g < b capped at 255 + assert color_util.color_rgbww_to_rgb(0, 118, 241, 255, 255, 2702, 6493) == ( 163, 204, 255, ) - assert color_util.color_rgbww_to_rgb(0, 27, 49, 128, 128, 154, 370) == ( + # r < g < b + both white channels 50% enabled -> r < g < b capped at 128 + assert color_util.color_rgbww_to_rgb(0, 27, 49, 128, 128, 2702, 6493) == ( 128, 128, 128, ) - assert color_util.color_rgbww_to_rgb(0, 14, 25, 64, 64, 154, 370) == (64, 64, 64) - assert color_util.color_rgbww_to_rgb(9, 64, 0, 38, 38, 154, 370) == (32, 64, 16) - assert color_util.color_rgbww_to_rgb(0, 0, 0, 0, 0, 154, 370) == (0, 0, 0) - assert color_util.color_rgbww_to_rgb(103, 69, 0, 255, 255, 153, 370) == ( + # r < g < b + both white channels 25% enabled -> r < g < b capped at 64 + assert color_util.color_rgbww_to_rgb(0, 14, 25, 64, 64, 2702, 6493) == (64, 64, 64) + assert color_util.color_rgbww_to_rgb(9, 64, 0, 38, 38, 2702, 6493) == (32, 64, 16) + assert color_util.color_rgbww_to_rgb(0, 0, 0, 0, 0, 2702, 6493) == (0, 0, 0) + assert color_util.color_rgbww_to_rgb(103, 69, 0, 255, 255, 2702, 6535) == ( 255, 193, 112, ) - assert color_util.color_rgbww_to_rgb(255, 255, 255, 0, 0, 0, 0) == (255, 255, 255) - assert color_util.color_rgbww_to_rgb(255, 255, 255, 255, 255, 0, 0) == ( - 255, - 161, - 128, - ) - assert color_util.color_rgbww_to_rgb(255, 255, 255, 255, 255, 0, 370) == ( - 255, - 245, - 237, - ) def test_color_temperature_to_rgbww(): @@ -454,42 +456,45 @@ def test_color_temperature_to_rgbww(): Temperature values must be in mireds Home Assistant uses rgbcw for rgbww """ - assert color_util.color_temperature_to_rgbww(153, 255, 153, 500) == ( + # Coldest color temperature -> only cold channel enabled + assert color_util.color_temperature_to_rgbww(6535, 255, 2000, 6535) == ( 0, 0, 0, 255, 0, ) - assert color_util.color_temperature_to_rgbww(153, 128, 153, 500) == ( + assert color_util.color_temperature_to_rgbww(6535, 128, 2000, 6535) == ( 0, 0, 0, 128, 0, ) - assert color_util.color_temperature_to_rgbww(500, 255, 153, 500) == ( + # Warmest color temperature -> only cold channel enabled + assert color_util.color_temperature_to_rgbww(2000, 255, 2000, 6535) == ( 0, 0, 0, 0, 255, ) - assert color_util.color_temperature_to_rgbww(500, 128, 153, 500) == ( + assert color_util.color_temperature_to_rgbww(2000, 128, 2000, 6535) == ( 0, 0, 0, 0, 128, ) - assert color_util.color_temperature_to_rgbww(347, 255, 153, 500) == ( + # Warmer than mid point color temperature -> More warm than cold channel enabled + assert color_util.color_temperature_to_rgbww(2881, 255, 2000, 6535) == ( 0, 0, 0, 112, 143, ) - assert color_util.color_temperature_to_rgbww(347, 128, 153, 500) == ( + assert color_util.color_temperature_to_rgbww(2881, 128, 2000, 6535) == ( 0, 0, 0, @@ -504,39 +509,36 @@ def test_rgbww_to_color_temperature(): Temperature values must be in mireds Home Assistant uses rgbcw for rgbww """ - assert color_util.rgbww_to_color_temperature( - ( - 0, - 0, - 0, - 255, - 0, - ), - 153, - 500, - ) == (153, 255) - assert color_util.rgbww_to_color_temperature((0, 0, 0, 128, 0), 153, 500) == ( - 153, - 128, - ) - assert color_util.rgbww_to_color_temperature((0, 0, 0, 0, 255), 153, 500) == ( - 500, + # Only cold channel enabled -> coldest color temperature + assert color_util.rgbww_to_color_temperature((0, 0, 0, 255, 0), 2000, 6535) == ( + 6535, 255, ) - assert color_util.rgbww_to_color_temperature((0, 0, 0, 0, 128), 153, 500) == ( - 500, + assert color_util.rgbww_to_color_temperature((0, 0, 0, 128, 0), 2000, 6535) == ( + 6535, 128, ) - assert color_util.rgbww_to_color_temperature((0, 0, 0, 112, 143), 153, 500) == ( - 348, + # Only warm channel enabled -> warmest color temperature + assert color_util.rgbww_to_color_temperature((0, 0, 0, 0, 255), 2000, 6535) == ( + 2000, 255, ) - assert color_util.rgbww_to_color_temperature((0, 0, 0, 56, 72), 153, 500) == ( - 348, + assert color_util.rgbww_to_color_temperature((0, 0, 0, 0, 128), 2000, 6535) == ( + 2000, 128, ) - assert color_util.rgbww_to_color_temperature((0, 0, 0, 0, 0), 153, 500) == ( - 500, + # More warm than cold channel enabled -> warmer than mid point + assert color_util.rgbww_to_color_temperature((0, 0, 0, 112, 143), 2000, 6535) == ( + 2876, + 255, + ) + assert color_util.rgbww_to_color_temperature((0, 0, 0, 56, 72), 2000, 6535) == ( + 2872, + 128, + ) + # Both channels turned off -> warmest color temperature + assert color_util.rgbww_to_color_temperature((0, 0, 0, 0, 0), 2000, 6535) == ( + 2000, 0, ) @@ -547,33 +549,34 @@ def test_white_levels_to_color_temperature(): Temperature values must be in mireds Home Assistant uses rgbcw for rgbww """ - assert color_util.while_levels_to_color_temperature( - 255, - 0, - 153, - 500, - ) == (153, 255) - assert color_util.while_levels_to_color_temperature(128, 0, 153, 500) == ( - 153, - 128, - ) - assert color_util.while_levels_to_color_temperature(0, 255, 153, 500) == ( - 500, + # Only cold channel enabled -> coldest color temperature + assert color_util._white_levels_to_color_temperature(255, 0, 2000, 6535) == ( + 6535, 255, ) - assert color_util.while_levels_to_color_temperature(0, 128, 153, 500) == ( - 500, + assert color_util._white_levels_to_color_temperature(128, 0, 2000, 6535) == ( + 6535, 128, ) - assert color_util.while_levels_to_color_temperature(112, 143, 153, 500) == ( - 348, + # Only warm channel enabled -> warmest color temperature + assert color_util._white_levels_to_color_temperature(0, 255, 2000, 6535) == ( + 2000, 255, ) - assert color_util.while_levels_to_color_temperature(56, 72, 153, 500) == ( - 348, + assert color_util._white_levels_to_color_temperature(0, 128, 2000, 6535) == ( + 2000, 128, ) - assert color_util.while_levels_to_color_temperature(0, 0, 153, 500) == ( - 500, + assert color_util._white_levels_to_color_temperature(112, 143, 2000, 6535) == ( + 2876, + 255, + ) + assert color_util._white_levels_to_color_temperature(56, 72, 2000, 6535) == ( + 2872, + 128, + ) + # Both channels turned off -> warmest color temperature + assert color_util._white_levels_to_color_temperature(0, 0, 2000, 6535) == ( + 2000, 0, ) From aa0bb9c3d234e8bffea06e6fc184296f6368282e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 6 Oct 2022 12:48:31 +0200 Subject: [PATCH 194/985] Improve precision in pressure conversion (#79362) * Improve precision in pressure conversion * Use _STANDARD_GRAVITY * Add again pytest.approx --- homeassistant/util/unit_conversion.py | 8 ++++++-- tests/util/test_pressure.py | 16 ++++++++-------- tests/util/test_unit_conversion.py | 16 ++++++++-------- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 6d502ee6e6d..9aa3084887e 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -71,6 +71,10 @@ _DAYS_TO_SECS = 24 * _HRS_TO_SECS # 1 day = 24 hours = 86400 seconds _POUND_TO_G = 453.59237 _OUNCE_TO_G = _POUND_TO_G / 16 +# Pressure conversion constants +_STANDARD_GRAVITY = 9.80665 +_MERCURY_DENSITY = 13.5951 + # Volume conversion constants _L_TO_CUBIC_METER = 0.001 # 1 L = 0.001 m³ _ML_TO_CUBIC_METER = 0.001 * _L_TO_CUBIC_METER # 1 mL = 0.001 L @@ -211,9 +215,9 @@ class PressureConverter(BaseUnitConverter): PRESSURE_BAR: 1 / 100000, PRESSURE_CBAR: 1 / 1000, PRESSURE_MBAR: 1 / 100, - PRESSURE_INHG: 1 / 3386.389, + PRESSURE_INHG: 1 / (_IN_TO_M * 1000 * _STANDARD_GRAVITY * _MERCURY_DENSITY), PRESSURE_PSI: 1 / 6894.757, - PRESSURE_MMHG: 1 / 133.322, + PRESSURE_MMHG: 1 / (_MM_TO_M * 1000 * _STANDARD_GRAVITY * _MERCURY_DENSITY), } VALID_UNITS = { PRESSURE_PA, diff --git a/tests/util/test_pressure.py b/tests/util/test_pressure.py index 769c5aaf801..f87b89df3f7 100644 --- a/tests/util/test_pressure.py +++ b/tests/util/test_pressure.py @@ -118,7 +118,7 @@ def test_convert_from_inhg(): 101.59167 ) assert pressure_util.convert(inhg, PRESSURE_INHG, PRESSURE_MMHG) == pytest.approx( - 762.002 + 762 ) @@ -126,23 +126,23 @@ def test_convert_from_mmhg(): """Test conversion from mmHg to other units.""" inhg = 30 assert pressure_util.convert(inhg, PRESSURE_MMHG, PRESSURE_PSI) == pytest.approx( - 0.580102 + 0.580103 ) assert pressure_util.convert(inhg, PRESSURE_MMHG, PRESSURE_KPA) == pytest.approx( - 3.99966 + 3.99967 ) assert pressure_util.convert(inhg, PRESSURE_MMHG, PRESSURE_HPA) == pytest.approx( - 39.9966 + 39.9967 ) assert pressure_util.convert(inhg, PRESSURE_MMHG, PRESSURE_PA) == pytest.approx( - 3999.66 + 3999.67 ) assert pressure_util.convert(inhg, PRESSURE_MMHG, PRESSURE_MBAR) == pytest.approx( - 39.9966 + 39.9967 ) assert pressure_util.convert(inhg, PRESSURE_MMHG, PRESSURE_CBAR) == pytest.approx( - 3.99966 + 3.99967 ) assert pressure_util.convert(inhg, PRESSURE_MMHG, PRESSURE_INHG) == pytest.approx( - 1.181099 + 1.181102 ) diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index ec839a6575c..d74bacc66f8 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -361,14 +361,14 @@ def test_power_convert( (30, PRESSURE_INHG, pytest.approx(101591.67), PRESSURE_PA), (30, PRESSURE_INHG, pytest.approx(1015.9167), PRESSURE_MBAR), (30, PRESSURE_INHG, pytest.approx(101.59167), PRESSURE_CBAR), - (30, PRESSURE_INHG, pytest.approx(762.002), PRESSURE_MMHG), - (30, PRESSURE_MMHG, pytest.approx(0.580102), PRESSURE_PSI), - (30, PRESSURE_MMHG, pytest.approx(3.99966), PRESSURE_KPA), - (30, PRESSURE_MMHG, pytest.approx(39.9966), PRESSURE_HPA), - (30, PRESSURE_MMHG, pytest.approx(3999.66), PRESSURE_PA), - (30, PRESSURE_MMHG, pytest.approx(39.9966), PRESSURE_MBAR), - (30, PRESSURE_MMHG, pytest.approx(3.99966), PRESSURE_CBAR), - (30, PRESSURE_MMHG, pytest.approx(1.181099), PRESSURE_INHG), + (30, PRESSURE_INHG, pytest.approx(762), PRESSURE_MMHG), + (30, PRESSURE_MMHG, pytest.approx(0.580103), PRESSURE_PSI), + (30, PRESSURE_MMHG, pytest.approx(3.99967), PRESSURE_KPA), + (30, PRESSURE_MMHG, pytest.approx(39.9967), PRESSURE_HPA), + (30, PRESSURE_MMHG, pytest.approx(3999.67), PRESSURE_PA), + (30, PRESSURE_MMHG, pytest.approx(39.9967), PRESSURE_MBAR), + (30, PRESSURE_MMHG, pytest.approx(3.99967), PRESSURE_CBAR), + (30, PRESSURE_MMHG, pytest.approx(1.181102), PRESSURE_INHG), ], ) def test_pressure_convert( From df7b8f419eb020ed62a60032438cb0267cc8109b Mon Sep 17 00:00:00 2001 From: Matthew Simpson Date: Thu, 6 Oct 2022 16:01:27 +0100 Subject: [PATCH 195/985] Bump btsmarthub_devicelist to 0.2.3 (#79705) * Bump btsmarthub_devicelist This PR bumps the btsmarthub_devicelist version to correct an issue experienced by a recent firmware upgrade to the SmartHub2. * Bump btsmarthub_devicelist to 0.2.3 This version bump fixes an issue where BT SmartHub2 devices cannot be correctly autodetected. The current workaround is to specifiy it manually, which isn't great UX (and did previously work until a recent firmware upgrade). I've also taken the opportunity to reassign ownership of the component to myself as @jxwolstenholme no longer has a SmartHub so cannot do manual testing and also has no need to use the component anymore. --- CODEOWNERS | 2 +- homeassistant/components/bt_smarthub/manifest.json | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index d6d4ca61613..af8423f42ae 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -164,7 +164,7 @@ build.json @home-assistant/supervisor /tests/components/brunt/ @eavanvalkenburg /homeassistant/components/bsblan/ @liudger /tests/components/bsblan/ @liudger -/homeassistant/components/bt_smarthub/ @jxwolstenholme +/homeassistant/components/bt_smarthub/ @typhoon2099 /homeassistant/components/bthome/ @Ernst79 /tests/components/bthome/ @Ernst79 /homeassistant/components/buienradar/ @mjj4791 @ties @Robbie1221 diff --git a/homeassistant/components/bt_smarthub/manifest.json b/homeassistant/components/bt_smarthub/manifest.json index fb34117eb6b..4519ee517c3 100644 --- a/homeassistant/components/bt_smarthub/manifest.json +++ b/homeassistant/components/bt_smarthub/manifest.json @@ -2,8 +2,8 @@ "domain": "bt_smarthub", "name": "BT Smart Hub", "documentation": "https://www.home-assistant.io/integrations/bt_smarthub", - "requirements": ["btsmarthub_devicelist==0.2.2"], - "codeowners": ["@jxwolstenholme"], + "requirements": ["btsmarthub_devicelist==0.2.3"], + "codeowners": ["@typhoon2099"], "iot_class": "local_polling", "loggers": ["btsmarthub_devicelist"] } diff --git a/requirements_all.txt b/requirements_all.txt index cb2d10ad5a3..33b52cd94ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -478,7 +478,7 @@ bthome-ble==1.2.2 bthomehub5-devicelist==0.1.1 # homeassistant.components.bt_smarthub -btsmarthub_devicelist==0.2.2 +btsmarthub_devicelist==0.2.3 # homeassistant.components.buienradar buienradar==1.0.5 From 00029ca344b0e039e37b5af11c88e96285a7278f Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 6 Oct 2022 10:11:38 -0500 Subject: [PATCH 196/985] Bump pyipp to 0.12.0 (#79687) * update pyipp to 0.12.0 * Update requirements_all.txt * Update requirements_test_all.txt --- homeassistant/components/ipp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index 39e798f99bf..46f62993295 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -2,7 +2,7 @@ "domain": "ipp", "name": "Internet Printing Protocol (IPP)", "documentation": "https://www.home-assistant.io/integrations/ipp", - "requirements": ["pyipp==0.11.0"], + "requirements": ["pyipp==0.12.0"], "codeowners": ["@ctalkington"], "config_flow": true, "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 33b52cd94ad..89bc443a1a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1628,7 +1628,7 @@ pyintesishome==1.8.0 pyipma==3.0.5 # homeassistant.components.ipp -pyipp==0.11.0 +pyipp==0.12.0 # homeassistant.components.iqvia pyiqvia==2022.04.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5bbc88760a1..88b3aa02e4f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1144,7 +1144,7 @@ pyinsteon==1.2.0 pyipma==3.0.5 # homeassistant.components.ipp -pyipp==0.11.0 +pyipp==0.12.0 # homeassistant.components.iqvia pyiqvia==2022.04.0 From 61deb54ec84b3b732c5e17a6db1aa4f72232a421 Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 6 Oct 2022 19:21:57 +0200 Subject: [PATCH 197/985] Fix max_color_temp_kelvin (#79738) fix max_color_temp_kelvin --- homeassistant/components/light/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 866c3338a75..cf018014a2f 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -889,9 +889,9 @@ class LightEntity(ToggleEntity): @property def max_color_temp_kelvin(self) -> int: """Return the coldest color_temp_kelvin that this light supports.""" - if self._attr_min_color_temp_kelvin is None: + if self._attr_max_color_temp_kelvin is None: return color_util.color_temperature_mired_to_kelvin(self.min_mireds) - return self._attr_min_color_temp_kelvin + return self._attr_max_color_temp_kelvin @property def effect_list(self) -> list[str] | None: From e2c1a36e24ac36e2728d9699b30006a08774517b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 6 Oct 2022 20:01:18 +0200 Subject: [PATCH 198/985] Update frontend to 20221006.0 (#79745) --- 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 e6d5f63272d..6f243da444a 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==20221005.0"], + "requirements": ["home-assistant-frontend==20221006.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cc6d716a43b..56700d017c3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.26.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20221005.0 +home-assistant-frontend==20221006.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 89bc443a1a7..00a12e8497b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,7 +868,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221005.0 +home-assistant-frontend==20221006.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 88b3aa02e4f..9940b764c66 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,7 +648,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221005.0 +home-assistant-frontend==20221006.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 0a59d37e624e0d476259cfbf3b6dad0e1f71168f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 6 Oct 2022 20:01:54 +0200 Subject: [PATCH 199/985] Correct how unit used for statistics is determined (#79725) --- homeassistant/components/sensor/recorder.py | 35 ++++++++++++--------- tests/components/sensor/test_recorder.py | 24 +++++++------- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 1a72444c758..beae06f78ff 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -149,13 +149,20 @@ def _normalize_states( state_unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if state_unit not in statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER or ( - old_metadata - and old_metadata["unit_of_measurement"] - not in statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER + statistics_unit: str | None + if not old_metadata: + # We've not seen this sensor before, the first valid state determines the unit + # used for statistics + statistics_unit = state_unit + else: + # We have seen this sensor before, use the unit from metadata + statistics_unit = old_metadata["unit_of_measurement"] + + if ( + not statistics_unit + or statistics_unit not in statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER ): - # We're either not normalizing this device class or this entity is not stored - # in a unit which can be converted, return the states as they are + # The unit used by this sensor doesn't support unit conversion all_units = _get_units(fstates) if len(all_units) > 1: @@ -182,13 +189,9 @@ def _normalize_states( state_unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) return state_unit, state_unit, fstates - converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER[state_unit] + converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER[statistics_unit] valid_fstates: list[tuple[float, State]] = [] - statistics_unit: str | None = None - if old_metadata: - statistics_unit = old_metadata["unit_of_measurement"] - for fstate, state in fstates: state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) # Exclude states with unsupported unit from statistics @@ -198,14 +201,18 @@ def _normalize_states( if entity_id not in hass.data[WARN_UNSUPPORTED_UNIT]: hass.data[WARN_UNSUPPORTED_UNIT].add(entity_id) _LOGGER.warning( - "%s has unit %s which can't be converted to %s", + "The unit of %s (%s) can not be converted to the unit of previously " + "compiled statistics (%s). Generation of long term statistics " + "will be suppressed unless the unit changes back to %s or a " + "compatible unit. " + "Go to %s to fix this", entity_id, state_unit, statistics_unit, + statistics_unit, + LINK_DEV_STATISTICS, ) continue - if statistics_unit is None: - statistics_unit = state_unit valid_fstates.append( ( diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 8d9e34d005f..0a72dcf6fcd 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -1900,12 +1900,13 @@ def test_list_statistic_ids_unsupported(hass_recorder, caplog, _attributes): @pytest.mark.parametrize( - "device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max", + "device_class, state_unit, state_unit2, unit_class, mean, min, max", [ - (None, None, None, None, None, 13.050847, -10, 30), - (None, "%", "%", "%", None, 13.050847, -10, 30), - ("battery", "%", "%", "%", None, 13.050847, -10, 30), - ("battery", None, None, None, None, 13.050847, -10, 30), + (None, None, "cats", None, 13.050847, -10, 30), + (None, "%", "cats", None, 13.050847, -10, 30), + ("battery", "%", "cats", None, 13.050847, -10, 30), + ("battery", None, "cats", None, 13.050847, -10, 30), + (None, "kW", "Wh", "power", 13.050847, -10, 30), ], ) def test_compile_hourly_statistics_changing_units_1( @@ -1913,8 +1914,7 @@ def test_compile_hourly_statistics_changing_units_1( caplog, device_class, state_unit, - display_unit, - statistics_unit, + state_unit2, unit_class, mean, min, @@ -1931,7 +1931,7 @@ def test_compile_hourly_statistics_changing_units_1( "unit_of_measurement": state_unit, } four, states = record_states(hass, zero, "sensor.test1", attributes) - attributes["unit_of_measurement"] = "cats" + attributes["unit_of_measurement"] = state_unit2 four, _states = record_states( hass, zero + timedelta(minutes=5), "sensor.test1", attributes ) @@ -1954,7 +1954,7 @@ def test_compile_hourly_statistics_changing_units_1( "has_sum": False, "name": None, "source": "recorder", - "statistics_unit_of_measurement": statistics_unit, + "statistics_unit_of_measurement": state_unit, "unit_class": unit_class, }, ] @@ -1978,8 +1978,8 @@ def test_compile_hourly_statistics_changing_units_1( do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) wait_recording_done(hass) assert ( - "The unit of sensor.test1 (cats) can not be converted to the unit of " - f"previously compiled statistics ({display_unit})" in caplog.text + f"The unit of sensor.test1 ({state_unit2}) can not be converted to the unit of " + f"previously compiled statistics ({state_unit})" in caplog.text ) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ @@ -1989,7 +1989,7 @@ def test_compile_hourly_statistics_changing_units_1( "has_sum": False, "name": None, "source": "recorder", - "statistics_unit_of_measurement": statistics_unit, + "statistics_unit_of_measurement": state_unit, "unit_class": unit_class, }, ] From 2dab9073fed21d7fea2e7db9cc7aa0df4998055a Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 6 Oct 2022 14:02:24 -0400 Subject: [PATCH 200/985] ZHA radio migration: reset the old adapter (#79663) --- homeassistant/components/zha/config_flow.py | 84 ++++++++-- homeassistant/components/zha/strings.json | 16 ++ .../components/zha/translations/en.json | 39 ++--- tests/components/zha/test_config_flow.py | 148 ++++++++++++++---- 4 files changed, 219 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index ce2080e4a13..85f03b9f1f5 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -1,6 +1,7 @@ """Config flow for ZHA.""" from __future__ import annotations +import asyncio import collections import contextlib import copy @@ -65,8 +66,16 @@ FORMATION_UPLOAD_MANUAL_BACKUP = "upload_manual_backup" CHOOSE_AUTOMATIC_BACKUP = "choose_automatic_backup" OVERWRITE_COORDINATOR_IEEE = "overwrite_coordinator_ieee" +OPTIONS_INTENT_MIGRATE = "intent_migrate" +OPTIONS_INTENT_RECONFIGURE = "intent_reconfigure" + UPLOADED_BACKUP_FILE = "uploaded_backup_file" +DEFAULT_ZHA_ZEROCONF_PORT = 6638 +ESPHOME_API_PORT = 6053 + +CONNECT_DELAY_S = 1.0 + _LOGGER = logging.getLogger(__name__) @@ -159,6 +168,7 @@ class BaseZhaFlow(FlowHandler): yield app finally: await app.disconnect() + await asyncio.sleep(CONNECT_DELAY_S) async def _restore_backup( self, backup: zigpy.backups.NetworkBackup, **kwargs: Any @@ -628,14 +638,21 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN # Hostname is format: livingroom.local. local_name = discovery_info.hostname[:-1] - radio_type = discovery_info.properties.get("radio_type") or local_name + port = discovery_info.port or DEFAULT_ZHA_ZEROCONF_PORT + + # Fix incorrect port for older TubesZB devices + if "tube" in local_name and port == ESPHOME_API_PORT: + port = DEFAULT_ZHA_ZEROCONF_PORT + + if "radio_type" in discovery_info.properties: + self._radio_type = RadioType[discovery_info.properties["radio_type"]] + elif "efr32" in local_name: + self._radio_type = RadioType.ezsp + else: + self._radio_type = RadioType.znp + node_name = local_name[: -len(".local")] - host = discovery_info.host - port = discovery_info.port - if local_name.startswith("tube") or "efr32" in local_name: - # This is hard coded to work with legacy devices - port = 6638 - device_path = f"socket://{host}:{port}" + device_path = f"socket://{discovery_info.host}:{port}" if current_entry := await self.async_set_unique_id(node_name): self._abort_if_unique_id_configured( @@ -651,13 +668,6 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN self._title = device_path self._device_path = device_path - if "efr32" in radio_type: - self._radio_type = RadioType.ezsp - elif "zigate" in radio_type: - self._radio_type = RadioType.zigate - else: - self._radio_type = RadioType.znp - return await self.async_step_confirm() async def async_step_hardware( @@ -720,10 +730,54 @@ class ZhaOptionsFlowHandler(BaseZhaFlow, config_entries.OptionsFlow): # ZHA is not running pass - return await self.async_step_choose_serial_port() + return await self.async_step_prompt_migrate_or_reconfigure() return self.async_show_form(step_id="init") + async def async_step_prompt_migrate_or_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm if we are migrating adapters or just re-configuring.""" + + return self.async_show_menu( + step_id="prompt_migrate_or_reconfigure", + menu_options=[ + OPTIONS_INTENT_RECONFIGURE, + OPTIONS_INTENT_MIGRATE, + ], + ) + + async def async_step_intent_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Virtual step for when the user is reconfiguring the integration.""" + return await self.async_step_choose_serial_port() + + async def async_step_intent_migrate( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm the user wants to reset their current radio.""" + + if user_input is not None: + # Reset the current adapter + async with self._connect_zigpy_app() as app: + await app.reset_network_info() + + return await self.async_step_instruct_unplug() + + return self.async_show_form(step_id="intent_migrate") + + async def async_step_instruct_unplug( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Instruct the user to unplug the current radio, if possible.""" + + if user_input is not None: + # Now that the old radio is gone, we can scan for serial ports again + return await self.async_step_choose_serial_port() + + return self.async_show_form(step_id="instruct_unplug") + async def _async_create_radio_entity(self): """Re-implementation of the base flow's final step to update the config.""" device_settings = self._device_settings.copy() diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 3901f9f9439..240f3c4ee83 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -76,6 +76,22 @@ "title": "Reconfigure ZHA", "description": "ZHA will be stopped. Do you wish to continue?" }, + "prompt_migrate_or_reconfigure": { + "title": "Migrate or re-configure", + "description": "Are you migrating to a new radio or re-configuring the current radio?", + "menu_options": { + "intent_migrate": "Migrate to a new radio", + "intent_reconfigure": "Re-configure the current radio" + } + }, + "intent_migrate": { + "title": "Migrate to a new radio", + "description": "Your old radio will be factory reset. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\nDo you wish to continue?" + }, + "instruct_unplug": { + "title": "Unplug your old radio", + "description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it." + }, "choose_serial_port": { "title": "[%key:component::zha::config::step::choose_serial_port::title%]", "data": { diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index c75fa14628d..bb62ccca64a 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -64,35 +64,12 @@ "description": "Your backup has a different IEEE address than your radio. For your network to function properly, the IEEE address of your radio should also be changed.\n\nThis is a permanent operation.", "title": "Overwrite Radio IEEE Address" }, - "pick_radio": { - "data": { - "radio_type": "Radio Type" - }, - "description": "Pick a type of your Zigbee radio", - "title": "Radio Type" - }, - "port_config": { - "data": { - "baudrate": "port speed", - "flow_control": "data flow control", - "path": "Serial device path" - }, - "description": "Enter port specific settings", - "title": "Settings" - }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Upload a file" }, "description": "Restore your network settings from an uploaded backup JSON file. You can download one from a different ZHA installation from **Network Settings**, or use a Zigbee2MQTT `coordinator_backup.json` file.", "title": "Upload a Manual Backup" - }, - "user": { - "data": { - "path": "Serial Device Path" - }, - "description": "Select serial port for Zigbee radio", - "title": "ZHA" } } }, @@ -212,6 +189,14 @@ "description": "ZHA will be stopped. Do you wish to continue?", "title": "Reconfigure ZHA" }, + "instruct_unplug": { + "description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it.", + "title": "Unplug your old radio" + }, + "intent_migrate": { + "description": "Your old radio will be factory reset. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\nDo you wish to continue?", + "title": "Migrate to a new radio" + }, "manual_pick_radio_type": { "data": { "radio_type": "Radio Type" @@ -235,6 +220,14 @@ "description": "Your backup has a different IEEE address than your radio. For your network to function properly, the IEEE address of your radio should also be changed.\n\nThis is a permanent operation.", "title": "Overwrite Radio IEEE Address" }, + "prompt_migrate_or_reconfigure": { + "description": "Are you migrating to a new radio or re-configuring the current radio?", + "menu_options": { + "intent_migrate": "Migrate to a new radio", + "intent_reconfigure": "Re-configure the current radio" + }, + "title": "Migrate or re-configure" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Upload a file" diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 5fc4b232634..725f9cc0917 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -46,6 +46,13 @@ def disable_platform_only(): yield +@pytest.fixture(autouse=True) +def reduce_reconnect_timeout(): + """Reduces reconnect timeout to speed up tests.""" + with patch("homeassistant.components.zha.config_flow.CONNECT_DELAY_S", 0.01): + yield + + @pytest.fixture(autouse=True) def mock_app(): """Mock zigpy app interface.""" @@ -230,10 +237,10 @@ async def test_efr32_via_zeroconf(hass): await hass.async_block_till_done() assert result3["type"] == FlowResultType.CREATE_ENTRY - assert result3["title"] == "socket://192.168.1.200:6638" + assert result3["title"] == "socket://192.168.1.200:1234" assert result3["data"] == { CONF_DEVICE: { - CONF_DEVICE_PATH: "socket://192.168.1.200:6638", + CONF_DEVICE_PATH: "socket://192.168.1.200:1234", CONF_BAUDRATE: 115200, CONF_FLOWCONTROL: "software", }, @@ -1476,21 +1483,28 @@ async def test_options_flow_defaults(async_setup_entry, async_unload_effect, has # Unload it ourselves entry.state = config_entries.ConfigEntryState.NOT_LOADED + # Reconfigure ZHA + assert result1["step_id"] == "prompt_migrate_or_reconfigure" + result2 = await hass.config_entries.options.async_configure( + flow["flow_id"], + user_input={"next_step_id": config_flow.OPTIONS_INTENT_RECONFIGURE}, + ) + # Current path is the default - assert result1["step_id"] == "choose_serial_port" - assert "/dev/ttyUSB0" in result1["data_schema"]({})[CONF_DEVICE_PATH] + assert result2["step_id"] == "choose_serial_port" + assert "/dev/ttyUSB0" in result2["data_schema"]({})[CONF_DEVICE_PATH] # Autoprobing fails, we have to manually choose the radio type - result2 = await hass.config_entries.options.async_configure( + result3 = await hass.config_entries.options.async_configure( flow["flow_id"], user_input={} ) # Current radio type is the default - assert result2["step_id"] == "manual_pick_radio_type" - assert result2["data_schema"]({})[CONF_RADIO_TYPE] == RadioType.znp.description + assert result3["step_id"] == "manual_pick_radio_type" + assert result3["data_schema"]({})[CONF_RADIO_TYPE] == RadioType.znp.description # Continue on to port settings - result3 = await hass.config_entries.options.async_configure( + result4 = await hass.config_entries.options.async_configure( flow["flow_id"], user_input={ CONF_RADIO_TYPE: RadioType.znp.description, @@ -1498,12 +1512,12 @@ async def test_options_flow_defaults(async_setup_entry, async_unload_effect, has ) # The defaults match our current settings - assert result3["step_id"] == "manual_port_config" - assert result3["data_schema"]({}) == entry.data[CONF_DEVICE] + assert result4["step_id"] == "manual_port_config" + assert result4["data_schema"]({}) == entry.data[CONF_DEVICE] with patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)): # Change the serial port path - result4 = await hass.config_entries.options.async_configure( + result5 = await hass.config_entries.options.async_configure( flow["flow_id"], user_input={ # Change everything @@ -1514,18 +1528,18 @@ async def test_options_flow_defaults(async_setup_entry, async_unload_effect, has ) # The radio has been detected, we can move on to creating the config entry - assert result4["step_id"] == "choose_formation_strategy" + assert result5["step_id"] == "choose_formation_strategy" async_setup_entry.assert_not_called() - result5 = await hass.config_entries.options.async_configure( + result6 = await hass.config_entries.options.async_configure( result1["flow_id"], user_input={"next_step_id": config_flow.FORMATION_REUSE_SETTINGS}, ) await hass.async_block_till_done() - assert result5["type"] == FlowResultType.CREATE_ENTRY - assert result5["data"] == {} + assert result6["type"] == FlowResultType.CREATE_ENTRY + assert result6["data"] == {} # The updated entry contains correct settings assert entry.data == { @@ -1581,33 +1595,39 @@ async def test_options_flow_defaults_socket(hass): flow["flow_id"], user_input={} ) - # Radio path must be manually entered - assert result1["step_id"] == "choose_serial_port" - assert result1["data_schema"]({})[CONF_DEVICE_PATH] == config_flow.CONF_MANUAL_PATH - + assert result1["step_id"] == "prompt_migrate_or_reconfigure" result2 = await hass.config_entries.options.async_configure( - flow["flow_id"], user_input={} + flow["flow_id"], + user_input={"next_step_id": config_flow.OPTIONS_INTENT_RECONFIGURE}, ) - # Current radio type is the default - assert result2["step_id"] == "manual_pick_radio_type" - assert result2["data_schema"]({})[CONF_RADIO_TYPE] == RadioType.znp.description + # Radio path must be manually entered + assert result2["step_id"] == "choose_serial_port" + assert result2["data_schema"]({})[CONF_DEVICE_PATH] == config_flow.CONF_MANUAL_PATH - # Continue on to port settings result3 = await hass.config_entries.options.async_configure( flow["flow_id"], user_input={} ) + # Current radio type is the default + assert result3["step_id"] == "manual_pick_radio_type" + assert result3["data_schema"]({})[CONF_RADIO_TYPE] == RadioType.znp.description + + # Continue on to port settings + result4 = await hass.config_entries.options.async_configure( + flow["flow_id"], user_input={} + ) + # The defaults match our current settings - assert result3["step_id"] == "manual_port_config" - assert result3["data_schema"]({}) == entry.data[CONF_DEVICE] + assert result4["step_id"] == "manual_port_config" + assert result4["data_schema"]({}) == entry.data[CONF_DEVICE] with patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)): - result4 = await hass.config_entries.options.async_configure( + result5 = await hass.config_entries.options.async_configure( flow["flow_id"], user_input={} ) - assert result4["step_id"] == "choose_formation_strategy" + assert result5["step_id"] == "choose_formation_strategy" @patch("homeassistant.components.zha.async_setup_entry", return_value=True) @@ -1643,14 +1663,82 @@ async def test_options_flow_restarts_running_zha_if_cancelled(async_setup_entry, entry.state = config_entries.ConfigEntryState.NOT_LOADED + assert result1["step_id"] == "prompt_migrate_or_reconfigure" + result2 = await hass.config_entries.options.async_configure( + flow["flow_id"], + user_input={"next_step_id": config_flow.OPTIONS_INTENT_RECONFIGURE}, + ) + # Radio path must be manually entered - assert result1["step_id"] == "choose_serial_port" + assert result2["step_id"] == "choose_serial_port" async_setup_entry.reset_mock() # Abort the flow - hass.config_entries.options.async_abort(result1["flow_id"]) + hass.config_entries.options.async_abort(result2["flow_id"]) await hass.async_block_till_done() # ZHA was set up once more async_setup_entry.assert_called_once_with(hass, entry) + + +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +async def test_options_flow_migration_reset_old_adapter(hass, mock_app): + """Test options flow for migrating from an old radio.""" + + entry = MockConfigEntry( + version=config_flow.ZhaConfigFlowHandler.VERSION, + domain=DOMAIN, + data={ + CONF_DEVICE: { + CONF_DEVICE_PATH: "/dev/serial/by-id/old_radio", + CONF_BAUDRATE: 12345, + CONF_FLOWCONTROL: None, + }, + CONF_RADIO_TYPE: "znp", + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + flow = await hass.config_entries.options.async_init(entry.entry_id) + + # ZHA gets unloaded + with patch( + "homeassistant.config_entries.ConfigEntries.async_unload", return_value=True + ): + result1 = await hass.config_entries.options.async_configure( + flow["flow_id"], user_input={} + ) + + entry.state = config_entries.ConfigEntryState.NOT_LOADED + + assert result1["step_id"] == "prompt_migrate_or_reconfigure" + result2 = await hass.config_entries.options.async_configure( + flow["flow_id"], + user_input={"next_step_id": config_flow.OPTIONS_INTENT_MIGRATE}, + ) + + # User must explicitly approve radio reset + assert result2["step_id"] == "intent_migrate" + + mock_app.reset_network_info = AsyncMock() + + result3 = await hass.config_entries.options.async_configure( + flow["flow_id"], + user_input={}, + ) + + mock_app.reset_network_info.assert_awaited_once() + + # Now we can unplug the old radio + assert result3["step_id"] == "instruct_unplug" + + # And move on to choosing the new radio + result4 = await hass.config_entries.options.async_configure( + flow["flow_id"], + user_input={}, + ) + assert result4["step_id"] == "choose_serial_port" From 28df576e51424229e452230fe3aeb72fc0a0c35f Mon Sep 17 00:00:00 2001 From: Vincent Knoop Pathuis <48653141+vpathuis@users.noreply.github.com> Date: Thu, 6 Oct 2022 20:06:52 +0200 Subject: [PATCH 201/985] Update ultraheat api to 0.5.0 (#79666) --- homeassistant/components/landisgyr_heat_meter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/landisgyr_heat_meter/manifest.json b/homeassistant/components/landisgyr_heat_meter/manifest.json index 9d1faa570b7..dc6444b478d 100644 --- a/homeassistant/components/landisgyr_heat_meter/manifest.json +++ b/homeassistant/components/landisgyr_heat_meter/manifest.json @@ -3,7 +3,7 @@ "name": "Landis+Gyr Heat Meter", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/landisgyr_heat_meter", - "requirements": ["ultraheat-api==0.4.3"], + "requirements": ["ultraheat-api==0.5.0"], "ssdp": [], "zeroconf": [], "homekit": {}, diff --git a/requirements_all.txt b/requirements_all.txt index 00a12e8497b..cc0027f0b69 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2446,7 +2446,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.landisgyr_heat_meter -ultraheat-api==0.4.3 +ultraheat-api==0.5.0 # homeassistant.components.unifiprotect unifi-discovery==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9940b764c66..3a5d09b2773 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1683,7 +1683,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.landisgyr_heat_meter -ultraheat-api==0.4.3 +ultraheat-api==0.5.0 # homeassistant.components.unifiprotect unifi-discovery==1.1.7 From d3fee8aad9871395464e71d369d0adebf32735ea Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 6 Oct 2022 14:33:37 -0400 Subject: [PATCH 202/985] Add supported brands to UPB integration (#79619) --- homeassistant/components/upb/manifest.json | 6 +++++- homeassistant/generated/supported_brands.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/upb/manifest.json b/homeassistant/components/upb/manifest.json index fd5d68e577f..aaa26bfdd66 100644 --- a/homeassistant/components/upb/manifest.json +++ b/homeassistant/components/upb/manifest.json @@ -6,5 +6,9 @@ "codeowners": ["@gwww"], "config_flow": true, "iot_class": "local_push", - "loggers": ["upb_lib"] + "loggers": ["upb_lib"], + "supported_brands": { + "pcs_lighting": "PCS Lighting", + "simply_automated": "Simply Automated" + } } diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py index 15f2a580a29..0efd1982c61 100644 --- a/homeassistant/generated/supported_brands.py +++ b/homeassistant/generated/supported_brands.py @@ -13,6 +13,7 @@ HAS_SUPPORTED_BRANDS = [ "renault", "switchbee", "thermobeacon", + "upb", "wemo", "yalexs_ble", ] From 96a8beb29f44ab50d7e475da2cca098bd7df5235 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 6 Oct 2022 21:17:24 +0200 Subject: [PATCH 203/985] Tweak comment in LightEntity (#79750) --- homeassistant/components/light/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index cf018014a2f..1038fef7a30 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -788,7 +788,7 @@ class LightEntity(ToggleEntity): _attr_max_color_temp_kelvin: int | None = None _attr_min_color_temp_kelvin: int | None = None _attr_max_mireds: int = 500 # 2000 K - _attr_min_mireds: int = 153 # 6535 K + _attr_min_mireds: int = 153 # 6500 K _attr_rgb_color: tuple[int, int, int] | None = None _attr_rgbw_color: tuple[int, int, int, int] | None = None _attr_rgbww_color: tuple[int, int, int, int, int] | None = None From aa5575ba653a72808fe4e6ac3a465d895524df71 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 6 Oct 2022 21:17:46 +0200 Subject: [PATCH 204/985] Only validate sensors in sensor.recorder.validate_statistics (#79749) --- homeassistant/components/sensor/recorder.py | 4 +- tests/components/sensor/test_recorder.py | 57 +++++++++++++++++++-- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index beae06f78ff..eee13c09813 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -24,7 +24,7 @@ from homeassistant.components.recorder.models import ( StatisticResult, ) from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT -from homeassistant.core import HomeAssistant, State +from homeassistant.core import HomeAssistant, State, split_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import entity_sources from homeassistant.util import dt as dt_util @@ -689,6 +689,8 @@ def validate_statistics( ) for statistic_id in sensor_statistic_ids - sensor_entity_ids: + if split_entity_id(statistic_id)[0] != DOMAIN: + continue # There is no sensor matching the statistics_id validation_result[statistic_id].append( statistics.ValidationIssue( diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 0a72dcf6fcd..3f15b35d7b1 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -1,6 +1,6 @@ """The tests for sensor recorder platform.""" # pylint: disable=protected-access,invalid-name -from datetime import timedelta +from datetime import datetime, timedelta import math from statistics import mean from unittest.mock import patch @@ -9,10 +9,15 @@ import pytest from pytest import approx from homeassistant import loader -from homeassistant.components.recorder import history +from homeassistant.components.recorder import DOMAIN as RECORDER_DOMAIN, history from homeassistant.components.recorder.db_schema import StatisticsMeta -from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat +from homeassistant.components.recorder.models import ( + StatisticData, + StatisticMetaData, + process_timestamp_to_utc_isoformat, +) from homeassistant.components.recorder.statistics import ( + async_import_statistics, get_metadata, list_statistic_ids, statistics_during_period, @@ -3756,6 +3761,52 @@ async def test_validate_statistics_unit_change_no_conversion( await assert_validation_result(client, expected) +async def test_validate_statistics_other_domain(hass, hass_ws_client, recorder_mock): + """Test sensor does not raise issues for statistics for other domains.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + async def assert_validation_result(client, expected_result): + await client.send_json( + {"id": next_id(), "type": "recorder/validate_statistics"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected_result + + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + client = await hass_ws_client() + + # Create statistics for another domain + metadata: StatisticMetaData = { + "has_mean": True, + "has_sum": True, + "name": None, + "source": RECORDER_DOMAIN, + "statistic_id": "number.test", + "unit_of_measurement": None, + } + statistics: StatisticData = { + "last_reset": None, + "max": None, + "mean": None, + "min": None, + "start": datetime(2020, 10, 6, tzinfo=dt_util.UTC), + "state": None, + "sum": None, + } + async_import_statistics(hass, metadata, (statistics,)) + await async_recorder_block_till_done(hass) + + # We should not get complains about the missing number entity + await assert_validation_result(client, {}) + + def record_meter_states(hass, zero, entity_id, _attributes, seq): """Record some test states. From 51e6d49451924df800bccbcf87137be930eea505 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 6 Oct 2022 21:20:10 +0200 Subject: [PATCH 205/985] Adapt homekit to color temperatures in K (#79713) --- .../components/homekit/type_lights.py | 32 +++++++++---------- tests/components/homekit/test_type_lights.py | 22 ++++++------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 7ccf3dcc38f..65c0368f6e6 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -import math from pyhap.const import CATEGORY_LIGHTBULB @@ -10,10 +9,10 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_MODE, - ATTR_COLOR_TEMP, + ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, - ATTR_MAX_MIREDS, - ATTR_MIN_MIREDS, + ATTR_MAX_COLOR_TEMP_KELVIN, + ATTR_MIN_COLOR_TEMP_KELVIN, ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, ATTR_SUPPORTED_COLOR_MODES, @@ -33,6 +32,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers.event import async_call_later from homeassistant.util.color import ( + color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin, color_temperature_to_hs, color_temperature_to_rgbww, @@ -55,8 +55,8 @@ _LOGGER = logging.getLogger(__name__) CHANGE_COALESCE_TIME_WINDOW = 0.01 -DEFAULT_MIN_MIREDS = 153 -DEFAULT_MAX_MIREDS = 500 +DEFAULT_MIN_COLOR_TEMP = 2000 # 500 mireds +DEFAULT_MAX_COLOR_TEMP = 6500 # 153 mireds COLOR_MODES_WITH_WHITES = {ColorMode.RGBW, ColorMode.RGBWW, ColorMode.WHITE} @@ -110,11 +110,11 @@ class Light(HomeAccessory): self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100) if CHAR_COLOR_TEMPERATURE in self.chars: - self.min_mireds = math.floor( - attributes.get(ATTR_MIN_MIREDS, DEFAULT_MIN_MIREDS) + self.min_mireds = color_temperature_kelvin_to_mired( + attributes.get(ATTR_MAX_COLOR_TEMP_KELVIN, DEFAULT_MAX_COLOR_TEMP) ) - self.max_mireds = math.ceil( - attributes.get(ATTR_MAX_MIREDS, DEFAULT_MAX_MIREDS) + self.max_mireds = color_temperature_kelvin_to_mired( + attributes.get(ATTR_MIN_COLOR_TEMP_KELVIN, DEFAULT_MIN_COLOR_TEMP) ) if not self.color_temp_supported and not self.rgbww_supported: self.max_mireds = self.min_mireds @@ -190,7 +190,7 @@ class Light(HomeAccessory): ((brightness_pct or self.char_brightness.value) * 255) / 100 ) if self.color_temp_supported: - params[ATTR_COLOR_TEMP] = temp + params[ATTR_COLOR_TEMP_KELVIN] = color_temperature_mired_to_kelvin(temp) elif self.rgbww_supported: params[ATTR_RGBWW_COLOR] = color_temperature_to_rgbww( color_temperature_mired_to_kelvin(temp), @@ -261,10 +261,8 @@ class Light(HomeAccessory): # Handle Color - color must always be set before color temperature # or the iOS UI will not display it correctly. if self.color_supported: - if color_temp := attributes.get(ATTR_COLOR_TEMP): - hue, saturation = color_temperature_to_hs( - color_temperature_mired_to_kelvin(color_temp) - ) + if color_temp := attributes.get(ATTR_COLOR_TEMP_KELVIN): + hue, saturation = color_temperature_to_hs(color_temp) elif color_mode == ColorMode.WHITE: hue, saturation = 0, 0 else: @@ -281,7 +279,9 @@ class Light(HomeAccessory): if CHAR_COLOR_TEMPERATURE in self.chars: color_temp = None if self.color_temp_supported: - color_temp = attributes.get(ATTR_COLOR_TEMP) + color_temp_kelvin = attributes.get(ATTR_COLOR_TEMP_KELVIN) + if color_temp_kelvin is not None: + color_temp = color_temperature_kelvin_to_mired(color_temp_kelvin) elif color_mode == ColorMode.WHITE: color_temp = self.min_mireds if isinstance(color_temp, (int, float)): diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 64e45aa937d..3dcf2a7698c 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -18,7 +18,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_MODE, - ATTR_COLOR_TEMP, + ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, @@ -250,7 +250,7 @@ async def test_light_color_temperature(hass, hk_driver, events): hass.states.async_set( entity_id, STATE_ON, - {ATTR_SUPPORTED_COLOR_MODES: ["color_temp"], ATTR_COLOR_TEMP: 190}, + {ATTR_SUPPORTED_COLOR_MODES: ["color_temp"], ATTR_COLOR_TEMP_KELVIN: 5263}, ) await hass.async_block_till_done() acc = Light(hass, hk_driver, "Light", entity_id, 1, None) @@ -282,7 +282,7 @@ async def test_light_color_temperature(hass, hk_driver, events): await _wait_for_light_coalesce(hass) assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id - assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250 + assert call_turn_on[0].data[ATTR_COLOR_TEMP_KELVIN] == 4000 assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == "color temperature at 250" @@ -302,7 +302,7 @@ async def test_light_color_temperature_and_rgb_color( STATE_ON, { ATTR_SUPPORTED_COLOR_MODES: supported_color_modes, - ATTR_COLOR_TEMP: 190, + ATTR_COLOR_TEMP_KELVIN: 5263, ATTR_HS_COLOR: (260, 90), }, ) @@ -316,7 +316,7 @@ async def test_light_color_temperature_and_rgb_color( assert hasattr(acc, "char_color_temp") - hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 224}) + hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP_KELVIN: 4464}) await hass.async_block_till_done() await acc.run() await hass.async_block_till_done() @@ -324,7 +324,7 @@ async def test_light_color_temperature_and_rgb_color( assert acc.char_hue.value == 27 assert acc.char_saturation.value == 27 - hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 352}) + hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP_KELVIN: 2840}) await hass.async_block_till_done() await acc.run() await hass.async_block_till_done() @@ -373,7 +373,7 @@ async def test_light_color_temperature_and_rgb_color( assert call_turn_on[0] assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 - assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250 + assert call_turn_on[0].data[ATTR_COLOR_TEMP_KELVIN] == 4000 assert len(events) == 1 assert ( @@ -446,7 +446,7 @@ async def test_light_color_temperature_and_rgb_color( ) await _wait_for_light_coalesce(hass) assert call_turn_on[3] - assert call_turn_on[3].data[ATTR_COLOR_TEMP] == 320 + assert call_turn_on[3].data[ATTR_COLOR_TEMP_KELVIN] == 3125 assert events[-1].data[ATTR_VALUE] == "color temperature at 320" # Generate a conflict by setting color temp then saturation @@ -991,7 +991,7 @@ async def test_light_rgb_with_white_switch_to_temp( await _wait_for_light_coalesce(hass) assert call_turn_on assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id - assert call_turn_on[-1].data[ATTR_COLOR_TEMP] == 500 + assert call_turn_on[-1].data[ATTR_COLOR_TEMP_KELVIN] == 2000 assert len(events) == 2 assert events[-1].data[ATTR_VALUE] == "color temperature at 500" assert acc.char_brightness.value == 100 @@ -1335,7 +1335,7 @@ async def test_light_set_brightness_and_color_temp(hass, hk_driver, events): await hass.async_block_till_done() assert acc.char_brightness.value == 40 - hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: (224.14)}) + hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP_KELVIN: (4461)}) await hass.async_block_till_done() assert acc.char_color_temp.value == 224 @@ -1364,7 +1364,7 @@ async def test_light_set_brightness_and_color_temp(hass, hk_driver, events): assert call_turn_on[0] assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 - assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250 + assert call_turn_on[0].data[ATTR_COLOR_TEMP_KELVIN] == 4000 assert len(events) == 1 assert ( From 6111fb38a7397661d16536a05de1357a843ce7d0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 6 Oct 2022 22:16:41 +0200 Subject: [PATCH 206/985] Add translations to Plugwise regulation mode (#79597) --- homeassistant/components/plugwise/select.py | 1 + .../components/plugwise/strings.select.json | 11 ++++++++++ .../components/plugwise/translations/en.json | 22 ------------------- .../plugwise/translations/select.en.json | 11 ++++++++++ 4 files changed, 23 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/plugwise/strings.select.json create mode 100644 homeassistant/components/plugwise/translations/select.en.json diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index 989f56adcf3..a6f49380678 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -52,6 +52,7 @@ SELECT_TYPES = ( command=lambda api, loc, opt: api.set_regulation_mode(opt), current_option_key="regulation_mode", options_key="regulation_modes", + device_class="plugwise__regulation_mode", ), ) diff --git a/homeassistant/components/plugwise/strings.select.json b/homeassistant/components/plugwise/strings.select.json new file mode 100644 index 00000000000..1c278f44315 --- /dev/null +++ b/homeassistant/components/plugwise/strings.select.json @@ -0,0 +1,11 @@ +{ + "state": { + "plugwise__regulation_mode": { + "bleeding_cold": "Bleeding cold", + "bleeding_hot": "Bleeding hot", + "cooling": "Cooling", + "heating": "Heating", + "off": "Off" + } + } +} diff --git a/homeassistant/components/plugwise/translations/en.json b/homeassistant/components/plugwise/translations/en.json index cd10502d0c3..aa5a318bbff 100644 --- a/homeassistant/components/plugwise/translations/en.json +++ b/homeassistant/components/plugwise/translations/en.json @@ -10,11 +10,9 @@ "invalid_setup": "Add your Adam instead of your Anna, see the Home Assistant Plugwise integration documentation for more information", "unknown": "Unexpected error" }, - "flow_title": "{name}", "step": { "user": { "data": { - "flow_type": "Connection type", "host": "IP Address", "password": "Smile ID", "port": "Port", @@ -22,26 +20,6 @@ }, "description": "Please enter", "title": "Connect to the Smile" - }, - "user_gateway": { - "data": { - "host": "IP Address", - "password": "Smile ID", - "port": "Port", - "username": "Smile Username" - }, - "description": "Please enter", - "title": "Connect to the Smile" - } - } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Scan Interval (seconds)" - }, - "description": "Adjust Plugwise Options" } } } diff --git a/homeassistant/components/plugwise/translations/select.en.json b/homeassistant/components/plugwise/translations/select.en.json new file mode 100644 index 00000000000..b71301ba047 --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.en.json @@ -0,0 +1,11 @@ +{ + "state": { + "plugwise__regulation_mode": { + "bleeding_cold": "Bleeding cold", + "bleeding_hot": "Bleeding hot", + "cooling": "Cooling", + "heating": "Heating", + "off": "Off" + } + } +} \ No newline at end of file From 6040c30b453a4dd3c20044fa46f03d0e77a07436 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 6 Oct 2022 21:24:19 +0100 Subject: [PATCH 207/985] Add visual image preview during generic camera config flow (#71269) * Add visual preview during setup of generic camera * Code review: standardize preview url * Fix slug test * Refactor to use HomeAssistantView * Code review: simplify * Update manifest * Don't illegally access protected member * Increase test coverage * Prevent browser caching of preview images. * Code review:move incrementor to ?t=X + simplify * Discard old flow preview data * Increase test coverage * Code review: rename variables for clarity * Add timeout for image previews * Fix preview timeout tests * Simplify: store cam image preview in config_flow * Call step method to transition between flow steps * Only store user_input in flow, not CameraObject * Fix problem where test wouldn't run in isolation. * Simplify test * Don't move directly to another step's form * Remove unused constant * Simplify test Co-authored-by: Dave T --- .../components/generic/config_flow.py | 95 ++++++++++-- homeassistant/components/generic/const.py | 1 + .../components/generic/manifest.json | 1 + homeassistant/components/generic/strings.json | 8 +- .../components/generic/translations/en.json | 10 +- tests/components/generic/conftest.py | 1 - tests/components/generic/test_camera.py | 2 + tests/components/generic/test_config_flow.py | 139 +++++++++++++++--- 8 files changed, 216 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 514264f919e..19ab7666b7c 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -3,17 +3,21 @@ from __future__ import annotations from collections.abc import Mapping import contextlib +from datetime import datetime from errno import EHOSTUNREACH, EIO import io import logging from typing import Any import PIL +from aiohttp import web from async_timeout import timeout from httpx import HTTPStatusError, RequestError, TimeoutException import voluptuous as vol import yarl +from homeassistant.components.camera import CAMERA_IMAGE_TIMEOUT, _async_get_image +from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.stream import ( CONF_RTSP_TRANSPORT, CONF_USE_WALLCLOCK_AS_TIMESTAMPS, @@ -33,14 +37,15 @@ from homeassistant.const import ( HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import FlowResult, UnknownFlow from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template as template_helper from homeassistant.helpers.httpx_client import get_async_client from homeassistant.util import slugify -from .camera import generate_auth +from .camera import GenericCamera, generate_auth from .const import ( + CONF_CONFIRMED_OK, CONF_CONTENT_TYPE, CONF_FRAMERATE, CONF_LIMIT_REFETCH_TO_URL_CHANGE, @@ -62,6 +67,7 @@ DEFAULT_DATA = { } SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml", "webp"} +IMAGE_PREVIEWS_ACTIVE = "previews" def build_schema( @@ -190,6 +196,7 @@ def slug( hass: HomeAssistant, template: str | template_helper.Template | None ) -> str | None: """Convert a camera url into a string suitable for a camera name.""" + url = "" if not template: return None if not isinstance(template, template_helper.Template): @@ -197,10 +204,8 @@ def slug( try: url = template.async_render(parse_result=False) return slugify(yarl.URL(url).host) - except TemplateError as err: - _LOGGER.error("Syntax error in '%s': %s", template.template, err) - except (ValueError, TypeError) as err: - _LOGGER.error("Syntax error in '%s': %s", url, err) + except (ValueError, TemplateError, TypeError) as err: + _LOGGER.error("Syntax error in '%s': %s", template, err) return None @@ -261,6 +266,16 @@ async def async_test_stream( return {} +def register_preview(hass: HomeAssistant): + """Set up previews for camera feeds during config flow.""" + hass.data.setdefault(DOMAIN, {}) + + if not hass.data[DOMAIN].get(IMAGE_PREVIEWS_ACTIVE): + _LOGGER.debug("Registering camera image preview handler") + hass.http.register_view(CameraImagePreview(hass)) + hass.data[DOMAIN][IMAGE_PREVIEWS_ACTIVE] = True + + class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for generic IP camera.""" @@ -268,8 +283,8 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize Generic ConfigFlow.""" - self.cached_user_input: dict[str, Any] = {} - self.cached_title = "" + self.user_input: dict[str, Any] = {} + self.title = "" @staticmethod def async_get_options_flow( @@ -314,19 +329,45 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): # The automatically generated still image that stream generates # is always jpeg user_input[CONF_CONTENT_TYPE] = "image/jpeg" + self.user_input = user_input + self.title = name - return self.async_create_entry( - title=name, data={}, options=user_input - ) + # temporary preview for user to check the image + self.context["preview_cam"] = user_input + return await self.async_step_user_confirm_still() + elif self.user_input: + user_input = self.user_input else: user_input = DEFAULT_DATA.copy() - return self.async_show_form( step_id="user", data_schema=build_schema(user_input), errors=errors, ) + async def async_step_user_confirm_still( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle user clicking confirm after still preview.""" + if user_input: + if not user_input.get(CONF_CONFIRMED_OK): + return await self.async_step_user() + return self.async_create_entry( + title=self.title, data={}, options=self.user_input + ) + register_preview(self.hass) + preview_url = f"/api/generic/preview_flow_image/{self.flow_id}?t={datetime.now().isoformat()}" + return self.async_show_form( + step_id="user_confirm_still", + data_schema=vol.Schema( + { + vol.Required(CONF_CONFIRMED_OK, default=False): bool, + } + ), + description_placeholders={"preview_url": preview_url}, + errors=None, + ) + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: """Handle config import from yaml.""" # abort if we've already got this one. @@ -410,3 +451,33 @@ class GenericOptionsFlowHandler(OptionsFlow): ), errors=errors, ) + + +class CameraImagePreview(HomeAssistantView): + """Camera view to temporarily serve an image.""" + + url = "/api/generic/preview_flow_image/{flow_id}" + name = "api:generic:preview_flow_image" + requires_auth = False + + def __init__(self, hass: HomeAssistant) -> None: + """Initialise.""" + self.hass = hass + + async def get(self, request: web.Request, flow_id: str) -> web.Response: + """Start a GET request.""" + _LOGGER.debug("processing GET request for flow_id=%s", flow_id) + try: + flow: FlowResult = self.hass.config_entries.flow.async_get(flow_id) + except UnknownFlow as exc: + raise web.HTTPNotFound() from exc + user_input = flow["context"]["preview_cam"] + camera = GenericCamera(self.hass, user_input, flow_id, "preview") + if not camera.is_on: + _LOGGER.debug("Camera is off") + raise web.HTTPServiceUnavailable() + image = await _async_get_image( + camera, + CAMERA_IMAGE_TIMEOUT, + ) + return web.Response(body=image.content, content_type=image.content_type) diff --git a/homeassistant/components/generic/const.py b/homeassistant/components/generic/const.py index eb0d81d493c..eb376909422 100644 --- a/homeassistant/components/generic/const.py +++ b/homeassistant/components/generic/const.py @@ -2,6 +2,7 @@ DOMAIN = "generic" DEFAULT_NAME = "Generic Camera" +CONF_CONFIRMED_OK = "confirmed_ok" CONF_CONTENT_TYPE = "content_type" CONF_LIMIT_REFETCH_TO_URL_CHANGE = "limit_refetch_to_url_change" CONF_STILL_IMAGE_URL = "still_image_url" diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index 8749a45d3de..78c3625abc7 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -3,6 +3,7 @@ "name": "Generic Camera", "config_flow": true, "requirements": ["ha-av==10.0.0b5", "pillow==9.2.0"], + "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/generic", "codeowners": ["@davet2001"], "iot_class": "local_push" diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index 608c85c1379..7ada90e3c90 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -40,8 +40,12 @@ "content_type": "Content Type" } }, - "confirm": { - "description": "[%key:common::config_flow::description::confirm_setup%]" + "user_confirm_still": { + "title": "Preview", + "description": "![Camera Still Image Preview]({preview_url})", + "data": { + "confirmed_ok": "This image looks good." + } } } }, diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json index a4e96718225..1953610395b 100644 --- a/homeassistant/components/generic/translations/en.json +++ b/homeassistant/components/generic/translations/en.json @@ -23,9 +23,6 @@ "unknown": "Unexpected error" }, "step": { - "confirm": { - "description": "Do you want to start set up?" - }, "content_type": { "data": { "content_type": "Content Type" @@ -45,6 +42,13 @@ "verify_ssl": "Verify SSL certificate" }, "description": "Enter the settings to connect to the camera." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "This image looks good." + }, + "description": "![Camera Still Image Preview]({preview_url})", + "title": "Preview" } } }, diff --git a/tests/components/generic/conftest.py b/tests/components/generic/conftest.py index 808e858b259..74679f050b6 100644 --- a/tests/components/generic/conftest.py +++ b/tests/components/generic/conftest.py @@ -78,7 +78,6 @@ def mock_create_stream(): @pytest.fixture async def user_flow(hass): """Initiate a user flow.""" - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index f7e1898f735..04c7fcca5b5 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -20,6 +20,7 @@ from homeassistant.components.generic.const import ( CONF_STREAM_SOURCE, DOMAIN, ) +from homeassistant.components.stream.const import CONF_RTSP_TRANSPORT from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL @@ -209,6 +210,7 @@ async def test_stream_source(hass, hass_client, hass_ws_client, fakeimgbytes_png CONF_VERIFY_SSL: False, CONF_USERNAME: "barney", CONF_PASSWORD: "betty", + CONF_RTSP_TRANSPORT: "http", }, ) mock_entry.add_to_hass(hass) diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index d303e064c1f..6ec1ce0c32b 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -1,8 +1,9 @@ """Test The generic (IP Camera) config flow.""" import errno +from http import HTTPStatus import os.path -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, PropertyMock, patch import httpx import pytest @@ -12,6 +13,7 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components.camera import async_get_image from homeassistant.components.generic.config_flow import slug from homeassistant.components.generic.const import ( + CONF_CONFIRMED_OK, CONF_CONTENT_TYPE, CONF_FRAMERATE, CONF_LIMIT_REFETCH_TO_URL_CHANGE, @@ -58,16 +60,30 @@ TESTDATA_YAML = { @respx.mock -async def test_form(hass, fakeimg_png, user_flow, mock_create_stream): +async def test_form(hass, fakeimgbytes_png, hass_client, user_flow, mock_create_stream): """Test the form with a normal set of settings.""" + respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) with mock_create_stream as mock_setup, patch( "homeassistant.components.generic.async_setup_entry", return_value=True ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( + result1 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], TESTDATA, ) + assert result1["type"] == data_entry_flow.FlowResultType.FORM + assert result1["step_id"] == "user_confirm_still" + client = await hass_client() + preview_id = result1["flow_id"] + # Check the preview image works. + resp = await client.get(f"/api/generic/preview_flow_image/{preview_id}?t=1") + assert resp.status == HTTPStatus.OK + assert await resp.read() == fakeimgbytes_png + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + user_input={CONF_CONFIRMED_OK: True}, + ) + await hass.async_block_till_done() assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { @@ -83,6 +99,9 @@ async def test_form(hass, fakeimg_png, user_flow, mock_create_stream): } await hass.async_block_till_done() + # Check that the preview image is disabled after. + resp = await client.get(f"/api/generic/preview_flow_image/{preview_id}") + assert resp.status == HTTPStatus.NOT_FOUND assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -99,11 +118,17 @@ async def test_form_only_stillimage(hass, fakeimg_png, user_flow): data = TESTDATA.copy() data.pop(CONF_STREAM_SOURCE) with patch("homeassistant.components.generic.async_setup_entry", return_value=True): - result2 = await hass.config_entries.flow.async_configure( + result1 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], data, ) await hass.async_block_till_done() + assert result1["type"] == data_entry_flow.FlowResultType.FORM + assert result1["step_id"] == "user_confirm_still" + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + user_input={CONF_CONFIRMED_OK: True}, + ) assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { @@ -120,16 +145,65 @@ async def test_form_only_stillimage(hass, fakeimg_png, user_flow): assert respx.calls.call_count == 1 +@respx.mock +async def test_form_reject_still_preview( + hass, fakeimgbytes_png, mock_create_stream, user_flow +): + """Test we go back to the config screen if the user rejects the still preview.""" + respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) + with mock_create_stream: + result1 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result1["step_id"] == "user_confirm_still" + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + user_input={CONF_CONFIRMED_OK: False}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "user" + + +@respx.mock +async def test_form_still_preview_cam_off( + hass, fakeimg_png, mock_create_stream, user_flow, hass_client +): + """Test camera errors are triggered during preview.""" + with patch( + "homeassistant.components.generic.camera.GenericCamera.is_on", + new_callable=PropertyMock(return_value=False), + ), mock_create_stream: + result1 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result1["step_id"] == "user_confirm_still" + preview_id = result1["flow_id"] + # Try to view the image, should be unavailable. + client = await hass_client() + resp = await client.get(f"/api/generic/preview_flow_image/{preview_id}?t=1") + assert resp.status == HTTPStatus.SERVICE_UNAVAILABLE + + @respx.mock async def test_form_only_stillimage_gif(hass, fakeimg_gif, user_flow): """Test we complete ok if the user wants a gif.""" data = TESTDATA.copy() data.pop(CONF_STREAM_SOURCE) with patch("homeassistant.components.generic.async_setup_entry", return_value=True): - result2 = await hass.config_entries.flow.async_configure( + result1 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], data, ) + assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result1["step_id"] == "user_confirm_still" + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + user_input={CONF_CONFIRMED_OK: True}, + ) await hass.async_block_till_done() assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["options"][CONF_CONTENT_TYPE] == "image/gif" @@ -143,11 +217,17 @@ async def test_form_only_svg_whitespace(hass, fakeimgbytes_svg, user_flow): data = TESTDATA.copy() data.pop(CONF_STREAM_SOURCE) with patch("homeassistant.components.generic.async_setup_entry", return_value=True): - result2 = await hass.config_entries.flow.async_configure( + result1 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], data, ) - await hass.async_block_till_done() + assert result1["type"] == data_entry_flow.FlowResultType.FORM + assert result1["step_id"] == "user_confirm_still" + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + user_input={CONF_CONFIRMED_OK: True}, + ) + await hass.async_block_till_done() assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @@ -170,10 +250,16 @@ async def test_form_only_still_sample(hass, user_flow, image_file): data = TESTDATA.copy() data.pop(CONF_STREAM_SOURCE) with patch("homeassistant.components.generic.async_setup_entry", return_value=True): - result2 = await hass.config_entries.flow.async_configure( + result1 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], data, ) + assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result1["step_id"] == "user_confirm_still" + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + user_input={CONF_CONFIRMED_OK: True}, + ) await hass.async_block_till_done() assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @@ -186,31 +272,31 @@ async def test_form_only_still_sample(hass, user_flow, image_file): ( "http://localhost:812{{3}}/static/icons/favicon-apple-180x180.png", "http://localhost:8123/static/icons/favicon-apple-180x180.png", - data_entry_flow.FlowResultType.CREATE_ENTRY, + "user_confirm_still", None, ), ( "{% if 1 %}https://bla{% else %}https://yo{% endif %}", "https://bla/", - data_entry_flow.FlowResultType.CREATE_ENTRY, + "user_confirm_still", None, ), ( "http://{{example.org", "http://example.org", - data_entry_flow.FlowResultType.FORM, + "user", {"still_image_url": "template_error"}, ), ( "invalid1://invalid:4\\1", "invalid1://invalid:4%5c1", - data_entry_flow.FlowResultType.FORM, + "user", {"still_image_url": "malformed_url"}, ), ( "relative/urls/are/not/allowed.jpg", "relative/urls/are/not/allowed.jpg", - data_entry_flow.FlowResultType.FORM, + "user", {"still_image_url": "relative_url"}, ), ], @@ -229,7 +315,7 @@ async def test_still_template( data, ) await hass.async_block_till_done() - assert result2["type"] == expected_result + assert result2["step_id"] == expected_result assert result2.get("errors") == expected_errors @@ -242,10 +328,15 @@ async def test_form_rtsp_mode(hass, fakeimg_png, user_flow, mock_create_stream): with mock_create_stream as mock_setup, patch( "homeassistant.components.generic.async_setup_entry", return_value=True ): - result2 = await hass.config_entries.flow.async_configure( + result1 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], data ) - assert "errors" not in result2, f"errors={result2['errors']}" + assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result1["step_id"] == "user_confirm_still" + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + user_input={CONF_CONFIRMED_OK: True}, + ) assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { @@ -265,21 +356,23 @@ async def test_form_rtsp_mode(hass, fakeimg_png, user_flow, mock_create_stream): assert len(mock_setup.mock_calls) == 1 -async def test_form_only_stream(hass, fakeimgbytes_jpg, mock_create_stream): +async def test_form_only_stream(hass, fakeimgbytes_jpg, user_flow, mock_create_stream): """Test we complete ok if the user wants stream only.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) data = TESTDATA.copy() data.pop(CONF_STILL_IMAGE_URL) data[CONF_STREAM_SOURCE] = "rtsp://user:pass@127.0.0.1/testurl/2" with mock_create_stream as mock_setup: - result3 = await hass.config_entries.flow.async_configure( - result["flow_id"], + result1 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], data, ) + assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result1["step_id"] == "user_confirm_still" + result3 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + user_input={CONF_CONFIRMED_OK: True}, + ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "127_0_0_1" assert result3["options"] == { From bba7b3b2be0514836f1e09a0a0aa1d1ae226247a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20L=C3=B6vdahl?= Date: Fri, 7 Oct 2022 00:47:50 +0300 Subject: [PATCH 208/985] Fix broken URLs in KNX service descriptions (#79752) --- homeassistant/components/knx/services.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml index 45ef6dcbd12..d95a1573872 100644 --- a/homeassistant/components/knx/services.yaml +++ b/homeassistant/components/knx/services.yaml @@ -18,7 +18,7 @@ send: object: type: name: "Value type" - description: "If set, the payload will not be sent as raw bytes, but encoded as given DPT. Knx sensor types are valid values (see https://www.home-assistant.io/integrations/sensor.knx)." + description: "If set, the payload will not be sent as raw bytes, but encoded as given DPT. KNX sensor types are valid values (see https://www.home-assistant.io/integrations/knx/#value-types)." required: false example: "temperature" selector: @@ -53,7 +53,7 @@ event_register: object: type: name: "Value type" - description: "If set, the payload will be decoded as given DPT in the event data `value` key. Knx sensor types are valid values (see https://www.home-assistant.io/integrations/sensor.knx)." + description: "If set, the payload will be decoded as given DPT in the event data `value` key. KNX sensor types are valid values (see https://www.home-assistant.io/integrations/knx/#value-types)." required: false example: "2byte_float" selector: @@ -77,7 +77,7 @@ exposure_register: text: type: name: "Value type" - description: "Telegrams will be encoded as given DPT. 'binary' and all Knx sensor types are valid values (see https://www.home-assistant.io/integrations/sensor.knx)" + description: "Telegrams will be encoded as given DPT. 'binary' and all KNX sensor types are valid values (see https://www.home-assistant.io/integrations/knx/#value-types)" required: true example: "percentU8" selector: From 53c51c92212cde7496b0af87dce48d39593a706d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 7 Oct 2022 01:16:38 +0200 Subject: [PATCH 209/985] Uninstall pre-installed tools from devcontainer (#79765) --- .devcontainer/devcontainer.json | 8 +++++++- Dockerfile.dev | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ba2911dcf0c..fe0d53a92ef 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -17,8 +17,14 @@ // Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json "settings": { "python.pythonPath": "/usr/local/bin/python", - "python.linting.pylintEnabled": true, "python.linting.enabled": true, + "python.linting.pylintEnabled": true, + "python.formatting.blackPath": "/usr/local/bin/black", + "python.linting.flake8Path": "/usr/local/bin/flake8", + "python.linting.pycodestylePath": "/usr/local/bin/pycodestyle", + "python.linting.pydocstylePath": "/usr/local/bin/pydocstyle", + "python.linting.mypyPath": "/usr/local/bin/mypy", + "python.linting.pylintPath": "/usr/local/bin/pylint", "python.formatting.provider": "black", "python.testing.pytestArgs": ["--no-cov"], "editor.formatOnPaste": false, diff --git a/Dockerfile.dev b/Dockerfile.dev index 0559ebb43cd..fc9843461a0 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -2,6 +2,15 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.9 SHELL ["/bin/bash", "-o", "pipefail", "-c"] +# Uninstall pre-installed formatting and linting tools +# They would conflict with our pinned versions +RUN pipx uninstall black +RUN pipx uninstall flake8 +RUN pipx uninstall pydocstyle +RUN pipx uninstall pycodestyle +RUN pipx uninstall mypy +RUN pipx uninstall pylint + RUN \ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ && apt-get update \ From bc1941717ccb3cea6f4c6f4fc3a28c9399e63e83 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 7 Oct 2022 12:17:31 +1300 Subject: [PATCH 210/985] Bump aioesphomeapi to 11.1.1 (#79762) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 066050d796d..3ffceaae971 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==11.1.0"], + "requirements": ["aioesphomeapi==11.1.1"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index cc0027f0b69..78e310502d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -156,7 +156,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.1.0 +aioesphomeapi==11.1.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a5d09b2773..0597ea7f800 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -143,7 +143,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.1.0 +aioesphomeapi==11.1.1 # homeassistant.components.flo aioflo==2021.11.0 From eb19927df61fbe7717476ebfe026a241c2899ff5 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Fri, 7 Oct 2022 01:18:13 +0200 Subject: [PATCH 211/985] Bump pydaikin version (#79761) bump pydaikin version --- homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 28bfec14760..0657f597a5d 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -3,7 +3,7 @@ "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": ["pydaikin==2.7.0"], + "requirements": ["pydaikin==2.7.2"], "codeowners": ["@fredrike"], "zeroconf": ["_dkapi._tcp.local."], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 78e310502d2..64459d5fd5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1502,7 +1502,7 @@ pycsspeechtts==1.0.4 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.7.0 +pydaikin==2.7.2 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0597ea7f800..36d0785d580 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1060,7 +1060,7 @@ pycomfoconnect==0.4 pycoolmasternet-async==0.1.2 # homeassistant.components.daikin -pydaikin==2.7.0 +pydaikin==2.7.2 # homeassistant.components.deconz pydeconz==104 From 7b2cad388e03e31a04f97096bd5359c39d774e48 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 7 Oct 2022 01:22:15 +0200 Subject: [PATCH 212/985] Show all valid heatpump selections (#79756) Iterate over the keys of the member dunder --- homeassistant/components/nibe_heatpump/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nibe_heatpump/config_flow.py b/homeassistant/components/nibe_heatpump/config_flow.py index f8e4974b79b..ba3325d9daf 100644 --- a/homeassistant/components/nibe_heatpump/config_flow.py +++ b/homeassistant/components/nibe_heatpump/config_flow.py @@ -29,7 +29,7 @@ from .const import ( STEP_USER_DATA_SCHEMA = vol.Schema( { - vol.Required(CONF_MODEL): vol.In([e.name for e in Model]), + vol.Required(CONF_MODEL): vol.In(list(Model.__members__)), vol.Required(CONF_IP_ADDRESS): str, vol.Required(CONF_LISTENING_PORT, default=9999): cv.port, vol.Required(CONF_REMOTE_READ_PORT, default=9999): cv.port, From e1047320a9153687d8edb203c5adf7734a8b02a4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 7 Oct 2022 00:38:36 +0000 Subject: [PATCH 213/985] [ci skip] Translation update --- .../airthings_ble/translations/ja.json | 23 ++++++++++++++++++ .../alarm_control_panel/translations/is.json | 16 ++++++++++++- .../components/apcupsd/translations/ca.json | 9 ++++++- .../components/apcupsd/translations/ja.json | 18 ++++++++++++++ .../binary_sensor/translations/is.json | 24 +++++++++++++++++++ .../components/braviatv/translations/ca.json | 3 ++- .../components/braviatv/translations/ja.json | 15 ++++++++++-- .../components/cover/translations/is.json | 9 +++++++ .../dsmr_reader/translations/ca.json | 6 +++++ .../dsmr_reader/translations/ja.json | 7 ++++++ .../components/ezviz/translations/ca.json | 4 ++-- .../components/generic/translations/el.json | 7 ++++++ .../components/generic/translations/en.json | 3 +++ .../google_sheets/translations/ja.json | 3 +++ .../litterrobot/translations/ca.json | 6 +++++ .../components/mikrotik/translations/ja.json | 10 +++++++- .../components/moon/translations/ca.json | 2 +- .../components/octoprint/translations/ja.json | 6 +++++ .../components/plugwise/translations/en.json | 22 +++++++++++++++++ .../plugwise/translations/select.el.json | 11 +++++++++ .../components/radarr/translations/ja.json | 16 +++++++++++++ .../rtsp_to_webrtc/translations/ca.json | 9 +++++++ .../components/season/translations/ca.json | 3 ++- .../components/shelly/translations/ja.json | 7 ++++++ .../components/uptime/translations/ca.json | 5 ++-- .../components/zha/translations/ca.json | 16 +++++++++++++ .../components/zha/translations/el.json | 16 +++++++++++++ .../components/zha/translations/en.json | 23 ++++++++++++++++++ .../components/zha/translations/es.json | 16 +++++++++++++ .../components/zha/translations/ru.json | 16 +++++++++++++ 30 files changed, 319 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/airthings_ble/translations/ja.json create mode 100644 homeassistant/components/apcupsd/translations/ja.json create mode 100644 homeassistant/components/dsmr_reader/translations/ja.json create mode 100644 homeassistant/components/plugwise/translations/select.el.json create mode 100644 homeassistant/components/radarr/translations/ja.json diff --git a/homeassistant/components/airthings_ble/translations/ja.json b/homeassistant/components/airthings_ble/translations/ja.json new file mode 100644 index 00000000000..07feda5788d --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/ja.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/translations/is.json b/homeassistant/components/alarm_control_panel/translations/is.json index eda11e6177f..16c2aeec21d 100644 --- a/homeassistant/components/alarm_control_panel/translations/is.json +++ b/homeassistant/components/alarm_control_panel/translations/is.json @@ -1,4 +1,18 @@ { + "device_automation": { + "condition_type": { + "is_armed_away": "{entity_name} er \u00e1 ver\u00f0i \u00fati", + "is_armed_home": "{entity_name} er \u00e1 ver\u00f0i heima", + "is_armed_night": "{entity_name} er \u00e1 ver\u00f0i n\u00f3tt", + "is_disarmed": "{entity_name} er ekki \u00e1 ver\u00f0i" + }, + "trigger_type": { + "armed_away": "{entity_name} \u00e1 ver\u00f0i \u00fati", + "armed_home": "\u00e1 ver\u00f0i heima", + "armed_night": "\u00e1 ver\u00f0i n\u00f3tt", + "disarmed": "ekki \u00e1 ver\u00f0i" + } + }, "state": { "_": { "armed": "\u00c1 ver\u00f0i", @@ -6,7 +20,7 @@ "armed_home": "\u00c1 ver\u00f0i heima", "armed_night": "\u00c1 ver\u00f0i n\u00f3tt", "arming": "Set \u00e1 v\u00f6r\u00f0", - "disarmed": "ekki \u00e1 ver\u00f0i", + "disarmed": "Ekki \u00e1 ver\u00f0i", "disarming": "tek af ver\u00f0i", "pending": "B\u00ed\u00f0ur", "triggered": "R\u00e6st" diff --git a/homeassistant/components/apcupsd/translations/ca.json b/homeassistant/components/apcupsd/translations/ca.json index bd4f7ee1826..3c890da936b 100644 --- a/homeassistant/components/apcupsd/translations/ca.json +++ b/homeassistant/components/apcupsd/translations/ca.json @@ -12,8 +12,15 @@ "data": { "host": "Amfitri\u00f3", "port": "Port" - } + }, + "description": "Introdueix l'amfitri\u00f3 i el port en qu\u00e8 s'est\u00e0 servint apcupsd NIS." } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 d'APC UPS Deamon mitjan\u00e7ant YAML s'est\u00e0 eliminant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML d'APC UPS Deamon del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML d'APC UPS Deamon est\u00e0 sent eliminada" + } } } \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/ja.json b/homeassistant/components/apcupsd/translations/ja.json new file mode 100644 index 00000000000..217f813b894 --- /dev/null +++ b/homeassistant/components/apcupsd/translations/ja.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/is.json b/homeassistant/components/binary_sensor/translations/is.json index 846e4ba1860..ac2c9901cad 100644 --- a/homeassistant/components/binary_sensor/translations/is.json +++ b/homeassistant/components/binary_sensor/translations/is.json @@ -1,4 +1,28 @@ { + "device_automation": { + "condition_type": { + "is_co": "{entity_name} skynja\u00f0i kolm\u00f3noxi\u00f0", + "is_gas": "{entity_name} skynja\u00f0i gas", + "is_light": "{entity_name} skynja\u00f0i lj\u00f3s", + "is_motion": "{entity_name} skynja\u00f0i hreyfingu", + "is_no_motion": "{entity_name} er ekki a\u00f0 skynja hreyfingu", + "is_no_smoke": "{entity_name} er ekki a\u00f0 skynja reyk", + "is_not_open": "{entity_name} er loku\u00f0", + "is_problem": "{entity_name} skynja\u00f0i vandam\u00e1l", + "is_smoke": "{entity_name} skynja\u00f0i reyk", + "is_sound": "{entity_name} skynja\u00f0i hlj\u00f3\u00f0", + "is_tampered": "{entity_name} skynja\u00f0i fikt", + "is_vibration": "{entity_name} skynja\u00f0i titring" + }, + "trigger_type": { + "gas": "{entity_name} byrja\u00f0i a\u00f0 skynja gas", + "motion": "{entity_name} byrja\u00f0i a\u00f0 skynja hreyfingu", + "no_motion": "{entity_name} h\u00e6tti a\u00f0 skynja hreyfingu", + "not_opened": "{entity_name} loku\u00f0", + "opened": "{entity_name} opnu\u00f0", + "problem": "{entity_name} byrja\u00f0i a\u00f0 skynja vandam\u00e1l" + } + }, "state": { "_": { "off": "Sl\u00f6kkt", diff --git a/homeassistant/components/braviatv/translations/ca.json b/homeassistant/components/braviatv/translations/ca.json index 08599a29a06..f686c076bb8 100644 --- a/homeassistant/components/braviatv/translations/ca.json +++ b/homeassistant/components/braviatv/translations/ca.json @@ -29,7 +29,8 @@ "data": { "pin": "Codi PIN", "use_psk": "Utilitza autenticaci\u00f3 PSK" - } + }, + "description": "Introdueix el codi PIN que es mostra al televisor Sony Bravia.\n\nSi no es mostra el codi, has d'eliminar Home Assistant del teu televisor. V\u00e9s a: Configuraci\u00f3 -> Xarxa -> Configuraci\u00f3 de dispositiu remot -> Elimina dispositiu remot.\n\nPots utilitzar una clau PSK (Pre-Shared-Key) enlloc d'un codi PIN. La clau PSK est\u00e0 definida per l'usuari i s'utilitza per al control d'acc\u00e9s. Es recomana aquest m\u00e8tode d'autenticaci\u00f3, ja que \u00e9s m\u00e9s estable. Per activar la clau PSK, v\u00e9s a: Configuraci\u00f3 -> Xarxa -> Configuraci\u00f3 de xarxa local -> Control IP. Tot seguit, marca la casella \u00abUtilitza autenticaci\u00f3 PSK\u00bb i introdueix la clau que desitgis enlloc del PIN." }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/ja.json b/homeassistant/components/braviatv/translations/ja.json index 263de9e35b0..f573b562f6c 100644 --- a/homeassistant/components/braviatv/translations/ja.json +++ b/homeassistant/components/braviatv/translations/ja.json @@ -2,21 +2,32 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "no_ip_control": "\u30c6\u30ec\u30d3\u3067IP\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u304c\u7121\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u304b\u3001\u30c6\u30ec\u30d3\u304c\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093\u3002" + "no_ip_control": "\u30c6\u30ec\u30d3\u3067IP\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u304c\u7121\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u304b\u3001\u30c6\u30ec\u30d3\u304c\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093\u3002", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9", "unsupported_model": "\u304a\u4f7f\u3044\u306e\u30c6\u30ec\u30d3\u306e\u30e2\u30c7\u30eb\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" }, "step": { "authorize": { "data": { - "pin": "PIN\u30b3\u30fc\u30c9" + "pin": "PIN\u30b3\u30fc\u30c9", + "use_psk": "PSK\u8a8d\u8a3c\u3092\u4f7f\u7528\u3059\u308b" }, "description": "\u30bd\u30cb\u30fc Bravia TV\u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u307e\u3059\u3002 \n\nPIN\u30b3\u30fc\u30c9\u304c\u8868\u793a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u30c6\u30ec\u30d3\u304b\u3089Home Assistant\u306e\u767b\u9332\u3092\u89e3\u9664\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u306e\u3067\u3001\u6b21\u306e\u624b\u9806\u3067\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002\u8a2d\u5b9a \u2192 \u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u2192 \u30ea\u30e2\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a \u2192 \u30ea\u30e2\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u767b\u9332\u89e3\u9664 \u3092\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u30bd\u30cb\u30fc Bravia TV\u3092\u8a8d\u8a3c\u3059\u308b" }, + "confirm": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + }, + "reauth_confirm": { + "data": { + "pin": "PIN\u30b3\u30fc\u30c9" + } + }, "user": { "data": { "host": "\u30db\u30b9\u30c8" diff --git a/homeassistant/components/cover/translations/is.json b/homeassistant/components/cover/translations/is.json index 4a61c4f7cc5..ce8052eb02b 100644 --- a/homeassistant/components/cover/translations/is.json +++ b/homeassistant/components/cover/translations/is.json @@ -1,4 +1,13 @@ { + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} er loku\u00f0" + }, + "trigger_type": { + "closed": "{entity_name} loku\u00f0", + "opened": "{entity_name} opnu\u00f0" + } + }, "state": { "_": { "closed": "Loka\u00f0", diff --git a/homeassistant/components/dsmr_reader/translations/ca.json b/homeassistant/components/dsmr_reader/translations/ca.json index 901034bca2a..24cd0ce614b 100644 --- a/homeassistant/components/dsmr_reader/translations/ca.json +++ b/homeassistant/components/dsmr_reader/translations/ca.json @@ -2,10 +2,16 @@ "config": { "abort": { "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "confirm": { + "description": "Assegura't de configurar les fonts de dades de 'split topic' de DSMR Reader." + } } }, "issues": { "deprecated_yaml": { + "description": "La configuraci\u00f3 de Lector DSMR mitjan\u00e7ant YAML s'est\u00e0 eliminant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML de Lector DSMR del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", "title": "La configuraci\u00f3 YAML de DSMR Reader est\u00e0 sent eliminada" } } diff --git a/homeassistant/components/dsmr_reader/translations/ja.json b/homeassistant/components/dsmr_reader/translations/ja.json new file mode 100644 index 00000000000..cf3ac93acad --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/ca.json b/homeassistant/components/ezviz/translations/ca.json index 08c9f2af4d1..126a563a1e5 100644 --- a/homeassistant/components/ezviz/translations/ca.json +++ b/homeassistant/components/ezviz/translations/ca.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "El compte ja est\u00e0 configurat", - "ezviz_cloud_account_missing": "Falta el compte d'Ezviz cloud. Torna'l a configurar", + "ezviz_cloud_account_missing": "Falta el compte d'EZVIZ cloud. Torna'l a configurar", "unknown": "Error inesperat" }, "error": { @@ -17,7 +17,7 @@ "password": "Contrasenya", "username": "Nom d'usuari" }, - "description": "Introdueix les credencials RTSP per a la c\u00e0mera Ezviz {serial} amb IP {ip_address}", + "description": "Introdueix les credencials RTSP de la c\u00e0mera EZVIZ {serial} amb IP {ip_address}", "title": "S'ha descobert una c\u00e0mera EZVIZ" }, "user": { diff --git a/homeassistant/components/generic/translations/el.json b/homeassistant/components/generic/translations/el.json index 29063cdc216..eda806cc137 100644 --- a/homeassistant/components/generic/translations/el.json +++ b/homeassistant/components/generic/translations/el.json @@ -45,6 +45,13 @@ "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" }, "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1 \u03c6\u03b1\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03ba\u03b1\u03bb\u03ae." + }, + "description": "! [\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03c3\u03ba\u03cc\u03c0\u03b7\u03c3\u03b7 \u03c3\u03c4\u03b1\u03c4\u03b9\u03ba\u03ae\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1\u03c2] ({preview_url})", + "title": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03c3\u03ba\u03cc\u03c0\u03b7\u03c3\u03b7" } } }, diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json index 1953610395b..18c29225ffe 100644 --- a/homeassistant/components/generic/translations/en.json +++ b/homeassistant/components/generic/translations/en.json @@ -23,6 +23,9 @@ "unknown": "Unexpected error" }, "step": { + "confirm": { + "description": "Do you want to start set up?" + }, "content_type": { "data": { "content_type": "Content Type" diff --git a/homeassistant/components/google_sheets/translations/ja.json b/homeassistant/components/google_sheets/translations/ja.json index e37b4517358..5b574e5ee86 100644 --- a/homeassistant/components/google_sheets/translations/ja.json +++ b/homeassistant/components/google_sheets/translations/ja.json @@ -17,6 +17,9 @@ }, "pick_implementation": { "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + }, + "reauth_confirm": { + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/litterrobot/translations/ca.json b/homeassistant/components/litterrobot/translations/ca.json index 10506ab95d6..059e0ea6236 100644 --- a/homeassistant/components/litterrobot/translations/ca.json +++ b/homeassistant/components/litterrobot/translations/ca.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "Els atributs de l'entitat aspirador ara estan disponibles com a sensors de diagn\u00f2stic. \n\nActualitza les automatitzacions o scripts que tinguis que utilitzin aquests atributs.", + "title": "Els atributs de Litter-Robot s\u00f3n ara els seus propis sensors" + } } } \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/ja.json b/homeassistant/components/mikrotik/translations/ja.json index 93cde1c8391..27296d92e45 100644 --- a/homeassistant/components/mikrotik/translations/ja.json +++ b/homeassistant/components/mikrotik/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", @@ -9,6 +10,13 @@ "name_exists": "\u540d\u524d\u304c\u5b58\u5728\u3057\u307e\u3059" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u7121\u52b9\u3067\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8", diff --git a/homeassistant/components/moon/translations/ca.json b/homeassistant/components/moon/translations/ca.json index 6476e90a84b..ff98a6fea96 100644 --- a/homeassistant/components/moon/translations/ca.json +++ b/homeassistant/components/moon/translations/ca.json @@ -11,7 +11,7 @@ }, "issues": { "removed_yaml": { - "description": "La configuraci\u00f3 de la Lluna mitjan\u00e7ant YAML s'ha eliminat de Home Assistant.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "description": "La configuraci\u00f3 de la Lluna mitjan\u00e7ant YAML s'ha eliminat.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", "title": "La configuraci\u00f3 YAML de la Lluna s'ha eliminat" } }, diff --git a/homeassistant/components/octoprint/translations/ja.json b/homeassistant/components/octoprint/translations/ja.json index a7ce93830cf..5b54999c782 100644 --- a/homeassistant/components/octoprint/translations/ja.json +++ b/homeassistant/components/octoprint/translations/ja.json @@ -4,6 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "auth_failed": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3API \u30ad\u30fc\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { @@ -15,6 +16,11 @@ "get_api_key": "OctoPrint UI\u3092\u958b\u304d\u3001Home Assistant\u306e\u30a2\u30af\u30bb\u30b9\u30ea\u30af\u30a8\u30b9\u30c8\u3067\u3092 '\u8a31\u53ef' \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002" }, "step": { + "reauth_confirm": { + "data": { + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + }, "user": { "data": { "host": "\u30db\u30b9\u30c8", diff --git a/homeassistant/components/plugwise/translations/en.json b/homeassistant/components/plugwise/translations/en.json index aa5a318bbff..cd10502d0c3 100644 --- a/homeassistant/components/plugwise/translations/en.json +++ b/homeassistant/components/plugwise/translations/en.json @@ -10,8 +10,20 @@ "invalid_setup": "Add your Adam instead of your Anna, see the Home Assistant Plugwise integration documentation for more information", "unknown": "Unexpected error" }, + "flow_title": "{name}", "step": { "user": { + "data": { + "flow_type": "Connection type", + "host": "IP Address", + "password": "Smile ID", + "port": "Port", + "username": "Smile Username" + }, + "description": "Please enter", + "title": "Connect to the Smile" + }, + "user_gateway": { "data": { "host": "IP Address", "password": "Smile ID", @@ -22,5 +34,15 @@ "title": "Connect to the Smile" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Scan Interval (seconds)" + }, + "description": "Adjust Plugwise Options" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/select.el.json b/homeassistant/components/plugwise/translations/select.el.json new file mode 100644 index 00000000000..88f8e117b48 --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.el.json @@ -0,0 +1,11 @@ +{ + "state": { + "plugwise__regulation_mode": { + "bleeding_cold": "\u0391\u03b9\u03bc\u03bf\u03c1\u03c1\u03b1\u03b3\u03af\u03b1 \u03ba\u03c1\u03cd\u03b1", + "bleeding_hot": "\u0391\u03b9\u03bc\u03bf\u03c1\u03c1\u03b1\u03b3\u03af\u03b1 \u03ba\u03b1\u03c5\u03c4\u03ae", + "cooling": "\u03a8\u03cd\u03be\u03b7", + "heating": "\u0398\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7", + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/ja.json b/homeassistant/components/radarr/translations/ja.json new file mode 100644 index 00000000000..add29174eaf --- /dev/null +++ b/homeassistant/components/radarr/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" + }, + "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "url": "URL", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rtsp_to_webrtc/translations/ca.json b/homeassistant/components/rtsp_to_webrtc/translations/ca.json index f55a493c49d..683383d8376 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/ca.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/ca.json @@ -23,5 +23,14 @@ "title": "Configuraci\u00f3 de RTSPtoWebRTC" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Adre\u00e7a del servidor stun (host:port)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/ca.json b/homeassistant/components/season/translations/ca.json index 6695356f33a..a2c47ff2fc3 100644 --- a/homeassistant/components/season/translations/ca.json +++ b/homeassistant/components/season/translations/ca.json @@ -13,7 +13,8 @@ }, "issues": { "removed_yaml": { - "title": "La configuraci\u00f3 YAML de Season s'ha eliminat" + "description": "La configuraci\u00f3 d'Estaci\u00f3 de l'any mitjan\u00e7ant YAML s'ha eliminat.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML d'Estaci\u00f3 de l'any s'ha eliminat" } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/ja.json b/homeassistant/components/shelly/translations/ja.json index 748e70fd32a..ac530669f7d 100644 --- a/homeassistant/components/shelly/translations/ja.json +++ b/homeassistant/components/shelly/translations/ja.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "unsupported_firmware": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u30d0\u30fc\u30b8\u30e7\u30f3\u306e\u30d5\u30a1\u30fc\u30e0\u30a6\u30a7\u30a2\u3092\u4f7f\u7528\u3057\u3066\u3044\u307e\u3059\u3002" }, "error": { @@ -21,6 +22,12 @@ "username": "\u30e6\u30fc\u30b6\u30fc\u540d" } }, + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + }, "user": { "data": { "host": "\u30db\u30b9\u30c8" diff --git a/homeassistant/components/uptime/translations/ca.json b/homeassistant/components/uptime/translations/ca.json index bbd6caebc11..ef0b636dcf7 100644 --- a/homeassistant/components/uptime/translations/ca.json +++ b/homeassistant/components/uptime/translations/ca.json @@ -11,8 +11,9 @@ }, "issues": { "removed_yaml": { - "title": "La configuraci\u00f3 YAML d'Uptime s'ha eliminat" + "description": "La configuraci\u00f3 de data i temps d'engegada mitjan\u00e7ant YAML s'ha eliminat.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de data i temps d'engegada s'ha eliminat" } }, - "title": "Temps en funcionament" + "title": "Data i temps d'engegada" } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index 073e72b80a3..c66de6758ed 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -116,6 +116,8 @@ }, "device_automation": { "action_type": { + "issue_all_led_effect": "Envia efecte a tots els LEDs", + "issue_individual_led_effect": "Envia efecte a un LED individual", "squawk": "Squawk", "warn": "Av\u00eds" }, @@ -210,6 +212,12 @@ "description": "ZHA s'aturar\u00e0. Vols continuar?", "title": "Reconfiguraci\u00f3 de ZHA" }, + "instruct_unplug": { + "title": "Desconnecta la r\u00e0dio antiga" + }, + "intent_migrate": { + "title": "Migra a una nova r\u00e0dio" + }, "manual_pick_radio_type": { "data": { "radio_type": "Tipus de r\u00e0dio" @@ -233,6 +241,14 @@ "description": "La teva c\u00f2pia de seguretat t\u00e9 una adre\u00e7a IEEE diferent de la teva r\u00e0dio. Perqu\u00e8 la xarxa funcioni correctament, tamb\u00e9 s'ha de canviar l'adre\u00e7a IEEE de la teva r\u00e0dio. \n\nAquesta \u00e9s una operaci\u00f3 permanent.", "title": "Sobreescriu l'adre\u00e7a IEEE r\u00e0dio" }, + "prompt_migrate_or_reconfigure": { + "description": "Est\u00e0s migrant a una r\u00e0dio nova o tornant a configurar la r\u00e0dio actual?", + "menu_options": { + "intent_migrate": "Migra a una nova r\u00e0dio", + "intent_reconfigure": "Torna a configurar la r\u00e0dio actual" + }, + "title": "Migraci\u00f3 o reconfiguraci\u00f3" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Puja un fitxer" diff --git a/homeassistant/components/zha/translations/el.json b/homeassistant/components/zha/translations/el.json index 154eb011da3..2d3ca09560c 100644 --- a/homeassistant/components/zha/translations/el.json +++ b/homeassistant/components/zha/translations/el.json @@ -212,6 +212,14 @@ "description": "\u03a4\u03bf ZHA \u03b8\u03b1 \u03c3\u03c4\u03b1\u03bc\u03b1\u03c4\u03ae\u03c3\u03b5\u03b9. \u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5;", "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 ZHA" }, + "instruct_unplug": { + "description": "\u0388\u03b3\u03b9\u03bd\u03b5 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u03c4\u03bf\u03c5 \u03c0\u03b1\u03bb\u03b9\u03bf\u03cd \u03c3\u03b1\u03c2 \u03c0\u03bf\u03bc\u03c0\u03bf\u03b4\u03ad\u03ba\u03c4\u03b7. \u0395\u03ac\u03bd \u03c4\u03bf \u03c5\u03bb\u03b9\u03ba\u03cc \u03b4\u03b5\u03bd \u03c7\u03c1\u03b5\u03b9\u03ac\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03c4\u03ce\u03c1\u03b1 \u03bd\u03b1 \u03c4\u03bf \u03b1\u03c0\u03bf\u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5.", + "title": "\u0391\u03c0\u03bf\u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b1\u03bb\u03b9\u03cc \u03c3\u03b1\u03c2 \u03c0\u03bf\u03bc\u03c0\u03bf\u03b4\u03ad\u03ba\u03c4\u03b7" + }, + "intent_migrate": { + "description": "\u039f \u03c0\u03b1\u03bb\u03b9\u03cc\u03c2 \u03c3\u03b1\u03c2 \u03c0\u03bf\u03bc\u03c0\u03bf\u03b4\u03ad\u03ba\u03c4\u03b7 \u03b8\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03b5\u03c1\u03b8\u03b5\u03af \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 \u03b5\u03c1\u03b3\u03bf\u03c3\u03c4\u03b1\u03c3\u03b9\u03b1\u03ba\u03ad\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2. \u0395\u03ac\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03c3\u03c5\u03bd\u03b4\u03c5\u03b1\u03c3\u03bc\u03ad\u03bd\u03bf \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03b1 Z-Wave \u03ba\u03b1\u03b9 Zigbee \u03cc\u03c0\u03c9\u03c2 \u03c4\u03bf HUSBZB-1, \u03b1\u03c5\u03c4\u03cc \u03b8\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03b5\u03b9 \u03bc\u03cc\u03bd\u03bf \u03c4\u03bf \u03c4\u03bc\u03ae\u03bc\u03b1 Zigbee.\n\n\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5;", + "title": "\u039c\u03b5\u03c4\u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c3\u03b5 \u03bd\u03ad\u03bf \u03c0\u03bf\u03bc\u03c0\u03bf\u03b4\u03ad\u03ba\u03c4\u03b7" + }, "manual_pick_radio_type": { "data": { "radio_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03ba\u03b5\u03c1\u03b1\u03af\u03b1\u03c2" @@ -235,6 +243,14 @@ "description": "\u03a4\u03bf \u03b5\u03c6\u03b5\u03b4\u03c1\u03b9\u03ba\u03cc \u03c3\u03b1\u03c2 \u03b1\u03bd\u03c4\u03af\u03b3\u03c1\u03b1\u03c6\u03bf \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IEEE \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03c3\u03cd\u03c1\u03bc\u03b1\u03c4\u03cc \u03c3\u03b1\u03c2. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03b9 \u03c3\u03c9\u03c3\u03c4\u03ac \u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03b9 \u03ba\u03b1\u03b9 \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IEEE \u03c4\u03bf\u03c5 \u03c1\u03b1\u03b4\u03b9\u03bf\u03c6\u03ce\u03bd\u03bf\u03c5 \u03c3\u03b1\u03c2.\n\n\u0391\u03c5\u03c4\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03bc\u03cc\u03bd\u03b9\u03bc\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1.", "title": "\u0391\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IEEE Radio" }, + "prompt_migrate_or_reconfigure": { + "description": "\u03a0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03bc\u03b5\u03c4\u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c3\u03b5 \u03bd\u03ad\u03bf \u03c0\u03bf\u03bc\u03c0\u03bf\u03b4\u03ad\u03ba\u03c4\u03b7 \u03ae \u03c1\u03c5\u03b8\u03bc\u03af\u03b6\u03b5\u03c4\u03b5 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5 \u03c4\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03bf\u03c2 \u03c0\u03bf\u03bc\u03c0\u03bf\u03b4\u03ad\u03ba\u03c4\u03b7;", + "menu_options": { + "intent_migrate": "\u039c\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac \u03c3\u03b5 \u03bd\u03ad\u03bf \u03c0\u03bf\u03bc\u03c0\u03bf\u03b4\u03ad\u03ba\u03c4\u03b7", + "intent_reconfigure": "\u0395\u03c0\u03b1\u03bd\u03b1\u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c4\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03bf\u03c2 \u03c0\u03bf\u03bc\u03c0\u03bf\u03b4\u03ad\u03ba\u03c4\u03b7" + }, + "title": "\u039c\u03b5\u03c4\u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03ae \u03b5\u03c0\u03b1\u03bd\u03b1\u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "\u0391\u03bd\u03b5\u03b2\u03ac\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf" diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index bb62ccca64a..f624f4d5499 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -64,12 +64,35 @@ "description": "Your backup has a different IEEE address than your radio. For your network to function properly, the IEEE address of your radio should also be changed.\n\nThis is a permanent operation.", "title": "Overwrite Radio IEEE Address" }, + "pick_radio": { + "data": { + "radio_type": "Radio Type" + }, + "description": "Pick a type of your Zigbee radio", + "title": "Radio Type" + }, + "port_config": { + "data": { + "baudrate": "port speed", + "flow_control": "data flow control", + "path": "Serial device path" + }, + "description": "Enter port specific settings", + "title": "Settings" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Upload a file" }, "description": "Restore your network settings from an uploaded backup JSON file. You can download one from a different ZHA installation from **Network Settings**, or use a Zigbee2MQTT `coordinator_backup.json` file.", "title": "Upload a Manual Backup" + }, + "user": { + "data": { + "path": "Serial Device Path" + }, + "description": "Select serial port for Zigbee radio", + "title": "ZHA" } } }, diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 080814991db..2919302a1ac 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -212,6 +212,14 @@ "description": "ZHA se detendr\u00e1. \u00bfDeseas continuar?", "title": "Reconfigurar ZHA" }, + "instruct_unplug": { + "description": "Tu antigua radio ha sido reiniciada. Si ya no necesitas el hardware, puedes desconectarlo ahora.", + "title": "Desconecta tu antigua radio" + }, + "intent_migrate": { + "description": "Tu antigua radio se restablecer\u00e1 de f\u00e1brica. Si est\u00e1s utilizando un adaptador combinado de Z-Wave y Zigbee como el HUSBZB-1, esto solo restablecer\u00e1 la parte de Zigbee. \n\n\u00bfDeseas continuar?", + "title": "Migrar a una nueva radio" + }, "manual_pick_radio_type": { "data": { "radio_type": "Tipo de Radio" @@ -235,6 +243,14 @@ "description": "Tu copia de seguridad tiene una direcci\u00f3n IEEE diferente a la de tu radio. Para que tu red funcione correctamente, tambi\u00e9n debes cambiar la direcci\u00f3n IEEE de tu radio. \n\nEsta es una operaci\u00f3n permanente.", "title": "Sobrescribir la direcci\u00f3n IEEE de la radio" }, + "prompt_migrate_or_reconfigure": { + "description": "\u00bfEst\u00e1s migrando a una nueva radio o volviendo a configurar la radio actual?", + "menu_options": { + "intent_migrate": "Migrar a una nueva radio", + "intent_reconfigure": "Volver a configurar la radio actual" + }, + "title": "Migrar o volver a configurar" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Subir un archivo" diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index fee8080d8eb..a8e58f3ecd9 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -210,6 +210,14 @@ "description": "\u0420\u0430\u0431\u043e\u0442\u0430 ZHA \u0431\u0443\u0434\u0435\u0442 \u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0430. \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c?", "title": "\u041f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 ZHA" }, + "instruct_unplug": { + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0412\u0430\u0448\u0435\u0433\u043e \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f \u0431\u044b\u043b\u0438 \u0441\u0431\u0440\u043e\u0448\u0435\u043d\u044b. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e \u043e\u0431\u043e\u0440\u0443\u0434\u043e\u0432\u0430\u043d\u0438\u0435 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043d\u0443\u0436\u043d\u043e, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0435\u0433\u043e.", + "title": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0441\u0442\u0430\u0440\u043e\u0433\u043e \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f" + }, + "intent_migrate": { + "description": "\u0412\u0430\u0448 \u0441\u0442\u0430\u0440\u044b\u0439 \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044c \u0431\u0443\u0434\u0435\u0442 \u0441\u0431\u0440\u043e\u0448\u0435\u043d \u043a \u0437\u0430\u0432\u043e\u0434\u0441\u043a\u0438\u043c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c. \u0415\u0441\u043b\u0438 \u0412\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 \u043a\u043e\u043c\u0431\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0430\u0434\u0430\u043f\u0442\u0435\u0440 Z-Wave \u0438 Zigbee (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 HUSBZB-1), \u0431\u0443\u0434\u0443\u0442 \u0441\u0431\u0440\u043e\u0448\u0435\u043d\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Zigbee.\n\n\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c?", + "title": "\u041f\u0435\u0440\u0435\u0445\u043e\u0434 \u043d\u0430 \u0434\u0440\u0443\u0433\u043e\u0439 \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044c" + }, "manual_pick_radio_type": { "data": { "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f" @@ -233,6 +241,14 @@ "description": "\u0412 \u0412\u0430\u0448\u0435\u0439 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0439 \u043a\u043e\u043f\u0438\u0438 IEEE-\u0430\u0434\u0440\u0435\u0441 \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u0441\u0435\u0439\u0447\u0430\u0441. \u0427\u0442\u043e\u0431\u044b \u0412\u0430\u0448\u0430 \u0441\u0435\u0442\u044c \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043b\u0430 \u0434\u043e\u043b\u0436\u043d\u044b\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c, IEEE-\u0430\u0434\u0440\u0435\u0441 \u0412\u0430\u0448\u0435\u0433\u043e \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f \u0442\u0430\u043a\u0436\u0435 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0438\u0437\u043c\u0435\u043d\u0435\u043d. \n\n\u042d\u0442\u043e \u043d\u0435\u043e\u0431\u0440\u0430\u0442\u0438\u043c\u0430\u044f \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u044f.", "title": "\u041f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u044c IEEE-\u0430\u0434\u0440\u0435\u0441\u0430 \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044f" }, + "prompt_migrate_or_reconfigure": { + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044c \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043d\u0430 \u0434\u0440\u0443\u0433\u043e\u0439?", + "menu_options": { + "intent_migrate": "\u041f\u0435\u0440\u0435\u0439\u0442\u0438 \u043d\u0430 \u0434\u0440\u0443\u0433\u043e\u0439 \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044c", + "intent_reconfigure": "\u041f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0440\u0430\u0434\u0438\u043e\u043c\u043e\u0434\u0443\u043b\u044c" + }, + "title": "\u041f\u0435\u0440\u0435\u0445\u043e\u0434 \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b" From 22d6ce967d8ddf2ef0a6edb92b3341233498463a Mon Sep 17 00:00:00 2001 From: Jeef Date: Thu, 6 Oct 2022 19:09:38 -0600 Subject: [PATCH 214/985] Add Flume binary sensors (#77327) Co-authored-by: J. Nick Koston --- .coveragerc | 2 + .../components/flume/binary_sensor.py | 159 ++++++++++++++++++ homeassistant/components/flume/const.py | 16 +- homeassistant/components/flume/coordinator.py | 93 +++++++++- homeassistant/components/flume/entity.py | 21 ++- homeassistant/components/flume/sensor.py | 29 ++-- homeassistant/components/flume/util.py | 18 ++ 7 files changed, 311 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/flume/binary_sensor.py create mode 100644 homeassistant/components/flume/util.py diff --git a/.coveragerc b/.coveragerc index dd01fa11e84..8ee3e7a7cde 100644 --- a/.coveragerc +++ b/.coveragerc @@ -407,9 +407,11 @@ omit = homeassistant/components/flick_electric/sensor.py homeassistant/components/flock/notify.py homeassistant/components/flume/__init__.py + homeassistant/components/flume/binary_sensor.py homeassistant/components/flume/coordinator.py homeassistant/components/flume/entity.py homeassistant/components/flume/sensor.py + homeassistant/components/flume/util.py homeassistant/components/folder/sensor.py homeassistant/components/folder_watcher/* homeassistant/components/foobot/sensor.py diff --git a/homeassistant/components/flume/binary_sensor.py b/homeassistant/components/flume/binary_sensor.py new file mode 100644 index 00000000000..235d7c3edd6 --- /dev/null +++ b/homeassistant/components/flume/binary_sensor.py @@ -0,0 +1,159 @@ +"""Flume binary sensors.""" +from __future__ import annotations + +from dataclasses import dataclass + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ( + DOMAIN, + FLUME_AUTH, + FLUME_DEVICES, + FLUME_TYPE_BRIDGE, + FLUME_TYPE_SENSOR, + KEY_DEVICE_ID, + KEY_DEVICE_LOCATION, + KEY_DEVICE_LOCATION_NAME, + KEY_DEVICE_TYPE, + NOTIFICATION_HIGH_FLOW, + NOTIFICATION_LEAK_DETECTED, +) +from .coordinator import ( + FlumeDeviceConnectionUpdateCoordinator, + FlumeNotificationDataUpdateCoordinator, +) +from .entity import FlumeEntity +from .util import get_valid_flume_devices + + +@dataclass +class FlumeBinarySensorRequiredKeysMixin: + """Mixin for required keys.""" + + event_rule: str + + +@dataclass +class FlumeBinarySensorEntityDescription( + BinarySensorEntityDescription, FlumeBinarySensorRequiredKeysMixin +): + """Describes a binary sensor entity.""" + + +FLUME_BINARY_NOTIFICATION_SENSORS: tuple[FlumeBinarySensorEntityDescription, ...] = ( + FlumeBinarySensorEntityDescription( + key="leak", + name="Leak detected", + entity_category=EntityCategory.DIAGNOSTIC, + event_rule=NOTIFICATION_LEAK_DETECTED, + icon="mdi:pipe-leak", + ), + FlumeBinarySensorEntityDescription( + key="flow", + name="High flow", + entity_category=EntityCategory.DIAGNOSTIC, + event_rule=NOTIFICATION_HIGH_FLOW, + icon="mdi:waves", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a Flume binary sensor..""" + flume_domain_data = hass.data[DOMAIN][config_entry.entry_id] + flume_auth = flume_domain_data[FLUME_AUTH] + flume_devices = flume_domain_data[FLUME_DEVICES] + + flume_entity_list: list[ + FlumeNotificationBinarySensor | FlumeConnectionBinarySensor + ] = [] + + connection_coordinator = FlumeDeviceConnectionUpdateCoordinator( + hass=hass, flume_devices=flume_devices + ) + notification_coordinator = FlumeNotificationDataUpdateCoordinator( + hass=hass, auth=flume_auth + ) + flume_devices = get_valid_flume_devices(flume_devices) + for device in flume_devices: + device_id = device[KEY_DEVICE_ID] + device_location_name = device[KEY_DEVICE_LOCATION][KEY_DEVICE_LOCATION_NAME] + + connection_sensor = FlumeConnectionBinarySensor( + coordinator=connection_coordinator, + description=BinarySensorEntityDescription( + name="Connected", + key="connected", + ), + device_id=device_id, + location_name=device_location_name, + is_bridge=(device[KEY_DEVICE_TYPE] is FLUME_TYPE_BRIDGE), + ) + + flume_entity_list.append(connection_sensor) + + if device[KEY_DEVICE_TYPE] != FLUME_TYPE_SENSOR: + continue + + # Build notification sensors + flume_entity_list.extend( + [ + FlumeNotificationBinarySensor( + coordinator=notification_coordinator, + description=description, + device_id=device_id, + location_name=device_location_name, + ) + for description in FLUME_BINARY_NOTIFICATION_SENSORS + ] + ) + + if flume_entity_list: + async_add_entities(flume_entity_list) + + +class FlumeNotificationBinarySensor(FlumeEntity, BinarySensorEntity): + """Binary sensor class.""" + + entity_description: FlumeBinarySensorEntityDescription + coordinator: FlumeNotificationDataUpdateCoordinator + + @property + def is_on(self) -> bool: + """Return on state.""" + return bool( + ( + notifications := self.coordinator.active_notifications_by_device.get( + self.device_id + ) + ) + and self.entity_description.event_rule in notifications + ) + + +class FlumeConnectionBinarySensor(FlumeEntity, BinarySensorEntity): + """Binary Sensor class for WIFI Connection status.""" + + entity_description: FlumeBinarySensorEntityDescription + coordinator: FlumeDeviceConnectionUpdateCoordinator + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY + + @property + def is_on(self) -> bool: + """Return connection status.""" + return bool( + (connected := self.coordinator.connected) and connected[self.device_id] + ) diff --git a/homeassistant/components/flume/const.py b/homeassistant/components/flume/const.py index 656c2eb1018..2d53db4c486 100644 --- a/homeassistant/components/flume/const.py +++ b/homeassistant/components/flume/const.py @@ -8,17 +8,24 @@ from homeassistant.const import Platform DOMAIN = "flume" -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.SENSOR, +] DEFAULT_NAME = "Flume Sensor" +# Flume API limits individual endpoints to 120 queries per hour NOTIFICATION_SCAN_INTERVAL = timedelta(minutes=1) DEVICE_SCAN_INTERVAL = timedelta(minutes=1) +DEVICE_CONNECTION_SCAN_INTERVAL = timedelta(minutes=1) _LOGGER = logging.getLogger(__package__) +FLUME_TYPE_BRIDGE = 1 FLUME_TYPE_SENSOR = 2 + FLUME_AUTH = "flume_auth" FLUME_HTTP_SESSION = "http_session" FLUME_DEVICES = "devices" @@ -33,3 +40,10 @@ KEY_DEVICE_ID = "id" KEY_DEVICE_LOCATION = "location" KEY_DEVICE_LOCATION_NAME = "name" KEY_DEVICE_LOCATION_TIMEZONE = "tz" + + +NOTIFICATION_HIGH_FLOW = "High Flow Alert" +NOTIFICATION_BRIDGE_DISCONNECT = "Bridge Disconnection" +BRIDGE_NOTIFICATION_KEY = "connected" +BRIDGE_NOTIFICATION_RULE = "Bridge Disconnection" +NOTIFICATION_LEAK_DETECTED = "Flume Smart Leak Alert" diff --git a/homeassistant/components/flume/coordinator.py b/homeassistant/components/flume/coordinator.py index 9e23141cd5e..70a99f56968 100644 --- a/homeassistant/components/flume/coordinator.py +++ b/homeassistant/components/flume/coordinator.py @@ -1,10 +1,21 @@ """The IntelliFire integration.""" from __future__ import annotations +from typing import Any + +import pyflume +from pyflume import FlumeDeviceList + from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import _LOGGER, DEVICE_SCAN_INTERVAL, DOMAIN +from .const import ( + _LOGGER, + DEVICE_CONNECTION_SCAN_INTERVAL, + DEVICE_SCAN_INTERVAL, + DOMAIN, + NOTIFICATION_SCAN_INTERVAL, +) class FlumeDeviceDataUpdateCoordinator(DataUpdateCoordinator[None]): @@ -23,13 +34,89 @@ class FlumeDeviceDataUpdateCoordinator(DataUpdateCoordinator[None]): async def _async_update_data(self) -> None: """Get the latest data from the Flume.""" - _LOGGER.debug("Updating Flume data") try: await self.hass.async_add_executor_job(self.flume_device.update_force) except Exception as ex: raise UpdateFailed(f"Error communicating with flume API: {ex}") from ex _LOGGER.debug( - "Flume update details: values=%s query_payload=%s", + "Flume Device Data Update values=%s query_payload=%s", self.flume_device.values, self.flume_device.query_payload, ) + + +class FlumeDeviceConnectionUpdateCoordinator(DataUpdateCoordinator[None]): + """Date update coordinator to read connected status from Devices endpoint.""" + + def __init__(self, hass: HomeAssistant, flume_devices: FlumeDeviceList) -> None: + """Initialize the Coordinator.""" + super().__init__( + hass, + name=DOMAIN, + logger=_LOGGER, + update_interval=DEVICE_CONNECTION_SCAN_INTERVAL, + ) + + self.flume_devices = flume_devices + self.connected: dict[str, bool] = {} + + def _update_connectivity(self) -> None: + """Update device connectivity..""" + self.connected = { + device["id"]: device["connected"] + for device in self.flume_devices.get_devices() + } + _LOGGER.debug("Connectivity %s", self.connected) + + async def _async_update_data(self) -> None: + """Update the device list.""" + try: + await self.hass.async_add_executor_job(self._update_connectivity) + except Exception as ex: + raise UpdateFailed(f"Error communicating with flume API: {ex}") from ex + + +class FlumeNotificationDataUpdateCoordinator(DataUpdateCoordinator[None]): + """Data update coordinator for flume notifications.""" + + def __init__(self, hass: HomeAssistant, auth) -> None: + """Initialize the Coordinator.""" + super().__init__( + hass, + name=DOMAIN, + logger=_LOGGER, + update_interval=NOTIFICATION_SCAN_INTERVAL, + ) + self.auth = auth + self.active_notifications_by_device: dict = {} + self.notifications: list[dict[str, Any]] + + def _update_lists(self): + """Query flume for notification list.""" + self.notifications: list[dict[str, Any]] = pyflume.FlumeNotificationList( + self.auth, read="true" + ).notification_list + _LOGGER.debug("Notifications %s", self.notifications) + + active_notifications_by_device: dict[str, set[str]] = {} + + for notification in self.notifications: + if ( + not notification.get("device_id") + or not notification.get("extra") + or "event_rule_name" not in notification["extra"] + ): + continue + device_id = notification["device_id"] + rule = notification["extra"]["event_rule_name"] + active_notifications_by_device.setdefault(device_id, set()).add(rule) + + self.active_notifications_by_device = active_notifications_by_device + + async def _async_update_data(self) -> None: + """Update data.""" + _LOGGER.debug("Updating Flume Notification") + try: + await self.hass.async_add_executor_job(self._update_lists) + except Exception as ex: + raise UpdateFailed(f"Error communicating with flume API: {ex}") from ex diff --git a/homeassistant/components/flume/entity.py b/homeassistant/components/flume/entity.py index b36ecd28cf8..4aeba6d2bc6 100644 --- a/homeassistant/components/flume/entity.py +++ b/homeassistant/components/flume/entity.py @@ -2,13 +2,15 @@ from __future__ import annotations from homeassistant.helpers.entity import DeviceInfo, EntityDescription -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import DOMAIN -from .coordinator import FlumeDeviceDataUpdateCoordinator -class FlumeEntity(CoordinatorEntity[FlumeDeviceDataUpdateCoordinator]): +class FlumeEntity(CoordinatorEntity[DataUpdateCoordinator]): """Base entity class.""" _attr_attribution = "Data provided by Flume API" @@ -16,20 +18,29 @@ class FlumeEntity(CoordinatorEntity[FlumeDeviceDataUpdateCoordinator]): def __init__( self, - coordinator: FlumeDeviceDataUpdateCoordinator, + coordinator: DataUpdateCoordinator, description: EntityDescription, device_id: str, + location_name: str, + is_bridge: bool = False, ) -> None: """Class initializer.""" super().__init__(coordinator) self.entity_description = description self.device_id = device_id + + if is_bridge: + name = "Flume Bridge" + else: + name = "Flume Sensor" + self._attr_unique_id = f"{description.key}_{device_id}" + self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, device_id)}, manufacturer="Flume, Inc.", model="Flume Smart Water Monitor", - name=f"Flume {device_id}", + name=f"{name} {location_name}", configuration_url="https://portal.flumewater.com", ) diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index 51d1b54bee8..09f65f7d891 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -22,11 +22,13 @@ from .const import ( FLUME_TYPE_SENSOR, KEY_DEVICE_ID, KEY_DEVICE_LOCATION, + KEY_DEVICE_LOCATION_NAME, KEY_DEVICE_LOCATION_TIMEZONE, KEY_DEVICE_TYPE, ) from .coordinator import FlumeDeviceDataUpdateCoordinator from .entity import FlumeEntity +from .util import get_valid_flume_devices FLUME_QUERIES_SENSOR: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( @@ -78,21 +80,20 @@ async def async_setup_entry( """Set up the Flume sensor.""" flume_domain_data = hass.data[DOMAIN][config_entry.entry_id] - + flume_devices = flume_domain_data[FLUME_DEVICES] flume_auth = flume_domain_data[FLUME_AUTH] http_session = flume_domain_data[FLUME_HTTP_SESSION] - flume_devices = flume_domain_data[FLUME_DEVICES] - + flume_devices = [ + device + for device in get_valid_flume_devices(flume_devices) + if device[KEY_DEVICE_TYPE] == FLUME_TYPE_SENSOR + ] flume_entity_list = [] - for device in flume_devices.device_list: - if ( - device[KEY_DEVICE_TYPE] != FLUME_TYPE_SENSOR - or KEY_DEVICE_LOCATION not in device - ): - continue + for device in flume_devices: device_id = device[KEY_DEVICE_ID] device_timezone = device[KEY_DEVICE_LOCATION][KEY_DEVICE_LOCATION_TIMEZONE] + device_location_name = device[KEY_DEVICE_LOCATION][KEY_DEVICE_LOCATION_NAME] flume_device = FlumeData( flume_auth, @@ -113,6 +114,7 @@ async def async_setup_entry( coordinator=coordinator, description=description, device_id=device_id, + location_name=device_location_name, ) for description in FLUME_QUERIES_SENSOR ] @@ -127,15 +129,6 @@ class FlumeSensor(FlumeEntity, SensorEntity): coordinator: FlumeDeviceDataUpdateCoordinator - def __init__( - self, - coordinator: FlumeDeviceDataUpdateCoordinator, - device_id: str, - description: SensorEntityDescription, - ) -> None: - """Inlitializer function with type hints.""" - super().__init__(coordinator, description, device_id) - @property def native_value(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/flume/util.py b/homeassistant/components/flume/util.py new file mode 100644 index 00000000000..b943124b877 --- /dev/null +++ b/homeassistant/components/flume/util.py @@ -0,0 +1,18 @@ +"""Utilities for Flume.""" + +from __future__ import annotations + +from typing import Any + +from pyflume import FlumeDeviceList + +from .const import KEY_DEVICE_LOCATION, KEY_DEVICE_LOCATION_NAME + + +def get_valid_flume_devices(flume_devices: FlumeDeviceList) -> list[dict[str, Any]]: + """Return a list of Flume devices that have a valid location.""" + return [ + device + for device in flume_devices.device_list + if KEY_DEVICE_LOCATION_NAME in device[KEY_DEVICE_LOCATION] + ] From 07d4ac42d429e44f7618e1e7c6462613ef8d3f09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Oct 2022 16:40:40 -1000 Subject: [PATCH 215/985] Fix Bluetooth failover when esphome device unexpectedly disconnects (#79769) --- .../components/esphome/bluetooth/__init__.py | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/__init__.py b/homeassistant/components/esphome/bluetooth/__init__.py index 4f3235676a4..b4d5fdbd04d 100644 --- a/homeassistant/components/esphome/bluetooth/__init__.py +++ b/homeassistant/components/esphome/bluetooth/__init__.py @@ -1,6 +1,7 @@ """Bluetooth support for esphome.""" from __future__ import annotations +from collections.abc import Callable import logging from aioesphomeapi import APIClient @@ -11,14 +12,8 @@ from homeassistant.components.bluetooth import ( async_register_scanner, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import ( - CALLBACK_TYPE, - HomeAssistant, - async_get_hass, - callback as hass_callback, -) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback -from ..domain_data import DomainData from ..entry_data import RuntimeEntryData from .client import ESPHomeClient from .scanner import ESPHomeScanner @@ -27,18 +22,23 @@ _LOGGER = logging.getLogger(__name__) @hass_callback -def async_can_connect(source: str) -> bool: - """Check if a given source can make another connection.""" - domain_data = DomainData.get(async_get_hass()) - entry = domain_data.get_by_unique_id(source) - entry_data = domain_data.get_entry_data(entry) - _LOGGER.debug( - "Checking if %s can connect, available=%s, ble_connections_free=%s", - source, - entry_data.available, - entry_data.ble_connections_free, - ) - return bool(entry_data.available and entry_data.ble_connections_free) +def _async_can_connect_factory( + entry_data: RuntimeEntryData, source: str +) -> Callable[[], bool]: + """Create a can_connect function for a specific RuntimeEntryData instance.""" + + @hass_callback + def _async_can_connect() -> bool: + """Check if a given source can make another connection.""" + _LOGGER.debug( + "Checking if %s can connect, available=%s, ble_connections_free=%s", + source, + entry_data.available, + entry_data.ble_connections_free, + ) + return bool(entry_data.available and entry_data.ble_connections_free) + + return _async_can_connect async def async_connect_scanner( @@ -63,7 +63,7 @@ async def async_connect_scanner( connector = HaBluetoothConnector( client=ESPHomeClient, source=source, - can_connect=lambda: async_can_connect(source), + can_connect=_async_can_connect_factory(entry_data, source), ) scanner = ESPHomeScanner(hass, source, new_info_callback, connector, connectable) unload_callbacks = [ From 5694a4bfc8d90ee5a169dc3d5a95e5b280068f48 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 6 Oct 2022 23:29:34 -0500 Subject: [PATCH 216/985] Fix state updating for crossfade switch on Sonos (#79776) --- homeassistant/components/sonos/speaker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 98984eedc03..38d37e7cfd4 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -489,7 +489,10 @@ class SonosSpeaker: return if crossfade := event.variables.get("current_crossfade_mode"): - self.cross_fade = bool(int(crossfade)) + crossfade = bool(int(crossfade)) + if self.cross_fade != crossfade: + self.cross_fade = crossfade + self.async_write_entity_states() # Missing transport_state indicates a transient error if (new_status := event.variables.get("transport_state")) is None: From 633ffad4438fd415e4a8272c9797793b98b6c5a8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 7 Oct 2022 08:07:59 +0200 Subject: [PATCH 217/985] Add diagnostics to LaMetric (#79757) * Add diagnostics to LaMetric * Add return value typing Co-authored-by: Martin Hjelmare --- .../components/lametric/diagnostics.py | 29 ++++++++++ tests/components/lametric/test_diagnostics.py | 57 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 homeassistant/components/lametric/diagnostics.py create mode 100644 tests/components/lametric/test_diagnostics.py diff --git a/homeassistant/components/lametric/diagnostics.py b/homeassistant/components/lametric/diagnostics.py new file mode 100644 index 00000000000..256f5f06e91 --- /dev/null +++ b/homeassistant/components/lametric/diagnostics.py @@ -0,0 +1,29 @@ +"""Diagnostics support for LaMetric.""" +from __future__ import annotations + +import json +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import LaMetricDataUpdateCoordinator + +TO_REDACT = { + "device_id", + "name", + "serial_number", + "ssid", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + # Round-trip via JSON to trigger serialization + data = json.loads(coordinator.data.json()) + return async_redact_data(data, TO_REDACT) diff --git a/tests/components/lametric/test_diagnostics.py b/tests/components/lametric/test_diagnostics.py new file mode 100644 index 00000000000..27c031d19e4 --- /dev/null +++ b/tests/components/lametric/test_diagnostics.py @@ -0,0 +1,57 @@ +"""Tests for the diagnostics data provided by the LaMetric integration.""" +from aiohttp import ClientSession + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, + init_integration: MockConfigEntry, +) -> None: + """Test diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, init_integration + ) == { + "device_id": REDACTED, + "name": REDACTED, + "serial_number": REDACTED, + "os_version": "2.2.2", + "mode": "auto", + "model": "LM 37X8", + "audio": { + "volume": 100, + "volume_range": {"range_min": 0, "range_max": 100}, + "volume_limit": {"range_min": 0, "range_max": 100}, + }, + "bluetooth": { + "available": True, + "name": REDACTED, + "active": False, + "discoverable": True, + "pairable": True, + "address": "AA:BB:CC:DD:EE:FF", + }, + "display": { + "brightness": 100, + "brightness_mode": "auto", + "width": 37, + "height": 8, + "display_type": "mixed", + }, + "wifi": { + "active": True, + "mac": "AA:BB:CC:DD:EE:FF", + "available": True, + "encryption": "WPA", + "ssid": REDACTED, + "ip": "127.0.0.1", + "mode": "dhcp", + "netmask": "255.255.255.0", + "rssi": 21, + }, + } From 90d39a414c2f42cab0d53d3288d8486282683a1a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 7 Oct 2022 08:11:10 +0200 Subject: [PATCH 218/985] Add LaMetric number tests (#79748) --- .coveragerc | 1 - tests/components/lametric/test_number.py | 73 ++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 tests/components/lametric/test_number.py diff --git a/.coveragerc b/.coveragerc index 8ee3e7a7cde..cdbfd57024b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -662,7 +662,6 @@ omit = homeassistant/components/kwb/sensor.py homeassistant/components/lacrosse/sensor.py homeassistant/components/lametric/notify.py - homeassistant/components/lametric/number.py homeassistant/components/lannouncer/notify.py homeassistant/components/lastfm/sensor.py homeassistant/components/launch_library/__init__.py diff --git a/tests/components/lametric/test_number.py b/tests/components/lametric/test_number.py new file mode 100644 index 00000000000..92d4e262bdc --- /dev/null +++ b/tests/components/lametric/test_number.py @@ -0,0 +1,73 @@ +"""Tests for the LaMetric number platform.""" +from unittest.mock import MagicMock + +from homeassistant.components.lametric.const import DOMAIN +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE +from homeassistant.components.number.const import ( + ATTR_MAX, + ATTR_MIN, + ATTR_STEP, + ATTR_VALUE, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_ICON, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory + +from tests.common import MockConfigEntry + + +async def test_volume( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_lametric: MagicMock, +) -> None: + """Test the LaMetric volume controls.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + state = hass.states.get("number.frenck_s_lametric_volume") + assert state + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's LaMetric Volume" + assert state.attributes.get(ATTR_ICON) == "mdi:volume-high" + assert state.attributes.get(ATTR_MAX) == 100 + assert state.attributes.get(ATTR_MIN) == 0 + assert state.attributes.get(ATTR_STEP) == 1 + assert state.state == "100" + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry.device_id + assert entry.entity_category is EntityCategory.CONFIG + assert entry.unique_id == "SA110405124500W00BS9-volume" + + device = device_registry.async_get(entry.device_id) + assert device + assert device.configuration_url is None + assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")} + assert device.entry_type is None + assert device.hw_version is None + assert device.identifiers == {(DOMAIN, "SA110405124500W00BS9")} + assert device.manufacturer == "LaMetric Inc." + assert device.name == "Frenck's LaMetric" + assert device.sw_version == "2.2.2" + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.frenck_s_lametric_volume", + ATTR_VALUE: 42, + }, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(mock_lametric.audio.mock_calls) == 1 + mock_lametric.audio.assert_called_once_with(volume=42) From 9a81b658153d3d9cd5e3a055c3f0ddb537144731 Mon Sep 17 00:00:00 2001 From: taiyeoguns Date: Fri, 7 Oct 2022 07:21:18 +0100 Subject: [PATCH 219/985] Convert kira tests to pytest (#79747) --- tests/components/kira/test_remote.py | 48 +++++++++-------------- tests/components/kira/test_sensor.py | 58 ++++++++++++---------------- 2 files changed, 44 insertions(+), 62 deletions(-) diff --git a/tests/components/kira/test_remote.py b/tests/components/kira/test_remote.py index e91cbaca891..03268200077 100644 --- a/tests/components/kira/test_remote.py +++ b/tests/components/kira/test_remote.py @@ -1,48 +1,38 @@ """The tests for Kira sensor platform.""" -import unittest from unittest.mock import MagicMock from homeassistant.components.kira import remote as kira -from tests.common import get_test_home_assistant - SERVICE_SEND_COMMAND = "send_command" TEST_CONFIG = {kira.DOMAIN: {"devices": [{"host": "127.0.0.1", "port": 17324}]}} DISCOVERY_INFO = {"name": "kira", "device": "kira"} +DEVICES = [] -class TestKiraSensor(unittest.TestCase): - """Tests the Kira Sensor platform.""" - # pylint: disable=invalid-name - DEVICES = [] +def add_entities(devices): + """Mock add devices.""" + for device in devices: + DEVICES.append(device) - def add_entities(self, devices): - """Mock add devices.""" - for device in devices: - self.DEVICES.append(device) - def setUp(self): - """Initialize values for this testcase class.""" - self.hass = get_test_home_assistant() - self.mock_kira = MagicMock() - self.hass.data[kira.DOMAIN] = {kira.CONF_REMOTE: {}} - self.hass.data[kira.DOMAIN][kira.CONF_REMOTE]["kira"] = self.mock_kira - self.addCleanup(self.hass.stop) +def test_service_call(hass): + """Test Kira's ability to send commands.""" + mock_kira = MagicMock() + hass.data[kira.DOMAIN] = {kira.CONF_REMOTE: {}} + hass.data[kira.DOMAIN][kira.CONF_REMOTE]["kira"] = mock_kira - def test_service_call(self): - """Test Kira's ability to send commands.""" - kira.setup_platform(self.hass, TEST_CONFIG, self.add_entities, DISCOVERY_INFO) - assert len(self.DEVICES) == 1 - remote = self.DEVICES[0] + kira.setup_platform(hass, TEST_CONFIG, add_entities, DISCOVERY_INFO) + assert len(DEVICES) == 1 + remote = DEVICES[0] - assert remote.name == "kira" + assert remote.name == "kira" - command = ["FAKE_COMMAND"] - device = "FAKE_DEVICE" - commandTuple = (command[0], device) - remote.send_command(device=device, command=command) + command = ["FAKE_COMMAND"] + device = "FAKE_DEVICE" + commandTuple = (command[0], device) + remote.send_command(device=device, command=command) - self.mock_kira.sendCode.assert_called_with(commandTuple) + mock_kira.sendCode.assert_called_with(commandTuple) diff --git a/tests/components/kira/test_sensor.py b/tests/components/kira/test_sensor.py index b835a25ae90..f0c771fbda0 100644 --- a/tests/components/kira/test_sensor.py +++ b/tests/components/kira/test_sensor.py @@ -1,50 +1,42 @@ """The tests for Kira sensor platform.""" -import unittest -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from homeassistant.components.kira import sensor as kira -from tests.common import get_test_home_assistant - TEST_CONFIG = {kira.DOMAIN: {"sensors": [{"host": "127.0.0.1", "port": 17324}]}} DISCOVERY_INFO = {"name": "kira", "device": "kira"} +DEVICES = [] -class TestKiraSensor(unittest.TestCase): - """Tests the Kira Sensor platform.""" - # pylint: disable=invalid-name - DEVICES = [] +def add_entities(devices): + """Mock add devices.""" + for device in devices: + DEVICES.append(device) - def add_entities(self, devices): - """Mock add devices.""" - for device in devices: - self.DEVICES.append(device) - def setUp(self): - """Initialize values for this testcase class.""" - self.hass = get_test_home_assistant() - mock_kira = MagicMock() - self.hass.data[kira.DOMAIN] = {kira.CONF_SENSOR: {}} - self.hass.data[kira.DOMAIN][kira.CONF_SENSOR]["kira"] = mock_kira - self.addCleanup(self.hass.stop) +@patch("homeassistant.components.kira.sensor.KiraReceiver.schedule_update_ha_state") +def test_kira_sensor_callback(mock_schedule_update_ha_state, hass): + """Ensure Kira sensor properly updates its attributes from callback.""" + mock_kira = MagicMock() + hass.data[kira.DOMAIN] = {kira.CONF_SENSOR: {}} + hass.data[kira.DOMAIN][kira.CONF_SENSOR]["kira"] = mock_kira - # pylint: disable=protected-access - def test_kira_sensor_callback(self): - """Ensure Kira sensor properly updates its attributes from callback.""" - kira.setup_platform(self.hass, TEST_CONFIG, self.add_entities, DISCOVERY_INFO) - assert len(self.DEVICES) == 1 - sensor = self.DEVICES[0] + kira.setup_platform(hass, TEST_CONFIG, add_entities, DISCOVERY_INFO) + assert len(DEVICES) == 1 + sensor = DEVICES[0] - assert sensor.name == "kira" + assert sensor.name == "kira" - sensor.hass = self.hass + sensor.hass = hass - codeName = "FAKE_CODE" - deviceName = "FAKE_DEVICE" - codeTuple = (codeName, deviceName) - sensor._update_callback(codeTuple) + codeName = "FAKE_CODE" + deviceName = "FAKE_DEVICE" + codeTuple = (codeName, deviceName) + sensor._update_callback(codeTuple) - assert sensor.state == codeName - assert sensor.extra_state_attributes == {kira.CONF_DEVICE: deviceName} + mock_schedule_update_ha_state.assert_called + + assert sensor.state == codeName + assert sensor.extra_state_attributes == {kira.CONF_DEVICE: deviceName} From aee82e2b3babc8e22986b390d841a6072235496f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 7 Oct 2022 10:12:19 +0200 Subject: [PATCH 220/985] De-duplicate MQTT config_flow code (#79369) * De-duplicate config_flow code * De duplicate code birth and will --- homeassistant/components/mqtt/client.py | 3 +- homeassistant/components/mqtt/config_flow.py | 256 +++++++++++-------- homeassistant/components/mqtt/const.py | 3 + tests/components/mqtt/test_config_flow.py | 54 +++- 4 files changed, 195 insertions(+), 121 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index bd734318938..b0ce53d75fd 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -54,6 +54,7 @@ from .const import ( CONF_TLS_INSECURE, CONF_WILL_MESSAGE, DEFAULT_ENCODING, + DEFAULT_PROTOCOL, DEFAULT_QOS, MQTT_CONNECTED, MQTT_DISCONNECTED, @@ -272,7 +273,7 @@ class MqttClientSetup: # should be able to optionally rely on MQTT. import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel - if config[CONF_PROTOCOL] == PROTOCOL_31: + if config.get(CONF_PROTOCOL, DEFAULT_PROTOCOL) == PROTOCOL_31: proto = mqtt.MQTTv31 else: proto = mqtt.MQTTv311 diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 5d21619c498..df7b6137549 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -2,7 +2,9 @@ from __future__ import annotations from collections import OrderedDict +from collections.abc import Callable import queue +from types import MappingProxyType from typing import Any import voluptuous as vol @@ -15,10 +17,9 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PAYLOAD, CONF_PORT, - CONF_PROTOCOL, CONF_USERNAME, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import ConfigType @@ -33,6 +34,7 @@ from .const import ( CONF_WILL_MESSAGE, DEFAULT_BIRTH, DEFAULT_DISCOVERY, + DEFAULT_PORT, DEFAULT_WILL, DOMAIN, ) @@ -56,9 +58,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return MQTTOptionsFlowHandler(config_entry) - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -66,35 +66,38 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_broker() async def async_step_broker( - self, user_input: dict[str, Any] | None = None + self, user_input: ConfigType | None = None ) -> FlowResult: """Confirm the setup.""" - errors = {} - - if user_input is not None: + yaml_config: ConfigType = get_mqtt_data(self.hass, True).config or {} + errors: dict[str, str] = {} + fields: OrderedDict[Any, Any] = OrderedDict() + validated_user_input: ConfigType = {} + if await async_get_broker_settings( + self.hass, + fields, + yaml_config, + None, + user_input, + validated_user_input, + errors, + ): + test_config: ConfigType = yaml_config.copy() + test_config.update(validated_user_input) can_connect = await self.hass.async_add_executor_job( try_connection, - get_mqtt_data(self.hass, True).config or {}, - user_input[CONF_BROKER], - user_input[CONF_PORT], - user_input.get(CONF_USERNAME), - user_input.get(CONF_PASSWORD), + test_config, ) if can_connect: - user_input[CONF_DISCOVERY] = DEFAULT_DISCOVERY + validated_user_input[CONF_DISCOVERY] = DEFAULT_DISCOVERY return self.async_create_entry( - title=user_input[CONF_BROKER], data=user_input + title=validated_user_input[CONF_BROKER], + data=validated_user_input, ) errors["base"] = "cannot_connect" - fields = OrderedDict() - fields[vol.Required(CONF_BROKER)] = str - fields[vol.Required(CONF_PORT, default=1883)] = vol.Coerce(int) - fields[vol.Optional(CONF_USERNAME)] = str - fields[vol.Optional(CONF_PASSWORD)] = str - return self.async_show_form( step_id="broker", data_schema=vol.Schema(fields), errors=errors ) @@ -111,26 +114,22 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm a Hass.io discovery.""" - errors = {} + errors: dict[str, str] = {} assert self._hassio_discovery if user_input is not None: - data = self._hassio_discovery + data: ConfigType = self._hassio_discovery.copy() + data[CONF_BROKER] = data.pop(CONF_HOST) can_connect = await self.hass.async_add_executor_job( try_connection, - get_mqtt_data(self.hass, True).config or {}, - data[CONF_HOST], - data[CONF_PORT], - data.get(CONF_USERNAME), - data.get(CONF_PASSWORD), - data.get(CONF_PROTOCOL), + data, ) if can_connect: return self.async_create_entry( title=data["addon"], data={ - CONF_BROKER: data[CONF_HOST], + CONF_BROKER: data[CONF_BROKER], CONF_PORT: data[CONF_PORT], CONF_USERNAME: data.get(CONF_USERNAME), CONF_PASSWORD: data.get(CONF_PASSWORD), @@ -164,46 +163,32 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the MQTT broker configuration.""" - mqtt_data = get_mqtt_data(self.hass, True) - yaml_config = mqtt_data.config or {} - errors = {} - current_config = self.config_entry.data - if user_input is not None: + errors: dict[str, str] = {} + yaml_config: ConfigType = get_mqtt_data(self.hass, True).config or {} + fields: OrderedDict[Any, Any] = OrderedDict() + validated_user_input: ConfigType = {} + if await async_get_broker_settings( + self.hass, + fields, + yaml_config, + self.config_entry.data, + user_input, + validated_user_input, + errors, + ): + test_config: ConfigType = yaml_config.copy() + test_config.update(validated_user_input) can_connect = await self.hass.async_add_executor_job( try_connection, - yaml_config, - user_input[CONF_BROKER], - user_input[CONF_PORT], - user_input.get(CONF_USERNAME), - user_input.get(CONF_PASSWORD), + test_config, ) if can_connect: - self.broker_config.update(user_input) + self.broker_config.update(validated_user_input) return await self.async_step_options() errors["base"] = "cannot_connect" - fields = OrderedDict() - 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_user}, - ) - ] = str - fields[ - vol.Optional( - CONF_PASSWORD, - description={"suggested_value": current_pass}, - ) - ] = str - return self.async_show_form( step_id="broker", data_schema=vol.Schema(fields), @@ -212,53 +197,61 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): ) async def async_step_options( - self, user_input: dict[str, Any] | None = None + self, user_input: ConfigType | None = None ) -> FlowResult: """Manage the MQTT options.""" - mqtt_data = get_mqtt_data(self.hass, True) errors = {} current_config = self.config_entry.data - yaml_config = mqtt_data.config or {} - options_config: dict[str, Any] = {} - if user_input is not None: - bad_birth = False - bad_will = False + yaml_config = get_mqtt_data(self.hass, True).config or {} + options_config: ConfigType = {} + bad_input: bool = False + def _birth_will(birt_or_will: str) -> dict: + """Return the user input for birth or will.""" + assert user_input + return { + ATTR_TOPIC: user_input[f"{birt_or_will}_topic"], + ATTR_PAYLOAD: user_input.get(f"{birt_or_will}_payload", ""), + ATTR_QOS: user_input[f"{birt_or_will}_qos"], + ATTR_RETAIN: user_input[f"{birt_or_will}_retain"], + } + + def _validate( + field: str, values: ConfigType, error_code: str, schema: Callable + ): + """Validate the user input.""" + nonlocal bad_input + try: + option_values = schema(values) + options_config[field] = option_values + except vol.Invalid: + errors["base"] = error_code + bad_input = True + + if user_input is not None: + # validate input + options_config[CONF_DISCOVERY] = user_input[CONF_DISCOVERY] if "birth_topic" in user_input: - birth_message = { - ATTR_TOPIC: user_input["birth_topic"], - ATTR_PAYLOAD: user_input.get("birth_payload", ""), - ATTR_QOS: user_input["birth_qos"], - ATTR_RETAIN: user_input["birth_retain"], - } - try: - birth_message = MQTT_WILL_BIRTH_SCHEMA(birth_message) - options_config[CONF_BIRTH_MESSAGE] = birth_message - except vol.Invalid: - errors["base"] = "bad_birth" - bad_birth = True + _validate( + CONF_BIRTH_MESSAGE, + _birth_will("birth"), + "bad_birth", + MQTT_WILL_BIRTH_SCHEMA, + ) if not user_input["birth_enable"]: options_config[CONF_BIRTH_MESSAGE] = {} if "will_topic" in user_input: - will_message = { - ATTR_TOPIC: user_input["will_topic"], - ATTR_PAYLOAD: user_input.get("will_payload", ""), - ATTR_QOS: user_input["will_qos"], - ATTR_RETAIN: user_input["will_retain"], - } - try: - will_message = MQTT_WILL_BIRTH_SCHEMA(will_message) - options_config[CONF_WILL_MESSAGE] = will_message - except vol.Invalid: - errors["base"] = "bad_will" - bad_will = True + _validate( + CONF_WILL_MESSAGE, + _birth_will("will"), + "bad_will", + MQTT_WILL_BIRTH_SCHEMA, + ) if not user_input["will_enable"]: options_config[CONF_WILL_MESSAGE] = {} - options_config[CONF_DISCOVERY] = user_input[CONF_DISCOVERY] - - if not bad_birth and not bad_will: + if not bad_input: updated_config = {} updated_config.update(self.broker_config) updated_config.update(options_config) @@ -285,6 +278,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): CONF_DISCOVERY, yaml_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY) ) + # build form fields: OrderedDict[vol.Marker, Any] = OrderedDict() fields[vol.Optional(CONF_DISCOVERY, default=discovery)] = bool @@ -338,28 +332,66 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): ) -def try_connection( +async def async_get_broker_settings( + hass: HomeAssistant, + fields: OrderedDict[Any, Any], yaml_config: ConfigType, - broker: str, - port: int, - username: str | None, - password: str | None, - protocol: str = "3.1", + entry_config: MappingProxyType[str, Any] | None, + user_input: ConfigType | None, + validated_user_input: ConfigType, + errors: dict[str, str], +) -> bool: + """Build the config flow schema to collect the broker settings. + + Returns True when settings are collected successfully. + """ + user_input_basic: ConfigType = ConfigType() + current_config = entry_config.copy() if entry_config is not None else ConfigType() + + if user_input is not None: + validated_user_input.update(user_input) + return True + + # Update the current settings the the new posted data to fill the defaults + current_config.update(user_input_basic) + + # Get default settings (if any) + current_broker = current_config.get(CONF_BROKER, yaml_config.get(CONF_BROKER)) + current_port = current_config.get( + CONF_PORT, yaml_config.get(CONF_PORT, DEFAULT_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)) + + # Build form + 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_user}, + ) + ] = str + fields[ + vol.Optional( + CONF_PASSWORD, + description={"suggested_value": current_pass}, + ) + ] = str + + # Show form + return False + + +def try_connection( + user_input: ConfigType, ) -> bool: """Test if we can connect to an MQTT broker.""" # We don't import on the top because some integrations # should be able to optionally rely on MQTT. import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel - # Get the config from configuration.yaml - entry_config = { - CONF_BROKER: broker, - CONF_PORT: port, - CONF_USERNAME: username, - CONF_PASSWORD: password, - CONF_PROTOCOL: protocol, - } - client = MqttClientSetup({**yaml_config, **entry_config}).client + client = MqttClientSetup(user_input).client result: queue.Queue[bool] = queue.Queue(maxsize=1) @@ -369,7 +401,7 @@ def try_connection( client.on_connect = on_connect - client.connect_async(broker, port) + client.connect_async(user_input[CONF_BROKER], user_input[CONF_PORT]) client.loop_start() try: diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 93410f0c792..d266ed231ba 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -40,6 +40,7 @@ DEFAULT_ENCODING = "utf-8" DEFAULT_QOS = 0 DEFAULT_PAYLOAD_AVAILABLE = "online" DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline" +DEFAULT_PORT = 1883 DEFAULT_RETAIN = False DEFAULT_BIRTH = { @@ -67,6 +68,8 @@ PAYLOAD_NONE = "None" PROTOCOL_31 = "3.1" PROTOCOL_311 = "3.1.1" +DEFAULT_PROTOCOL = PROTOCOL_311 + PLATFORMS = [ Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 5d67b34db5d..631f373316b 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -188,15 +188,12 @@ async def test_manual_config_set( # Check we tried the connection, with precedence for config entry settings mock_try_connection.assert_called_once_with( { - "broker": "bla", + "broker": "127.0.0.1", + "protocol": "3.1.1", "keepalive": 60, "discovery_prefix": "homeassistant", - "protocol": "3.1.1", + "port": 1883, }, - "127.0.0.1", - 1883, - None, - None, ) # Check config entry got setup assert len(mock_finish_setup.mock_calls) == 1 @@ -291,6 +288,44 @@ async def test_hassio_confirm(hass, mock_try_connection_success, mock_finish_set assert len(mock_finish_setup.mock_calls) == 1 +async def test_hassio_cannot_connect( + hass, mock_try_connection_time_out, mock_finish_setup +): + """Test a config flow is aborted when a connection was not successful.""" + mock_try_connection.return_value = True + + result = await hass.config_entries.flow.async_init( + "mqtt", + data=HassioServiceInfo( + config={ + "addon": "Mock Addon", + "host": "mock-broker", + "port": 1883, + "username": "mock-user", + "password": "mock-pass", + "protocol": "3.1.1", # Set by the addon's discovery, ignored by HA + "ssl": False, # Set by the addon's discovery, ignored by HA + } + ), + context={"source": config_entries.SOURCE_HASSIO}, + ) + assert result["type"] == "form" + assert result["step_id"] == "hassio_confirm" + assert result["description_placeholders"] == {"addon": "Mock Addon"} + + mock_try_connection_time_out.reset_mock() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"discovery": True} + ) + + assert result["type"] == "form" + assert result["errors"]["base"] == "cannot_connect" + # Check we tried the connection + assert len(mock_try_connection_time_out.mock_calls) + # Check config entry got setup + assert len(mock_finish_setup.mock_calls) == 0 + + @patch( "homeassistant.config.async_hass_config_yaml", AsyncMock(return_value={}), @@ -299,7 +334,7 @@ async def test_option_flow( hass, mqtt_mock_entry_no_yaml_config, mock_try_connection, - mock_reload_after_entry_update, + caplog, ): """Test config flow options.""" mqtt_mock = await mqtt_mock_entry_no_yaml_config() @@ -372,7 +407,10 @@ async def test_option_flow( await hass.async_block_till_done() assert config_entry.title == "another-broker" # assert that the entry was reloaded with the new config - assert mock_reload_after_entry_update.call_count == 1 + assert ( + "" + in caplog.text + ) async def test_disable_birth_will( From 9b44cf01278a1122e170b2fec0c64fb45d1acd74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 7 Oct 2022 13:24:09 +0300 Subject: [PATCH 221/985] Add Huawei LTE reauth flow (#78005) * Add Huawei LTE reauth flow * Upgrade huawei-lte-api to 1.6.3, use LoginErrorInvalidCredentialsException --- .../components/huawei_lte/__init__.py | 5 +- .../components/huawei_lte/config_flow.py | 147 +++++++++++---- .../components/huawei_lte/manifest.json | 2 +- .../components/huawei_lte/strings.json | 11 +- .../huawei_lte/translations/en.json | 11 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/huawei_lte/test_config_flow.py | 176 ++++++++++++++++-- 8 files changed, 295 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 5740cf99f53..b51d01f0fd7 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -15,6 +15,7 @@ from huawei_lte_api.Client import Client from huawei_lte_api.Connection import Connection from huawei_lte_api.enums.device import ControlModeEnum from huawei_lte_api.exceptions import ( + LoginErrorInvalidCredentialsException, ResponseErrorException, ResponseErrorLoginRequiredException, ResponseErrorNotSupportedException, @@ -38,7 +39,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -339,6 +340,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: connection = await hass.async_add_executor_job(get_connection) + except LoginErrorInvalidCredentialsException as ex: + raise ConfigEntryAuthFailed from ex except Timeout as ex: raise ConfigEntryNotReady from ex diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 3dfd38d6304..036eec37d44 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -1,6 +1,7 @@ """Config flow for the Huawei LTE platform.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import TYPE_CHECKING, Any from urllib.parse import urlparse @@ -89,6 +90,70 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors or {}, ) + async def _async_show_reauth_form( + self, + user_input: dict[str, Any], + errors: dict[str, str] | None = None, + ) -> FlowResult: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Optional( + CONF_USERNAME, default=user_input.get(CONF_USERNAME) or "" + ): str, + vol.Optional( + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD) or "" + ): str, + } + ), + errors=errors or {}, + ) + + async def _try_connect( + self, user_input: dict[str, Any], errors: dict[str, str] + ) -> Connection | None: + """Try connecting with given data.""" + username = user_input.get(CONF_USERNAME) or "" + password = user_input.get(CONF_PASSWORD) or "" + + def _get_connection() -> Connection: + return Connection( + url=user_input[CONF_URL], + username=username, + password=password, + timeout=CONNECTION_TIMEOUT, + ) + + conn = None + try: + conn = await self.hass.async_add_executor_job(_get_connection) + except LoginErrorUsernameWrongException: + errors[CONF_USERNAME] = "incorrect_username" + except LoginErrorPasswordWrongException: + errors[CONF_PASSWORD] = "incorrect_password" + except LoginErrorUsernamePasswordWrongException: + errors[CONF_USERNAME] = "invalid_auth" + except LoginErrorUsernamePasswordOverrunException: + errors["base"] = "login_attempts_exceeded" + except ResponseErrorException: + _LOGGER.warning("Response error", exc_info=True) + errors["base"] = "response_error" + except Timeout: + _LOGGER.warning("Connection timeout", exc_info=True) + errors[CONF_URL] = "connection_timeout" + except Exception: # pylint: disable=broad-except + _LOGGER.warning("Unknown error connecting to device", exc_info=True) + errors[CONF_URL] = "unknown" + return conn + + @staticmethod + def _logout(conn: Connection) -> None: + try: + conn.user_session.user.logout() # type: ignore[union-attr] + except Exception: # pylint: disable=broad-except + _LOGGER.debug("Could not logout", exc_info=True) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -108,25 +173,9 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input=user_input, errors=errors ) - def logout() -> None: - try: - conn.user_session.user.logout() # type: ignore[union-attr] - except Exception: # pylint: disable=broad-except - _LOGGER.debug("Could not logout", exc_info=True) - - def try_connect(user_input: dict[str, Any]) -> Connection: - """Try connecting with given credentials.""" - username = user_input.get(CONF_USERNAME) or "" - password = user_input.get(CONF_PASSWORD) or "" - conn = Connection( - user_input[CONF_URL], - username=username, - password=password, - timeout=CONNECTION_TIMEOUT, - ) - return conn - - def get_device_info() -> tuple[GetResponseType, GetResponseType]: + def get_device_info( + conn: Connection, + ) -> tuple[GetResponseType, GetResponseType]: """Get router info.""" client = Client(conn) try: @@ -147,33 +196,17 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): wlan_settings = {} return device_info, wlan_settings - try: - conn = await self.hass.async_add_executor_job(try_connect, user_input) - except LoginErrorUsernameWrongException: - errors[CONF_USERNAME] = "incorrect_username" - except LoginErrorPasswordWrongException: - errors[CONF_PASSWORD] = "incorrect_password" - except LoginErrorUsernamePasswordWrongException: - errors[CONF_USERNAME] = "invalid_auth" - except LoginErrorUsernamePasswordOverrunException: - errors["base"] = "login_attempts_exceeded" - except ResponseErrorException: - _LOGGER.warning("Response error", exc_info=True) - errors["base"] = "response_error" - except Timeout: - _LOGGER.warning("Connection timeout", exc_info=True) - errors[CONF_URL] = "connection_timeout" - except Exception: # pylint: disable=broad-except - _LOGGER.warning("Unknown error connecting to device", exc_info=True) - errors[CONF_URL] = "unknown" + conn = await self._try_connect(user_input, errors) if errors: - await self.hass.async_add_executor_job(logout) return await self._async_show_user_form( user_input=user_input, errors=errors ) + assert conn - info, wlan_settings = await self.hass.async_add_executor_job(get_device_info) - await self.hass.async_add_executor_job(logout) + info, wlan_settings = await self.hass.async_add_executor_job( + get_device_info, conn + ) + await self.hass.async_add_executor_job(self._logout, conn) user_input[CONF_MAC] = get_device_macs(info, wlan_settings) @@ -228,6 +261,38 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } return await self._async_show_user_form(user_input) + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Dialog that informs the user that reauth is required.""" + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert entry + if not user_input: + return await self._async_show_reauth_form( + user_input={ + CONF_USERNAME: entry.data[CONF_USERNAME], + CONF_PASSWORD: entry.data[CONF_PASSWORD], + } + ) + + new_data = {**entry.data, **user_input} + errors: dict[str, str] = {} + conn = await self._try_connect(new_data, errors) + if conn: + await self.hass.async_add_executor_job(self._logout, conn) + if errors: + return await self._async_show_reauth_form( + user_input=user_input, errors=errors + ) + + self.hass.config_entries.async_update_entry(entry, data=new_data) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + class OptionsFlowHandler(config_entries.OptionsFlow): """Huawei LTE options flow.""" diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 910e0e132f1..473d8df3124 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ - "huawei-lte-api==1.6.1", + "huawei-lte-api==1.6.3", "stringcase==1.2.0", "url-normalize==1.4.3" ], diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 0c1373192c5..8f6ec64491b 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Not a Huawei LTE device" + "not_huawei_lte": "Not a Huawei LTE device", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "connection_timeout": "Connection timeout", @@ -15,6 +16,14 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "Enter device access credentials.", + "data": { + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + } + }, "user": { "data": { "password": "[%key:common::config_flow::data::password%]", diff --git a/homeassistant/components/huawei_lte/translations/en.json b/homeassistant/components/huawei_lte/translations/en.json index 5636d952b19..134a5372f71 100644 --- a/homeassistant/components/huawei_lte/translations/en.json +++ b/homeassistant/components/huawei_lte/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Not a Huawei LTE device" + "not_huawei_lte": "Not a Huawei LTE device", + "reauth_successful": "Re-authentication was successful" }, "error": { "connection_timeout": "Connection timeout", @@ -15,6 +16,14 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Enter device access credentials.", + "title": "Reauthenticate Integration" + }, "user": { "data": { "password": "Password", diff --git a/requirements_all.txt b/requirements_all.txt index 64459d5fd5b..a8a081e5bd4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -886,7 +886,7 @@ horimote==0.4.1 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.6.1 +huawei-lte-api==1.6.3 # homeassistant.components.hydrawise hydrawiser==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 36d0785d580..a90238ec882 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -663,7 +663,7 @@ homepluscontrol==0.0.5 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.6.1 +huawei-lte-api==1.6.3 # homeassistant.components.hyperion hyperion-py==0.7.5 diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index 84f66e8f0ab..56c177a3602 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import patch from huawei_lte_api.enums.client import ResponseCodeEnum from huawei_lte_api.enums.user import LoginErrorEnum, LoginStateEnum, PasswordTypeEnum import pytest +import requests.exceptions from requests.exceptions import ConnectionError from requests_mock import ANY @@ -119,27 +120,66 @@ def login_requests_mock(requests_mock): @pytest.mark.parametrize( - ("code", "errors"), + ("request_outcome", "fixture_override", "errors"), ( - (LoginErrorEnum.USERNAME_WRONG, {CONF_USERNAME: "incorrect_username"}), - (LoginErrorEnum.PASSWORD_WRONG, {CONF_PASSWORD: "incorrect_password"}), ( - LoginErrorEnum.USERNAME_PWD_WRONG, + { + "text": f"{LoginErrorEnum.USERNAME_WRONG}", + }, + {}, + {CONF_USERNAME: "incorrect_username"}, + ), + ( + { + "text": f"{LoginErrorEnum.PASSWORD_WRONG}", + }, + {}, + {CONF_PASSWORD: "incorrect_password"}, + ), + ( + { + "text": f"{LoginErrorEnum.USERNAME_PWD_WRONG}", + }, + {}, {CONF_USERNAME: "invalid_auth"}, ), - (LoginErrorEnum.USERNAME_PWD_OVERRUN, {"base": "login_attempts_exceeded"}), - (ResponseCodeEnum.ERROR_SYSTEM_UNKNOWN, {"base": "response_error"}), + ( + { + "text": f"{LoginErrorEnum.USERNAME_PWD_OVERRUN}", + }, + {}, + {"base": "login_attempts_exceeded"}, + ), + ( + { + "text": f"{ResponseCodeEnum.ERROR_SYSTEM_UNKNOWN}", + }, + {}, + {"base": "response_error"}, + ), + ({}, {CONF_URL: "/foo/bar"}, {CONF_URL: "invalid_url"}), + ( + { + "exc": requests.exceptions.Timeout, + }, + {}, + {CONF_URL: "connection_timeout"}, + ), ), ) -async def test_login_error(hass, login_requests_mock, code, errors): +async def test_login_error( + hass, login_requests_mock, request_outcome, fixture_override, errors +): """Test we show user form with appropriate error on response failure.""" login_requests_mock.request( ANY, f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login", - text=f"{code}", + **request_outcome, ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={**FIXTURE_USER_INPUT, **fixture_override}, ) assert result["type"] == data_entry_flow.FlowResultType.FORM @@ -170,7 +210,43 @@ async def test_success(hass, login_requests_mock): assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] -async def test_ssdp(hass): +@pytest.mark.parametrize( + ("upnp_data", "expected_result"), + ( + ( + { + ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi", + ssdp.ATTR_UPNP_SERIAL: "00000000", + }, + { + "type": data_entry_flow.FlowResultType.FORM, + "step_id": "user", + "errors": {}, + }, + ), + ( + { + ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi", + # No ssdp.ATTR_UPNP_SERIAL + }, + { + "type": data_entry_flow.FlowResultType.FORM, + "step_id": "user", + "errors": {}, + }, + ), + ( + { + ssdp.ATTR_UPNP_FRIENDLY_NAME: "Some other device", + }, + { + "type": data_entry_flow.FlowResultType.ABORT, + "reason": "not_huawei_lte", + }, + ), + ), +) +async def test_ssdp(hass, upnp_data, expected_result): """Test SSDP discovery initiates config properly.""" url = "http://192.168.100.1/" context = {"source": config_entries.SOURCE_SSDP} @@ -183,21 +259,93 @@ async def test_ssdp(hass): ssdp_location="http://192.168.100.1:60957/rootDesc.xml", upnp={ ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1", - ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi", ssdp.ATTR_UPNP_MANUFACTURER: "Huawei", ssdp.ATTR_UPNP_MANUFACTURER_URL: "http://www.huawei.com/", ssdp.ATTR_UPNP_MODEL_NAME: "Huawei router", ssdp.ATTR_UPNP_MODEL_NUMBER: "12345678", ssdp.ATTR_UPNP_PRESENTATION_URL: url, - ssdp.ATTR_UPNP_SERIAL: "00000000", ssdp.ATTR_UPNP_UDN: "uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + **upnp_data, }, ), ) + for k, v in expected_result.items(): + assert result[k] == v + if result.get("data_schema"): + result["data_schema"]({})[CONF_URL] == url + + +@pytest.mark.parametrize( + ("login_response_text", "expected_result", "expected_entry_data"), + ( + ( + "OK", + { + "type": data_entry_flow.FlowResultType.ABORT, + "reason": "reauth_successful", + }, + FIXTURE_USER_INPUT, + ), + ( + f"{LoginErrorEnum.PASSWORD_WRONG}", + { + "type": data_entry_flow.FlowResultType.FORM, + "errors": {CONF_PASSWORD: "incorrect_password"}, + "step_id": "reauth_confirm", + }, + {**FIXTURE_USER_INPUT, CONF_PASSWORD: "invalid-password"}, + ), + ), +) +async def test_reauth( + hass, login_requests_mock, login_response_text, expected_result, expected_entry_data +): + """Test reauth.""" + mock_entry_data = {**FIXTURE_USER_INPUT, CONF_PASSWORD: "invalid-password"} + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_UNIQUE_ID, + data=mock_entry_data, + title="Reauth canary", + ) + entry.add_to_hass(hass) + + context = { + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + } + result = await hass.config_entries.flow.async_init( + DOMAIN, context=context, data=entry.data + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - assert result["data_schema"]({})[CONF_URL] == url + assert result["step_id"] == "reauth_confirm" + assert result["data_schema"]({}) == { + CONF_USERNAME: mock_entry_data[CONF_USERNAME], + CONF_PASSWORD: mock_entry_data[CONF_PASSWORD], + } + assert not result["errors"] + + login_requests_mock.request( + ANY, + f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login", + text=login_response_text, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: FIXTURE_USER_INPUT[CONF_USERNAME], + CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD], + }, + ) + await hass.async_block_till_done() + + for k, v in expected_result.items(): + assert result[k] == v + for k, v in expected_entry_data.items(): + assert entry.data[k] == v async def test_options(hass): From b51c434b9d2f4a426d701f2ba26df2f9c88536f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Del=20Rinc=C3=B3n=20L=C3=B3pez?= Date: Fri, 7 Oct 2022 13:48:05 +0200 Subject: [PATCH 222/985] Add support for Xiaomi Purifier 4 Lite (#79758) * Added support for Xiaomi Purifier 4 Lite * Remove favorite level from Xiaomi purifier 4 lite. * Fix linting Co-authored-by: borky-git --- homeassistant/components/xiaomi_miio/const.py | 8 ++++++++ homeassistant/components/xiaomi_miio/fan.py | 13 +++++++++++++ homeassistant/components/xiaomi_miio/number.py | 5 +++++ homeassistant/components/xiaomi_miio/sensor.py | 14 ++++++++++++++ homeassistant/components/xiaomi_miio/switch.py | 5 +++++ 5 files changed, 45 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index c0711a02a36..6a3b7b6530d 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -48,6 +48,8 @@ class SetupException(Exception): # Fan Models MODEL_AIRPURIFIER_4 = "zhimi.airp.mb5" +MODEL_AIRPURIFIER_4_LITE_RMA1 = "zhimi.airpurifier.rma1" +MODEL_AIRPURIFIER_4_LITE_RMB1 = "zhimi.airp.rmb1" MODEL_AIRPURIFIER_4_PRO = "zhimi.airp.vb4" MODEL_AIRPURIFIER_2H = "zhimi.airpurifier.mc2" MODEL_AIRPURIFIER_2S = "zhimi.airpurifier.mc1" @@ -117,6 +119,8 @@ MODELS_PURIFIER_MIOT = [ MODEL_AIRPURIFIER_3C, MODEL_AIRPURIFIER_3H, MODEL_AIRPURIFIER_PROH, + MODEL_AIRPURIFIER_4_LITE_RMA1, + MODEL_AIRPURIFIER_4_LITE_RMB1, MODEL_AIRPURIFIER_4, MODEL_AIRPURIFIER_4_PRO, ] @@ -342,6 +346,10 @@ FEATURE_FLAGS_AIRPURIFIER_MIOT = ( | FEATURE_SET_LED_BRIGHTNESS ) +FEATURE_FLAGS_AIRPURIFIER_4_LITE = ( + FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_LED_BRIGHTNESS +) + FEATURE_FLAGS_AIRPURIFIER_4 = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index ddbd45bff08..dbc8c7a66d9 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -49,6 +49,7 @@ from .const import ( FEATURE_FLAGS_AIRPURIFIER_2S, FEATURE_FLAGS_AIRPURIFIER_3C, FEATURE_FLAGS_AIRPURIFIER_4, + FEATURE_FLAGS_AIRPURIFIER_4_LITE, FEATURE_FLAGS_AIRPURIFIER_MIIO, FEATURE_FLAGS_AIRPURIFIER_MIOT, FEATURE_FLAGS_AIRPURIFIER_PRO, @@ -70,6 +71,8 @@ from .const import ( MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_3C, MODEL_AIRPURIFIER_4, + MODEL_AIRPURIFIER_4_LITE_RMA1, + MODEL_AIRPURIFIER_4_LITE_RMB1, MODEL_AIRPURIFIER_4_PRO, MODEL_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7, @@ -151,6 +154,7 @@ AVAILABLE_ATTRIBUTES_AIRFRESH = { } PRESET_MODES_AIRPURIFIER = ["Auto", "Silent", "Favorite", "Idle"] +PRESET_MODES_AIRPURIFIER_4_LITE = ["Auto", "Silent", "Favorite"] PRESET_MODES_AIRPURIFIER_MIOT = ["Auto", "Silent", "Favorite", "Fan"] PRESET_MODES_AIRPURIFIER_PRO = ["Auto", "Silent", "Favorite"] PRESET_MODES_AIRPURIFIER_PRO_V7 = PRESET_MODES_AIRPURIFIER_PRO @@ -424,6 +428,15 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE ) self._speed_count = 3 + elif self._model in [ + MODEL_AIRPURIFIER_4_LITE_RMA1, + MODEL_AIRPURIFIER_4_LITE_RMB1, + ]: + self._device_features = FEATURE_FLAGS_AIRPURIFIER_4_LITE + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_MIOT + self._preset_modes = PRESET_MODES_AIRPURIFIER_4_LITE + self._attr_supported_features = FanEntityFeature.PRESET_MODE + self._speed_count = 1 elif self._model == MODEL_AIRPURIFIER_PRO_V7: self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO_V7 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 15cb7175d91..7c5439d8d35 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -32,6 +32,7 @@ from .const import ( FEATURE_FLAGS_AIRPURIFIER_2S, FEATURE_FLAGS_AIRPURIFIER_3C, FEATURE_FLAGS_AIRPURIFIER_4, + FEATURE_FLAGS_AIRPURIFIER_4_LITE, FEATURE_FLAGS_AIRPURIFIER_MIIO, FEATURE_FLAGS_AIRPURIFIER_MIOT, FEATURE_FLAGS_AIRPURIFIER_PRO, @@ -65,6 +66,8 @@ from .const import ( MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_3C, MODEL_AIRPURIFIER_4, + MODEL_AIRPURIFIER_4_LITE_RMA1, + MODEL_AIRPURIFIER_4_LITE_RMB1, MODEL_AIRPURIFIER_4_PRO, MODEL_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7, @@ -242,6 +245,8 @@ MODEL_TO_FEATURES_MAP = { MODEL_AIRPURIFIER_PRO_V7: FEATURE_FLAGS_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V1: FEATURE_FLAGS_AIRPURIFIER_V1, MODEL_AIRPURIFIER_V3: FEATURE_FLAGS_AIRPURIFIER_V3, + MODEL_AIRPURIFIER_4_LITE_RMA1: FEATURE_FLAGS_AIRPURIFIER_4_LITE, + MODEL_AIRPURIFIER_4_LITE_RMB1: FEATURE_FLAGS_AIRPURIFIER_4_LITE, MODEL_AIRPURIFIER_4: FEATURE_FLAGS_AIRPURIFIER_4, MODEL_AIRPURIFIER_4_PRO: FEATURE_FLAGS_AIRPURIFIER_4, MODEL_FAN_1C: FEATURE_FLAGS_FAN_1C, diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index c94e0e371fb..56938a4bd34 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -63,6 +63,8 @@ from .const import ( MODEL_AIRHUMIDIFIER_CB1, MODEL_AIRPURIFIER_3C, MODEL_AIRPURIFIER_4, + MODEL_AIRPURIFIER_4_LITE_RMA1, + MODEL_AIRPURIFIER_4_LITE_RMB1, MODEL_AIRPURIFIER_4_PRO, MODEL_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7, @@ -411,6 +413,16 @@ PURIFIER_MIOT_SENSORS = ( ATTR_TEMPERATURE, ATTR_USE_TIME, ) +PURIFIER_4_LITE_SENSORS = ( + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_LEFT_TIME, + ATTR_FILTER_USE, + ATTR_HUMIDITY, + ATTR_MOTOR_SPEED, + ATTR_PM25, + ATTR_TEMPERATURE, + ATTR_USE_TIME, +) PURIFIER_4_SENSORS = ( ATTR_FILTER_LIFE_REMAINING, ATTR_FILTER_LEFT_TIME, @@ -528,6 +540,8 @@ MODEL_TO_SENSORS_MAP: dict[str, tuple[str, ...]] = { MODEL_AIRHUMIDIFIER_CA1: HUMIDIFIER_CA1_CB1_SENSORS, MODEL_AIRHUMIDIFIER_CB1: HUMIDIFIER_CA1_CB1_SENSORS, MODEL_AIRPURIFIER_3C: PURIFIER_3C_SENSORS, + MODEL_AIRPURIFIER_4_LITE_RMA1: PURIFIER_4_LITE_SENSORS, + MODEL_AIRPURIFIER_4_LITE_RMB1: PURIFIER_4_LITE_SENSORS, MODEL_AIRPURIFIER_4: PURIFIER_4_SENSORS, MODEL_AIRPURIFIER_4_PRO: PURIFIER_4_PRO_SENSORS, MODEL_AIRPURIFIER_PRO: PURIFIER_PRO_SENSORS, diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index dbe783c6a87..2f45ba0adca 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -46,6 +46,7 @@ from .const import ( FEATURE_FLAGS_AIRPURIFIER_2S, FEATURE_FLAGS_AIRPURIFIER_3C, FEATURE_FLAGS_AIRPURIFIER_4, + FEATURE_FLAGS_AIRPURIFIER_4_LITE, FEATURE_FLAGS_AIRPURIFIER_MIIO, FEATURE_FLAGS_AIRPURIFIER_MIOT, FEATURE_FLAGS_AIRPURIFIER_PRO, @@ -82,6 +83,8 @@ from .const import ( MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_3C, MODEL_AIRPURIFIER_4, + MODEL_AIRPURIFIER_4_LITE_RMA1, + MODEL_AIRPURIFIER_4_LITE_RMB1, MODEL_AIRPURIFIER_4_PRO, MODEL_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7, @@ -197,6 +200,8 @@ MODEL_TO_FEATURES_MAP = { MODEL_AIRPURIFIER_PRO_V7: FEATURE_FLAGS_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V1: FEATURE_FLAGS_AIRPURIFIER_V1, MODEL_AIRPURIFIER_V3: FEATURE_FLAGS_AIRPURIFIER_V3, + MODEL_AIRPURIFIER_4_LITE_RMA1: FEATURE_FLAGS_AIRPURIFIER_4_LITE, + MODEL_AIRPURIFIER_4_LITE_RMB1: FEATURE_FLAGS_AIRPURIFIER_4_LITE, MODEL_AIRPURIFIER_4: FEATURE_FLAGS_AIRPURIFIER_4, MODEL_AIRPURIFIER_4_PRO: FEATURE_FLAGS_AIRPURIFIER_4, MODEL_FAN_1C: FEATURE_FLAGS_FAN_1C, From 43091a98562de0a4ccb689a034ad693f92a4c129 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 7 Oct 2022 14:23:53 +0200 Subject: [PATCH 223/985] Revert "Improve device_automation trigger validation" (#79778) Revert "Improve device_automation trigger validation (#75044)" This reverts commit 55b036ec5ef570b146a6126408da929dd66e431e. --- .../components/device_automation/action.py | 6 +-- .../components/device_automation/condition.py | 4 +- .../components/device_automation/trigger.py | 5 +- .../components/rfxtrx/device_action.py | 1 + .../components/device_automation/test_init.py | 51 ++----------------- .../components/webostv/test_device_trigger.py | 3 +- 6 files changed, 14 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/device_automation/action.py b/homeassistant/components/device_automation/action.py index 432ff2fdb7d..081b6bb283a 100644 --- a/homeassistant/components/device_automation/action.py +++ b/homeassistant/components/device_automation/action.py @@ -7,7 +7,6 @@ import voluptuous as vol from homeassistant.const import CONF_DOMAIN from homeassistant.core import Context, HomeAssistant -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType from . import DeviceAutomationType, async_get_device_automation_platform @@ -52,15 +51,14 @@ async def async_validate_action_config( ) -> ConfigType: """Validate config.""" try: - config = cv.DEVICE_ACTION_SCHEMA(config) platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], DeviceAutomationType.ACTION ) if hasattr(platform, "async_validate_action_config"): return await platform.async_validate_action_config(hass, config) return cast(ConfigType, platform.ACTION_SCHEMA(config)) - except (vol.Invalid, InvalidDeviceAutomationConfig) as err: - raise vol.Invalid("invalid action configuration: " + str(err)) from err + except InvalidDeviceAutomationConfig as err: + raise vol.Invalid(str(err) or "Invalid action configuration") from err async def async_call_action_from_config( diff --git a/homeassistant/components/device_automation/condition.py b/homeassistant/components/device_automation/condition.py index 3b0a5263f9e..d656908f4be 100644 --- a/homeassistant/components/device_automation/condition.py +++ b/homeassistant/components/device_automation/condition.py @@ -58,8 +58,8 @@ async def async_validate_condition_config( if hasattr(platform, "async_validate_condition_config"): return await platform.async_validate_condition_config(hass, config) return cast(ConfigType, platform.CONDITION_SCHEMA(config)) - except (vol.Invalid, InvalidDeviceAutomationConfig) as err: - raise vol.Invalid("invalid condition configuration: " + str(err)) from err + except InvalidDeviceAutomationConfig as err: + raise vol.Invalid(str(err) or "Invalid condition configuration") from err async def async_condition_from_config( diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py index aac56b39846..bd72b24d844 100644 --- a/homeassistant/components/device_automation/trigger.py +++ b/homeassistant/components/device_automation/trigger.py @@ -58,15 +58,14 @@ async def async_validate_trigger_config( ) -> ConfigType: """Validate config.""" try: - config = TRIGGER_SCHEMA(config) platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], DeviceAutomationType.TRIGGER ) if not hasattr(platform, "async_validate_trigger_config"): return cast(ConfigType, platform.TRIGGER_SCHEMA(config)) return await platform.async_validate_trigger_config(hass, config) - except (vol.Invalid, InvalidDeviceAutomationConfig) as err: - raise InvalidDeviceAutomationConfig("invalid trigger configuration") from err + except InvalidDeviceAutomationConfig as err: + raise vol.Invalid(str(err) or "Invalid trigger configuration") from err async def async_attach_trigger( diff --git a/homeassistant/components/rfxtrx/device_action.py b/homeassistant/components/rfxtrx/device_action.py index 7ea4ed07423..15595b88cd2 100644 --- a/homeassistant/components/rfxtrx/device_action.py +++ b/homeassistant/components/rfxtrx/device_action.py @@ -80,6 +80,7 @@ async def async_validate_action_config( hass: HomeAssistant, config: ConfigType ) -> ConfigType: """Validate config.""" + config = ACTION_SCHEMA(config) commands, _ = _get_commands(hass, config[CONF_DEVICE_ID], config[CONF_TYPE]) sub_type = config[CONF_SUBTYPE] diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 71c062cf7d9..3ead6fcb35d 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -720,28 +720,7 @@ async def test_automation_with_bad_condition_action(hass, caplog): assert "required key not provided" in caplog.text -async def test_automation_with_bad_condition_missing_domain(hass, caplog): - """Test automation with bad device condition.""" - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "alias": "hello", - "trigger": {"platform": "event", "event_type": "test_event1"}, - "condition": {"condition": "device", "device_id": "hello.device"}, - "action": {"service": "test.automation", "entity_id": "hello.world"}, - } - }, - ) - - assert ( - "Invalid config for [automation]: required key not provided @ data['condition'][0]['domain']" - in caplog.text - ) - - -async def test_automation_with_bad_condition_missing_device_id(hass, caplog): +async def test_automation_with_bad_condition(hass, caplog): """Test automation with bad device condition.""" assert await async_setup_component( hass, @@ -756,10 +735,7 @@ async def test_automation_with_bad_condition_missing_device_id(hass, caplog): }, ) - assert ( - "Invalid config for [automation]: required key not provided @ data['condition'][0]['device_id']" - in caplog.text - ) + assert "required key not provided" in caplog.text @pytest.fixture @@ -900,25 +876,8 @@ async def test_automation_with_bad_sub_condition(hass, caplog): assert "required key not provided" in caplog.text -async def test_automation_with_bad_trigger_missing_domain(hass, caplog): - """Test automation with device trigger this is missing domain.""" - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "alias": "hello", - "trigger": {"platform": "device", "device_id": "hello.device"}, - "action": {"service": "test.automation", "entity_id": "hello.world"}, - } - }, - ) - - assert "required key not provided @ data['domain']" in caplog.text - - -async def test_automation_with_bad_trigger_missing_device_id(hass, caplog): - """Test automation with device trigger that is missing device_id.""" +async def test_automation_with_bad_trigger(hass, caplog): + """Test automation with bad device trigger.""" assert await async_setup_component( hass, automation.DOMAIN, @@ -931,7 +890,7 @@ async def test_automation_with_bad_trigger_missing_device_id(hass, caplog): }, ) - assert "required key not provided @ data['device_id']" in caplog.text + assert "required key not provided" in caplog.text async def test_websocket_device_not_found(hass, hass_ws_client): diff --git a/tests/components/webostv/test_device_trigger.py b/tests/components/webostv/test_device_trigger.py index 96914885971..db15ce3a592 100644 --- a/tests/components/webostv/test_device_trigger.py +++ b/tests/components/webostv/test_device_trigger.py @@ -128,7 +128,8 @@ async def test_get_triggers_for_invalid_device_id(hass, caplog): await hass.async_block_till_done() assert ( - "Invalid config for [automation]: invalid trigger configuration" in caplog.text + "Invalid config for [automation]: Device invalid_device_id is not a valid webostv device" + in caplog.text ) From b450514fb3ae8eaf772970eb1309f72578397703 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 7 Oct 2022 14:57:48 +0200 Subject: [PATCH 224/985] Add Roborock S7 MaxV for xiaomi_miio (#79477) --- homeassistant/components/xiaomi_miio/const.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 6a3b7b6530d..0c090a58e02 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -4,6 +4,7 @@ from miio.vacuum import ( ROCKROBO_S6, ROCKROBO_S6_MAXV, ROCKROBO_S7, + ROCKROBO_S7_MAXV, ROCKROBO_V1, ) @@ -231,6 +232,7 @@ MODELS_VACUUM = [ ROCKROBO_S6_MAXV, ROCKROBO_S6_PURE, ROCKROBO_S7, + ROCKROBO_S7_MAXV, ROBOROCK_GENERIC, ROCKROBO_GENERIC, ] @@ -242,9 +244,11 @@ MODELS_VACUUM_WITH_MOP = [ ROCKROBO_S6_MAXV, ROCKROBO_S6_PURE, ROCKROBO_S7, + ROCKROBO_S7_MAXV, ] MODELS_VACUUM_WITH_SEPARATE_MOP = [ ROCKROBO_S7, + ROCKROBO_S7_MAXV, ] MODELS_AIR_MONITOR = [ From 0b9d02935076a7c78c7384392a30d3aa0e5bce7f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 7 Oct 2022 15:03:58 +0200 Subject: [PATCH 225/985] Add switch platform to LaMetric (#79759) * Add switch platform to LaMetric * Little naming tweak --- homeassistant/components/lametric/const.py | 2 +- homeassistant/components/lametric/switch.py | 102 ++++++++++++++++++++ tests/components/lametric/test_switch.py | 89 +++++++++++++++++ 3 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/lametric/switch.py create mode 100644 tests/components/lametric/test_switch.py diff --git a/homeassistant/components/lametric/const.py b/homeassistant/components/lametric/const.py index da84450e784..1ba48e0d992 100644 --- a/homeassistant/components/lametric/const.py +++ b/homeassistant/components/lametric/const.py @@ -7,7 +7,7 @@ from typing import Final from homeassistant.const import Platform DOMAIN: Final = "lametric" -PLATFORMS = [Platform.BUTTON, Platform.NUMBER] +PLATFORMS = [Platform.BUTTON, Platform.NUMBER, Platform.SWITCH] LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/lametric/switch.py b/homeassistant/components/lametric/switch.py new file mode 100644 index 00000000000..8c0acac65e6 --- /dev/null +++ b/homeassistant/components/lametric/switch.py @@ -0,0 +1,102 @@ +"""Support for LaMetric switches.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any + +from demetriek import Device, LaMetricDevice + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import LaMetricDataUpdateCoordinator +from .entity import LaMetricEntity + + +@dataclass +class LaMetricEntityDescriptionMixin: + """Mixin values for LaMetric entities.""" + + is_on_fn: Callable[[Device], bool] + set_fn: Callable[[LaMetricDevice, bool], Awaitable[Any]] + + +@dataclass +class LaMetricSwitchEntityDescription( + SwitchEntityDescription, LaMetricEntityDescriptionMixin +): + """Class describing LaMetric switch entities.""" + + available_fn: Callable[[Device], bool] = lambda device: True + + +SWITCHES = [ + LaMetricSwitchEntityDescription( + key="bluetooth", + name="Bluetooth", + icon="mdi:bluetooth", + entity_category=EntityCategory.CONFIG, + available_fn=lambda device: device.bluetooth.available, + is_on_fn=lambda device: device.bluetooth.active, + set_fn=lambda api, active: api.bluetooth(active=active), + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up LaMetric switch based on a config entry.""" + coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + LaMetricSwitchEntity( + coordinator=coordinator, + description=description, + ) + for description in SWITCHES + ) + + +class LaMetricSwitchEntity(LaMetricEntity, SwitchEntity): + """Representation of a LaMetric switch.""" + + entity_description: LaMetricSwitchEntityDescription + + def __init__( + self, + coordinator: LaMetricDataUpdateCoordinator, + description: LaMetricSwitchEntityDescription, + ) -> None: + """Initiate LaMetric Switch.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.data.serial_number}-{description.key}" + + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and self.entity_description.available_fn( + self.coordinator.data + ) + + @property + def is_on(self) -> bool: + """Return state of the switch.""" + return self.entity_description.is_on_fn(self.coordinator.data) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.entity_description.set_fn(self.coordinator.lametric, True) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self.entity_description.set_fn(self.coordinator.lametric, False) + await self.coordinator.async_request_refresh() diff --git a/tests/components/lametric/test_switch.py b/tests/components/lametric/test_switch.py new file mode 100644 index 00000000000..350fa1b24f8 --- /dev/null +++ b/tests/components/lametric/test_switch.py @@ -0,0 +1,89 @@ +"""Tests for the LaMetric switch platform.""" +from unittest.mock import MagicMock + +from homeassistant.components.lametric.const import DOMAIN, SCAN_INTERVAL +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + STATE_OFF, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory +import homeassistant.util.dt as dt_util + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_bluetooth( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_lametric: MagicMock, +) -> None: + """Test the LaMetric Bluetooth control.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + state = hass.states.get("switch.frenck_s_lametric_bluetooth") + assert state + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's LaMetric Bluetooth" + assert state.attributes.get(ATTR_ICON) == "mdi:bluetooth" + assert state.state == STATE_OFF + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry.device_id + assert entry.entity_category is EntityCategory.CONFIG + assert entry.unique_id == "SA110405124500W00BS9-bluetooth" + + device = device_registry.async_get(entry.device_id) + assert device + assert device.configuration_url is None + assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")} + assert device.entry_type is None + assert device.hw_version is None + assert device.identifiers == {(DOMAIN, "SA110405124500W00BS9")} + assert device.manufacturer == "LaMetric Inc." + assert device.name == "Frenck's LaMetric" + assert device.sw_version == "2.2.2" + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "switch.frenck_s_lametric_bluetooth", + }, + blocking=True, + ) + + assert len(mock_lametric.bluetooth.mock_calls) == 1 + mock_lametric.bluetooth.assert_called_once_with(active=True) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: "switch.frenck_s_lametric_bluetooth", + }, + blocking=True, + ) + + assert len(mock_lametric.bluetooth.mock_calls) == 2 + mock_lametric.bluetooth.assert_called_with(active=False) + + mock_lametric.device.return_value.bluetooth.available = False + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("switch.frenck_s_lametric_bluetooth") + assert state + assert state.state == STATE_UNAVAILABLE From e45701fe89e4ce523fcd0ccb9a0abd07868eda90 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 7 Oct 2022 07:33:53 -0600 Subject: [PATCH 226/985] Add @bachya as a LaMetric codeowner (#79772) --- CODEOWNERS | 4 ++-- homeassistant/components/lametric/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index af8423f42ae..c7bc33d244f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -604,8 +604,8 @@ build.json @home-assistant/supervisor /tests/components/kulersky/ @emlove /homeassistant/components/lacrosse_view/ @IceBotYT /tests/components/lacrosse_view/ @IceBotYT -/homeassistant/components/lametric/ @robbiet480 @frenck -/tests/components/lametric/ @robbiet480 @frenck +/homeassistant/components/lametric/ @robbiet480 @frenck @bachya +/tests/components/lametric/ @robbiet480 @frenck @bachya /homeassistant/components/landisgyr_heat_meter/ @vpathuis /tests/components/landisgyr_heat_meter/ @vpathuis /homeassistant/components/launch_library/ @ludeeus @DurgNomis-drol diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json index cddf28e5487..fb9f0bf03b2 100644 --- a/homeassistant/components/lametric/manifest.json +++ b/homeassistant/components/lametric/manifest.json @@ -3,7 +3,7 @@ "name": "LaMetric", "documentation": "https://www.home-assistant.io/integrations/lametric", "requirements": ["demetriek==0.2.4"], - "codeowners": ["@robbiet480", "@frenck"], + "codeowners": ["@robbiet480", "@frenck", "@bachya"], "iot_class": "local_polling", "dependencies": ["application_credentials"], "loggers": ["demetriek"], From f9aa7c58089ceeb816e1ba8694877be5e0fcfeed Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 7 Oct 2022 16:03:24 +0200 Subject: [PATCH 227/985] Update pyoverkiz to 1.5.5 (#79798) --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index 0b3e041f302..480b0b1d9ed 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -3,7 +3,7 @@ "name": "Overkiz", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", - "requirements": ["pyoverkiz==1.5.3"], + "requirements": ["pyoverkiz==1.5.5"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index a8a081e5bd4..b5a2fd74bc9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1786,7 +1786,7 @@ pyotgw==2.0.3 pyotp==2.7.0 # homeassistant.components.overkiz -pyoverkiz==1.5.3 +pyoverkiz==1.5.5 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a90238ec882..bc85a9bda61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1263,7 +1263,7 @@ pyotgw==2.0.3 pyotp==2.7.0 # homeassistant.components.overkiz -pyoverkiz==1.5.3 +pyoverkiz==1.5.5 # homeassistant.components.openweathermap pyowm==3.2.0 From e1d3ba6ff11d4a7ece3fa4a822a0d4e9582a98ef Mon Sep 17 00:00:00 2001 From: borky Date: Fri, 7 Oct 2022 17:03:34 +0300 Subject: [PATCH 228/985] Add xiaomi miio airpurifier 4 led brightness (#78793) Fixed Led Brightness not available --- homeassistant/components/xiaomi_miio/select.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 69f8dbfad30..0c573f749cd 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -47,6 +47,8 @@ from .const import ( MODEL_AIRHUMIDIFIER_V1, MODEL_AIRPURIFIER_3, MODEL_AIRPURIFIER_3H, + MODEL_AIRPURIFIER_4, + MODEL_AIRPURIFIER_4_PRO, MODEL_AIRPURIFIER_M1, MODEL_AIRPURIFIER_M2, MODEL_AIRPURIFIER_PROH, @@ -111,6 +113,12 @@ MODEL_TO_ATTR_MAP: dict[str, list] = { MODEL_AIRPURIFIER_3H: [ AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierMiotLedBrightness) ], + MODEL_AIRPURIFIER_4: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierMiotLedBrightness) + ], + MODEL_AIRPURIFIER_4_PRO: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierMiotLedBrightness) + ], MODEL_AIRPURIFIER_M1: [ AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierLedBrightness) ], From 9850709b37fdfa704ac3db4c45a2660880a7ca65 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 7 Oct 2022 10:28:05 -0400 Subject: [PATCH 229/985] Add strict typing to Skybell (#79800) --- .strict-typing | 1 + homeassistant/components/skybell/sensor.py | 18 +++++++++++++----- mypy.ini | 10 ++++++++++ 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/.strict-typing b/.strict-typing index 635356f4950..466312a30a9 100644 --- a/.strict-typing +++ b/.strict-typing @@ -232,6 +232,7 @@ homeassistant.components.sensor.* homeassistant.components.senz.* homeassistant.components.shelly.* homeassistant.components.simplisafe.* +homeassistant.components.skybell.* homeassistant.components.slack.* homeassistant.components.sleepiq.* homeassistant.components.smhi.* diff --git a/homeassistant/components/skybell/sensor.py b/homeassistant/components/skybell/sensor.py index 7acc30d0bd0..ff3ba47ae85 100644 --- a/homeassistant/components/skybell/sensor.py +++ b/homeassistant/components/skybell/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Any +from datetime import datetime from aioskybell import SkybellDevice from aioskybell.helpers import const as CONST @@ -17,15 +17,23 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from .entity import DOMAIN, SkybellEntity @dataclass -class SkybellSensorEntityDescription(SensorEntityDescription): - """Class to describe a Skybell sensor.""" +class SkybellSensorEntityDescriptionMixIn: + """Mixin for Skybell sensor.""" - value_fn: Callable[[SkybellDevice], Any] = lambda val: val + value_fn: Callable[[SkybellDevice], StateType | datetime] + + +@dataclass +class SkybellSensorEntityDescription( + SensorEntityDescription, SkybellSensorEntityDescriptionMixIn +): + """Class to describe a Skybell sensor.""" SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( @@ -110,6 +118,6 @@ class SkybellSensor(SkybellEntity, SensorEntity): entity_description: SkybellSensorEntityDescription @property - def native_value(self) -> int: + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" return self.entity_description.value_fn(self._device) diff --git a/mypy.ini b/mypy.ini index 267e856aa09..819f461454b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2072,6 +2072,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.skybell.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.slack.*] check_untyped_defs = true disallow_incomplete_defs = true From 04f07cecba22a6ee31ca6d7a459d646a858b8293 Mon Sep 17 00:00:00 2001 From: Jeef Date: Fri, 7 Oct 2022 10:37:00 -0600 Subject: [PATCH 230/985] IntelliFire Fan - Bug fix on off funciton (#79819) fix: Fan was double calling off as well as calling an async without an await --- homeassistant/components/intellifire/fan.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/intellifire/fan.py b/homeassistant/components/intellifire/fan.py index d37bef2189a..0f438569389 100644 --- a/homeassistant/components/intellifire/fan.py +++ b/homeassistant/components/intellifire/fan.py @@ -125,6 +125,5 @@ class IntellifireFan(IntellifireEntity, FanEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the fan.""" - self.coordinator.control_api.fan_off() await self.entity_description.set_fn(self.coordinator.control_api, 0) await self.coordinator.async_request_refresh() From 59818649922bf1f44282d6714fed37fb1005eef9 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 7 Oct 2022 13:08:08 -0400 Subject: [PATCH 231/985] Add strict typing to Sonarr (#79802) --- .strict-typing | 1 + homeassistant/components/sonarr/entity.py | 7 +------ homeassistant/components/sonarr/sensor.py | 6 +++--- mypy.ini | 10 ++++++++++ 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.strict-typing b/.strict-typing index 466312a30a9..0a0f5a2496c 100644 --- a/.strict-typing +++ b/.strict-typing @@ -236,6 +236,7 @@ homeassistant.components.skybell.* homeassistant.components.slack.* homeassistant.components.sleepiq.* homeassistant.components.smhi.* +homeassistant.components.sonarr.* homeassistant.components.ssdp.* homeassistant.components.statistics.* homeassistant.components.steamist.* diff --git a/homeassistant/components/sonarr/entity.py b/homeassistant/components/sonarr/entity.py index 41f6786503d..852e326cdb4 100644 --- a/homeassistant/components/sonarr/entity.py +++ b/homeassistant/components/sonarr/entity.py @@ -1,6 +1,4 @@ """Base Entity for Sonarr.""" -from __future__ import annotations - from aiopyarr import SystemStatus from aiopyarr.models.host_configuration import PyArrHostConfiguration from aiopyarr.sonarr_client import SonarrClient @@ -31,11 +29,8 @@ class SonarrEntity(Entity): self.system_status = system_status @property - def device_info(self) -> DeviceInfo | None: + def device_info(self) -> DeviceInfo: """Return device information about the application.""" - if self._device_id is None: - return None - return DeviceInfo( identifiers={(DOMAIN, self._device_id)}, name="Activity Sensor", diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index adc588f6951..cdb44dee359 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -5,7 +5,7 @@ from collections.abc import Awaitable, Callable, Coroutine from datetime import timedelta from functools import wraps import logging -from typing import Any, TypeVar +from typing import Any, TypeVar, cast from aiopyarr import ArrConnectionException, ArrException, SystemStatus from aiopyarr.models.host_configuration import PyArrHostConfiguration @@ -267,7 +267,7 @@ class SonarrSensor(SonarrEntity, SensorEntity): return len(self.data[key]) if key == "queue" and self.data.get(key) is not None: - return self.data[key].totalRecords + return cast(int, self.data[key].totalRecords) if key == "series" and self.data.get(key) is not None: return len(self.data[key]) @@ -276,6 +276,6 @@ class SonarrSensor(SonarrEntity, SensorEntity): return len(self.data[key]) if key == "wanted" and self.data.get(key) is not None: - return self.data[key].totalRecords + return cast(int, self.data[key].totalRecords) return None diff --git a/mypy.ini b/mypy.ini index 819f461454b..9b748a21124 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2112,6 +2112,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.sonarr.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.ssdp.*] check_untyped_defs = true disallow_incomplete_defs = true From a809f645a7caaaf9a3c4b3bcd74175d81e3d5a36 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 7 Oct 2022 20:53:34 +0200 Subject: [PATCH 232/985] Add strict typing for radarr (#79242) --- .strict-typing | 1 + homeassistant/components/radarr/__init__.py | 15 ++++--- .../components/radarr/binary_sensor.py | 4 +- .../components/radarr/coordinator.py | 20 ++++----- homeassistant/components/radarr/sensor.py | 42 +++++++++---------- mypy.ini | 10 +++++ 6 files changed, 55 insertions(+), 37 deletions(-) diff --git a/.strict-typing b/.strict-typing index 0a0f5a2496c..1f990bd81c5 100644 --- a/.strict-typing +++ b/.strict-typing @@ -210,6 +210,7 @@ homeassistant.components.pvoutput.* homeassistant.components.qnap_qsw.* homeassistant.components.rainmachine.* homeassistant.components.rdw.* +homeassistant.components.radarr.* homeassistant.components.recollect_waste.* homeassistant.components.recorder.* homeassistant.components.remote.* diff --git a/homeassistant/components/radarr/__init__.py b/homeassistant/components/radarr/__init__.py index 5e32f64b7ad..403bedda94a 100644 --- a/homeassistant/components/radarr/__init__.py +++ b/homeassistant/components/radarr/__init__.py @@ -1,6 +1,8 @@ """The Radarr component.""" from __future__ import annotations +from typing import Any, cast + from aiopyarr.models.host_configuration import PyArrHostConfiguration from aiopyarr.radarr_client import RadarrClient @@ -29,6 +31,7 @@ from .coordinator import ( MoviesDataUpdateCoordinator, RadarrDataUpdateCoordinator, StatusDataUpdateCoordinator, + T, ) PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -65,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: host_configuration=host_configuration, session=async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL]), ) - coordinators: dict[str, RadarrDataUpdateCoordinator] = { + coordinators: dict[str, RadarrDataUpdateCoordinator[Any]] = { "status": StatusDataUpdateCoordinator(hass, host_configuration, radarr), "disk_space": DiskSpaceDataUpdateCoordinator(hass, host_configuration, radarr), "health": HealthDataUpdateCoordinator(hass, host_configuration, radarr), @@ -86,15 +89,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class RadarrEntity(CoordinatorEntity[RadarrDataUpdateCoordinator]): +class RadarrEntity(CoordinatorEntity[RadarrDataUpdateCoordinator[T]]): """Defines a base Radarr entity.""" _attr_has_entity_name = True - coordinator: RadarrDataUpdateCoordinator + coordinator: RadarrDataUpdateCoordinator[T] def __init__( self, - coordinator: RadarrDataUpdateCoordinator, + coordinator: RadarrDataUpdateCoordinator[T], description: EntityDescription, ) -> None: """Create Radarr entity.""" @@ -113,5 +116,7 @@ class RadarrEntity(CoordinatorEntity[RadarrDataUpdateCoordinator]): name=self.coordinator.config_entry.title, ) if isinstance(self.coordinator, StatusDataUpdateCoordinator): - device_info[ATTR_SW_VERSION] = self.coordinator.data.version + device_info[ATTR_SW_VERSION] = cast( + StatusDataUpdateCoordinator, self.coordinator + ).data.version return device_info diff --git a/homeassistant/components/radarr/binary_sensor.py b/homeassistant/components/radarr/binary_sensor.py index 2a1a729e6f4..3952a694e94 100644 --- a/homeassistant/components/radarr/binary_sensor.py +++ b/homeassistant/components/radarr/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Radarr binary sensors.""" from __future__ import annotations +from aiopyarr import Health + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -32,7 +34,7 @@ async def async_setup_entry( async_add_entities([RadarrBinarySensor(coordinator, BINARY_SENSOR_TYPE)]) -class RadarrBinarySensor(RadarrEntity, BinarySensorEntity): +class RadarrBinarySensor(RadarrEntity[list[Health]], BinarySensorEntity): """Implementation of a Radarr binary sensor.""" @property diff --git a/homeassistant/components/radarr/coordinator.py b/homeassistant/components/radarr/coordinator.py index 06ea32e790f..dfcd1e3a269 100644 --- a/homeassistant/components/radarr/coordinator.py +++ b/homeassistant/components/radarr/coordinator.py @@ -3,9 +3,9 @@ from __future__ import annotations from abc import abstractmethod from datetime import timedelta -from typing import Generic, TypeVar, cast +from typing import Generic, TypeVar, Union, cast -from aiopyarr import Health, RootFolder, SystemStatus, exceptions +from aiopyarr import Health, RadarrMovie, RootFolder, SystemStatus, exceptions from aiopyarr.models.host_configuration import PyArrHostConfiguration from aiopyarr.radarr_client import RadarrClient @@ -16,10 +16,10 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DOMAIN, LOGGER -T = TypeVar("T", SystemStatus, list[RootFolder], list[Health], int) +T = TypeVar("T", bound=Union[SystemStatus, list[RootFolder], list[Health], int]) -class RadarrDataUpdateCoordinator(DataUpdateCoordinator, Generic[T]): +class RadarrDataUpdateCoordinator(DataUpdateCoordinator[T], Generic[T]): """Data update coordinator for the Radarr integration.""" config_entry: ConfigEntry @@ -58,7 +58,7 @@ class RadarrDataUpdateCoordinator(DataUpdateCoordinator, Generic[T]): raise NotImplementedError -class StatusDataUpdateCoordinator(RadarrDataUpdateCoordinator): +class StatusDataUpdateCoordinator(RadarrDataUpdateCoordinator[SystemStatus]): """Status update coordinator for Radarr.""" async def _fetch_data(self) -> SystemStatus: @@ -66,15 +66,15 @@ class StatusDataUpdateCoordinator(RadarrDataUpdateCoordinator): return await self.api_client.async_get_system_status() -class DiskSpaceDataUpdateCoordinator(RadarrDataUpdateCoordinator): +class DiskSpaceDataUpdateCoordinator(RadarrDataUpdateCoordinator[list[RootFolder]]): """Disk space update coordinator for Radarr.""" async def _fetch_data(self) -> list[RootFolder]: """Fetch the data.""" - return cast(list, await self.api_client.async_get_root_folders()) + return cast(list[RootFolder], await self.api_client.async_get_root_folders()) -class HealthDataUpdateCoordinator(RadarrDataUpdateCoordinator): +class HealthDataUpdateCoordinator(RadarrDataUpdateCoordinator[list[Health]]): """Health update coordinator.""" async def _fetch_data(self) -> list[Health]: @@ -82,9 +82,9 @@ class HealthDataUpdateCoordinator(RadarrDataUpdateCoordinator): return await self.api_client.async_get_failed_health_checks() -class MoviesDataUpdateCoordinator(RadarrDataUpdateCoordinator): +class MoviesDataUpdateCoordinator(RadarrDataUpdateCoordinator[int]): """Movies update coordinator.""" async def _fetch_data(self) -> int: """Fetch the movies data.""" - return len(cast(list, await self.api_client.async_get_movies())) + return len(cast(list[RadarrMovie], await self.api_client.async_get_movies())) diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index e424844c602..27d1a5487a2 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -4,10 +4,10 @@ from __future__ import annotations from collections.abc import Callable from copy import deepcopy from dataclasses import dataclass -from datetime import timezone -from typing import Generic +from datetime import datetime, timezone +from typing import Any, Generic -from aiopyarr import Diskspace, RootFolder +from aiopyarr import Diskspace, RootFolder, SystemStatus import voluptuous as vol from homeassistant.components.sensor import ( @@ -32,7 +32,7 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import RadarrEntity from .const import DOMAIN @@ -50,8 +50,8 @@ def get_space(data: list[Diskspace], name: str) -> str: def get_modified_description( - description: RadarrSensorEntityDescription, mount: RootFolder -) -> tuple[RadarrSensorEntityDescription, str]: + description: RadarrSensorEntityDescription[T], mount: RootFolder +) -> tuple[RadarrSensorEntityDescription[T], str]: """Return modified description and folder name.""" desc = deepcopy(description) name = mount.path.rsplit("/")[-1].rsplit("\\")[-1] @@ -64,7 +64,7 @@ def get_modified_description( class RadarrSensorEntityDescriptionMixIn(Generic[T]): """Mixin for required keys.""" - value_fn: Callable[[T, str], str] + value_fn: Callable[[T, str], str | int | datetime] @dataclass @@ -74,12 +74,12 @@ class RadarrSensorEntityDescription( """Class to describe a Radarr sensor.""" description_fn: Callable[ - [RadarrSensorEntityDescription, RootFolder], - tuple[RadarrSensorEntityDescription, str] | None, - ] = lambda _, __: None + [RadarrSensorEntityDescription[T], RootFolder], + tuple[RadarrSensorEntityDescription[T], str] | None, + ] | None = None -SENSOR_TYPES: dict[str, RadarrSensorEntityDescription] = { +SENSOR_TYPES: dict[str, RadarrSensorEntityDescription[Any]] = { "disk_space": RadarrSensorEntityDescription( key="disk_space", name="Disk space", @@ -88,7 +88,7 @@ SENSOR_TYPES: dict[str, RadarrSensorEntityDescription] = { value_fn=get_space, description_fn=get_modified_description, ), - "movie": RadarrSensorEntityDescription( + "movie": RadarrSensorEntityDescription[int]( key="movies", name="Movies", native_unit_of_measurement="Movies", @@ -96,7 +96,7 @@ SENSOR_TYPES: dict[str, RadarrSensorEntityDescription] = { entity_registry_enabled_default=False, value_fn=lambda data, _: data, ), - "status": RadarrSensorEntityDescription( + "status": RadarrSensorEntityDescription[SystemStatus]( key="start_time", name="Start time", device_class=SensorDeviceClass.TIMESTAMP, @@ -152,10 +152,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Radarr sensors based on a config entry.""" - coordinators: dict[str, RadarrDataUpdateCoordinator] = hass.data[DOMAIN][ + coordinators: dict[str, RadarrDataUpdateCoordinator[Any]] = hass.data[DOMAIN][ entry.entry_id ] - entities = [] + entities: list[RadarrSensor[Any]] = [] for coordinator_type, description in SENSOR_TYPES.items(): coordinator = coordinators[coordinator_type] if coordinator_type != "disk_space": @@ -169,16 +169,16 @@ async def async_setup_entry( async_add_entities(entities) -class RadarrSensor(RadarrEntity, SensorEntity): +class RadarrSensor(RadarrEntity[T], SensorEntity): """Implementation of the Radarr sensor.""" - coordinator: RadarrDataUpdateCoordinator - entity_description: RadarrSensorEntityDescription + coordinator: RadarrDataUpdateCoordinator[T] + entity_description: RadarrSensorEntityDescription[T] def __init__( self, - coordinator: RadarrDataUpdateCoordinator, - description: RadarrSensorEntityDescription, + coordinator: RadarrDataUpdateCoordinator[T], + description: RadarrSensorEntityDescription[T], folder_name: str = "", ) -> None: """Create Radarr entity.""" @@ -186,6 +186,6 @@ class RadarrSensor(RadarrEntity, SensorEntity): self.folder_name = folder_name @property - def native_value(self) -> StateType: + def native_value(self) -> str | int | datetime: """Return the state of the sensor.""" return self.entity_description.value_fn(self.coordinator.data, self.folder_name) diff --git a/mypy.ini b/mypy.ini index 9b748a21124..31a5268abcf 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1852,6 +1852,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.radarr.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.recollect_waste.*] check_untyped_defs = true disallow_incomplete_defs = true From 14d2bbfcd63ff6d55057abd3bc4b2c8ef199a958 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 7 Oct 2022 20:54:29 +0200 Subject: [PATCH 233/985] Add strict typing for lidarr (#79241) --- .strict-typing | 1 + homeassistant/components/lidarr/__init__.py | 11 +++-- .../components/lidarr/coordinator.py | 20 ++++---- homeassistant/components/lidarr/sensor.py | 48 +++++++++---------- mypy.ini | 10 ++++ 5 files changed, 53 insertions(+), 37 deletions(-) diff --git a/.strict-typing b/.strict-typing index 1f990bd81c5..13b2752e09c 100644 --- a/.strict-typing +++ b/.strict-typing @@ -162,6 +162,7 @@ homeassistant.components.lacrosse_view.* homeassistant.components.lametric.* homeassistant.components.laundrify.* homeassistant.components.lcn.* +homeassistant.components.lidarr.* homeassistant.components.lifx.* homeassistant.components.light.* homeassistant.components.litterrobot.* diff --git a/homeassistant/components/lidarr/__init__.py b/homeassistant/components/lidarr/__init__.py index 7fd3e799d88..9222164227b 100644 --- a/homeassistant/components/lidarr/__init__.py +++ b/homeassistant/components/lidarr/__init__.py @@ -1,6 +1,8 @@ """The Lidarr component.""" from __future__ import annotations +from typing import Any + from aiopyarr.lidarr_client import LidarrClient from aiopyarr.models.host_configuration import PyArrHostConfiguration @@ -18,6 +20,7 @@ from .coordinator import ( LidarrDataUpdateCoordinator, QueueDataUpdateCoordinator, StatusDataUpdateCoordinator, + T, WantedDataUpdateCoordinator, ) @@ -36,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session=async_get_clientsession(hass, host_configuration.verify_ssl), request_timeout=60, ) - coordinators: dict[str, LidarrDataUpdateCoordinator] = { + coordinators: dict[str, LidarrDataUpdateCoordinator[Any]] = { "disk_space": DiskSpaceDataUpdateCoordinator(hass, host_configuration, lidarr), "queue": QueueDataUpdateCoordinator(hass, host_configuration, lidarr), "status": StatusDataUpdateCoordinator(hass, host_configuration, lidarr), @@ -63,13 +66,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class LidarrEntity(CoordinatorEntity[LidarrDataUpdateCoordinator]): +class LidarrEntity(CoordinatorEntity[LidarrDataUpdateCoordinator[T]]): """Defines a base Lidarr entity.""" _attr_has_entity_name = True def __init__( - self, coordinator: LidarrDataUpdateCoordinator, description: EntityDescription + self, + coordinator: LidarrDataUpdateCoordinator[T], + description: EntityDescription, ) -> None: """Initialize the Lidarr entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/lidarr/coordinator.py b/homeassistant/components/lidarr/coordinator.py index be789c6a32a..c02d6525871 100644 --- a/homeassistant/components/lidarr/coordinator.py +++ b/homeassistant/components/lidarr/coordinator.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import abstractmethod from datetime import timedelta -from typing import Generic, TypeVar, cast +from typing import Generic, TypeVar, Union, cast from aiopyarr import LidarrAlbum, LidarrQueue, LidarrRootFolder, exceptions from aiopyarr.lidarr_client import LidarrClient @@ -16,10 +16,10 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DEFAULT_MAX_RECORDS, DOMAIN, LOGGER -T = TypeVar("T", list[LidarrRootFolder], LidarrQueue, str, LidarrAlbum) +T = TypeVar("T", bound=Union[list[LidarrRootFolder], LidarrQueue, str, LidarrAlbum]) -class LidarrDataUpdateCoordinator(DataUpdateCoordinator, Generic[T]): +class LidarrDataUpdateCoordinator(DataUpdateCoordinator[T], Generic[T]): """Data update coordinator for the Lidarr integration.""" config_entry: ConfigEntry @@ -59,15 +59,19 @@ class LidarrDataUpdateCoordinator(DataUpdateCoordinator, Generic[T]): raise NotImplementedError -class DiskSpaceDataUpdateCoordinator(LidarrDataUpdateCoordinator): +class DiskSpaceDataUpdateCoordinator( + LidarrDataUpdateCoordinator[list[LidarrRootFolder]] +): """Disk space update coordinator for Lidarr.""" async def _fetch_data(self) -> list[LidarrRootFolder]: """Fetch the data.""" - return cast(list, await self.api_client.async_get_root_folders()) + return cast( + list[LidarrRootFolder], await self.api_client.async_get_root_folders() + ) -class QueueDataUpdateCoordinator(LidarrDataUpdateCoordinator): +class QueueDataUpdateCoordinator(LidarrDataUpdateCoordinator[LidarrQueue]): """Queue update coordinator.""" async def _fetch_data(self) -> LidarrQueue: @@ -75,7 +79,7 @@ class QueueDataUpdateCoordinator(LidarrDataUpdateCoordinator): return await self.api_client.async_get_queue(page_size=DEFAULT_MAX_RECORDS) -class StatusDataUpdateCoordinator(LidarrDataUpdateCoordinator): +class StatusDataUpdateCoordinator(LidarrDataUpdateCoordinator[str]): """Status update coordinator for Lidarr.""" async def _fetch_data(self) -> str: @@ -83,7 +87,7 @@ class StatusDataUpdateCoordinator(LidarrDataUpdateCoordinator): return (await self.api_client.async_get_system_status()).version -class WantedDataUpdateCoordinator(LidarrDataUpdateCoordinator): +class WantedDataUpdateCoordinator(LidarrDataUpdateCoordinator[LidarrAlbum]): """Wanted update coordinator.""" async def _fetch_data(self) -> LidarrAlbum: diff --git a/homeassistant/components/lidarr/sensor.py b/homeassistant/components/lidarr/sensor.py index 8529d9a6469..2e5f9bb710f 100644 --- a/homeassistant/components/lidarr/sensor.py +++ b/homeassistant/components/lidarr/sensor.py @@ -4,10 +4,9 @@ from __future__ import annotations from collections.abc import Callable from copy import deepcopy from dataclasses import dataclass -from datetime import datetime -from typing import Generic +from typing import Any, Generic -from aiopyarr import LidarrQueueItem, LidarrRootFolder +from aiopyarr import LidarrQueue, LidarrQueueItem, LidarrRootFolder from homeassistant.components.sensor import ( SensorEntity, @@ -18,7 +17,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_GIGABYTES from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType from . import LidarrEntity from .const import BYTE_SIZES, DOMAIN @@ -27,7 +25,7 @@ from .coordinator import LidarrDataUpdateCoordinator, T def get_space(data: list[LidarrRootFolder], name: str) -> str: """Get space.""" - space = [] + space: list[float] = [] for mount in data: if name in mount.path: mount.freeSpace = mount.freeSpace if mount.accessible else 0 @@ -36,8 +34,8 @@ def get_space(data: list[LidarrRootFolder], name: str) -> str: def get_modified_description( - description: LidarrSensorEntityDescription, mount: LidarrRootFolder -) -> tuple[LidarrSensorEntityDescription, str]: + description: LidarrSensorEntityDescription[T], mount: LidarrRootFolder +) -> tuple[LidarrSensorEntityDescription[T], str]: """Return modified description and folder name.""" desc = deepcopy(description) name = mount.path.rsplit("/")[-1].rsplit("\\")[-1] @@ -50,25 +48,23 @@ def get_modified_description( class LidarrSensorEntityDescriptionMixIn(Generic[T]): """Mixin for required keys.""" - value_fn: Callable[[T, str], str] + value_fn: Callable[[T, str], str | int] @dataclass class LidarrSensorEntityDescription( - SensorEntityDescription, LidarrSensorEntityDescriptionMixIn, Generic[T] + SensorEntityDescription, LidarrSensorEntityDescriptionMixIn[T], Generic[T] ): """Class to describe a Lidarr sensor.""" - attributes_fn: Callable[ - [T], dict[str, StateType | datetime] | None - ] = lambda _: None + attributes_fn: Callable[[T], dict[str, str] | None] = lambda _: None description_fn: Callable[ - [LidarrSensorEntityDescription, LidarrRootFolder], - tuple[LidarrSensorEntityDescription, str] | None, - ] = lambda _, __: None + [LidarrSensorEntityDescription[T], LidarrRootFolder], + tuple[LidarrSensorEntityDescription[T], str] | None, + ] | None = None -SENSOR_TYPES: dict[str, LidarrSensorEntityDescription] = { +SENSOR_TYPES: dict[str, LidarrSensorEntityDescription[Any]] = { "disk_space": LidarrSensorEntityDescription( key="disk_space", name="Disk space", @@ -78,7 +74,7 @@ SENSOR_TYPES: dict[str, LidarrSensorEntityDescription] = { state_class=SensorStateClass.TOTAL, description_fn=get_modified_description, ), - "queue": LidarrSensorEntityDescription( + "queue": LidarrSensorEntityDescription[LidarrQueue]( key="queue", name="Queue", native_unit_of_measurement="Albums", @@ -87,7 +83,7 @@ SENSOR_TYPES: dict[str, LidarrSensorEntityDescription] = { state_class=SensorStateClass.TOTAL, attributes_fn=lambda data: {i.title: queue_str(i) for i in data.records}, ), - "wanted": LidarrSensorEntityDescription( + "wanted": LidarrSensorEntityDescription[LidarrQueue]( key="wanted", name="Wanted", native_unit_of_measurement="Albums", @@ -108,10 +104,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Lidarr sensors based on a config entry.""" - coordinators: dict[str, LidarrDataUpdateCoordinator] = hass.data[DOMAIN][ + coordinators: dict[str, LidarrDataUpdateCoordinator[Any]] = hass.data[DOMAIN][ entry.entry_id ] - entities = [] + entities: list[LidarrSensor[Any]] = [] for coordinator_type, description in SENSOR_TYPES.items(): coordinator = coordinators[coordinator_type] if coordinator_type != "disk_space": @@ -125,15 +121,15 @@ async def async_setup_entry( async_add_entities(entities) -class LidarrSensor(LidarrEntity, SensorEntity): +class LidarrSensor(LidarrEntity[T], SensorEntity): """Implementation of the Lidarr sensor.""" - entity_description: LidarrSensorEntityDescription + entity_description: LidarrSensorEntityDescription[T] def __init__( self, - coordinator: LidarrDataUpdateCoordinator, - description: LidarrSensorEntityDescription, + coordinator: LidarrDataUpdateCoordinator[T], + description: LidarrSensorEntityDescription[T], folder_name: str = "", ) -> None: """Create Lidarr entity.""" @@ -141,12 +137,12 @@ class LidarrSensor(LidarrEntity, SensorEntity): self.folder_name = folder_name @property - def extra_state_attributes(self) -> dict[str, StateType | datetime] | None: + def extra_state_attributes(self) -> dict[str, str] | None: """Return the state attributes of the sensor.""" return self.entity_description.attributes_fn(self.coordinator.data) @property - def native_value(self) -> StateType: + def native_value(self) -> str | int: """Return the state of the sensor.""" return self.entity_description.value_fn(self.coordinator.data, self.folder_name) diff --git a/mypy.ini b/mypy.ini index 31a5268abcf..c83f6cd048e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1372,6 +1372,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.lidarr.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.lifx.*] check_untyped_defs = true disallow_incomplete_defs = true From 33c94b005291643ca78aac3ee07a63d80d85eab7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 7 Oct 2022 20:56:29 +0200 Subject: [PATCH 234/985] Add strict typing for WLED (#79822) * Add strict typing for WLED * Update backoff constraint --- .strict-typing | 1 + homeassistant/components/wled/number.py | 2 +- homeassistant/package_constraints.txt | 5 ++--- mypy.ini | 10 ++++++++++ script/gen_requirements_all.py | 5 ++--- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.strict-typing b/.strict-typing index 13b2752e09c..2c193bb5173 100644 --- a/.strict-typing +++ b/.strict-typing @@ -285,6 +285,7 @@ homeassistant.components.websocket_api.* homeassistant.components.wemo.* homeassistant.components.whois.* homeassistant.components.wiz.* +homeassistant.components.wled.* homeassistant.components.worldclock.* homeassistant.components.yale_smart_alarm.* homeassistant.components.zeroconf.* diff --git a/homeassistant/components/wled/number.py b/homeassistant/components/wled/number.py index 33b27777c7e..60317003f19 100644 --- a/homeassistant/components/wled/number.py +++ b/homeassistant/components/wled/number.py @@ -92,7 +92,7 @@ class WLEDNumber(WLEDEntity, NumberEntity): @property def native_value(self) -> float | None: """Return the current WLED segment number value.""" - return getattr( + return getattr( # type: ignore[no-any-return] self.coordinator.data.state.segments[self._segment], self.entity_description.key, ) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 56700d017c3..38ff17b38c7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -114,9 +114,8 @@ multidict>=6.0.2 # https://github.com/home-assistant/core/pull/68176 authlib<1.0 -# Pin backoff for compatibility until most libraries have been updated -# https://github.com/home-assistant/core/pull/70817 -backoff<2.0 +# Version 2.0 added typing, prevent accidental fallbacks +backoff>=2.0 # Breaking change in version # https://github.com/samuelcolvin/pydantic/issues/4092 diff --git a/mypy.ini b/mypy.ini index c83f6cd048e..b63931d6c79 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2604,6 +2604,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.wled.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.worldclock.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index d0eb830f088..dc55b37b956 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -124,9 +124,8 @@ multidict>=6.0.2 # https://github.com/home-assistant/core/pull/68176 authlib<1.0 -# Pin backoff for compatibility until most libraries have been updated -# https://github.com/home-assistant/core/pull/70817 -backoff<2.0 +# Version 2.0 added typing, prevent accidental fallbacks +backoff>=2.0 # Breaking change in version # https://github.com/samuelcolvin/pydantic/issues/4092 From 447b71341ff3c5e0e40f35a5eda064ae7b7d37d6 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Fri, 7 Oct 2022 20:57:12 +0200 Subject: [PATCH 235/985] Bump plugwise to v0.21.4 (#79831) --- 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 9c8ea6f3be7..1b17f3e49f5 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==0.21.3"], + "requirements": ["plugwise==0.21.4"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index b5a2fd74bc9..ea4d3b36d59 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1309,7 +1309,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.21.3 +plugwise==0.21.4 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc85a9bda61..369450d1517 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -936,7 +936,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.21.3 +plugwise==0.21.4 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 From a18a0b39dd2e6599741633c27ee8c43b488cc460 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Fri, 7 Oct 2022 21:00:01 +0200 Subject: [PATCH 236/985] Bumb velbusaio to 2022.10.3 (#79821) * Bumb velbusaio to 2022.10.3 * Handle the possibility that get_cover position is None (unknown) in previous versions this was always 0 --- homeassistant/components/velbus/cover.py | 5 ++++- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py index 3ac66147bb6..782d8d3c81b 100644 --- a/homeassistant/components/velbus/cover.py +++ b/homeassistant/components/velbus/cover.py @@ -66,7 +66,10 @@ class VelbusCover(VelbusEntity, CoverEntity): None is unknown, 0 is closed, 100 is fully open Velbus: 100 = closed, 0 = open """ - return 100 - self._channel.get_position() + pos = self._channel.get_position() + if pos is not None: + return 100 - pos + return None async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 1a5d78d24d6..1acc65b898d 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": ["velbus-aio==2022.10.2"], + "requirements": ["velbus-aio==2022.10.3"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "dependencies": ["usb"], diff --git a/requirements_all.txt b/requirements_all.txt index ea4d3b36d59..b71c8941011 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2475,7 +2475,7 @@ vallox-websocket-api==2.12.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.10.2 +velbus-aio==2022.10.3 # homeassistant.components.venstar venstarcolortouch==0.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 369450d1517..99d2130dc41 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1709,7 +1709,7 @@ vallox-websocket-api==2.12.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.10.2 +velbus-aio==2022.10.3 # homeassistant.components.venstar venstarcolortouch==0.18 From 9d351a3c10fa1120768588ad4cbacf13dc7d5652 Mon Sep 17 00:00:00 2001 From: HarvsG <11440490+HarvsG@users.noreply.github.com> Date: Fri, 7 Oct 2022 21:23:25 +0100 Subject: [PATCH 237/985] Improve typing and code quality in beyesian (#79603) * strict typing * Detail implication * adds newline * don't change indenting * really dont change indenting * Update homeassistant/components/bayesian/binary_sensor.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * typing in async_setup_platform() + remove arg * less ambiguity * mypy thinks Literal[False] otherwise * clearer log * don't use `and` assignments * observations not values * clarify can be None * observation can't be none * assert we have at least one * make it clearer where we're using UUIDs * remove unnecessary bool Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Unnecessary None handling Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Better type setting Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Reccomended changes. * remove if statement not needed * Not strict until _TrackTemplateResultInfo fixed Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/bayesian/binary_sensor.py | 147 ++++++++++-------- homeassistant/components/bayesian/helpers.py | 7 +- homeassistant/components/bayesian/repairs.py | 7 +- 3 files changed, 94 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 190fb889553..1d2674255f9 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -2,12 +2,18 @@ from __future__ import annotations from collections import OrderedDict +from collections.abc import Callable import logging from typing import Any +from uuid import UUID import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA, + BinarySensorDeviceClass, + BinarySensorEntity, +) from homeassistant.const import ( CONF_ABOVE, CONF_BELOW, @@ -20,18 +26,19 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConditionError, TemplateError from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( TrackTemplate, + TrackTemplateResult, 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 homeassistant.helpers.template import Template, result_as_boolean from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DOMAIN, PLATFORMS @@ -107,7 +114,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def update_probability(prior, prob_given_true, prob_given_false): +def update_probability( + prior: float, prob_given_true: float, prob_given_false: float +) -> float: """Update probability using Bayes' rule.""" numerator = prob_given_true * prior denominator = numerator + prob_given_false * (1 - prior) @@ -123,18 +132,18 @@ async def async_setup_platform( """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] - probability_threshold = config[CONF_PROBABILITY_THRESHOLD] - device_class = config.get(CONF_DEVICE_CLASS) + name: str = config[CONF_NAME] + observations: list[ConfigType] = config[CONF_OBSERVATIONS] + prior: float = config[CONF_PRIOR] + probability_threshold: float = config[CONF_PROBABILITY_THRESHOLD] + device_class: BinarySensorDeviceClass | None = config.get(CONF_DEVICE_CLASS) # Should deprecate in some future version (2022.10 at time of writing) & make prob_given_false required in schemas. broken_observations: list[dict[str, Any]] = [] for observation in observations: if CONF_P_GIVEN_F not in observation: text: str = f"{name}/{observation.get(CONF_ENTITY_ID,'')}{observation.get(CONF_VALUE_TEMPLATE,'')}" - raise_no_prob_given_false(hass, observation, text) + raise_no_prob_given_false(hass, text) _LOGGER.error("Missing prob_given_false YAML entry for %s", text) broken_observations.append(observation) observations = [x for x in observations if x not in broken_observations] @@ -153,7 +162,14 @@ class BayesianBinarySensor(BinarySensorEntity): _attr_should_poll = False - def __init__(self, name, prior, observations, probability_threshold, device_class): + def __init__( + self, + name: str, + prior: float, + observations: list[ConfigType], + probability_threshold: float, + device_class: BinarySensorDeviceClass | None, + ) -> None: """Initialize the Bayesian sensor.""" self._attr_name = name self._observations = [ @@ -173,17 +189,17 @@ class BayesianBinarySensor(BinarySensorEntity): self._probability_threshold = probability_threshold self._attr_device_class = device_class self._attr_is_on = False - self._callbacks = [] + self._callbacks: list = [] self.prior = prior self.probability = prior - self.current_observations = OrderedDict({}) + self.current_observations: OrderedDict[UUID, Observation] = OrderedDict({}) self.observations_by_entity = self._build_observations_by_entity() self.observations_by_template = self._build_observations_by_template() - self.observation_handlers = { + self.observation_handlers: dict[str, Callable[[Observation], bool | None]] = { "numeric_state": self._process_numeric_state, "state": self._process_state, "multi_state": self._process_multi_state, @@ -205,7 +221,7 @@ class BayesianBinarySensor(BinarySensorEntity): """ @callback - def async_threshold_sensor_state_listener(event): + def async_threshold_sensor_state_listener(event: Event) -> None: """ Handle sensor state changes. @@ -213,7 +229,7 @@ class BayesianBinarySensor(BinarySensorEntity): then calculate the new probability. """ - entity = event.data.get("entity_id") + entity: str = event.data[CONF_ENTITY_ID] self.current_observations.update(self._record_entity_observations(entity)) self.async_set_context(event.context) @@ -228,11 +244,15 @@ class BayesianBinarySensor(BinarySensorEntity): ) @callback - def _async_template_result_changed(event, updates): + def _async_template_result_changed( + event: Event | None, updates: list[TrackTemplateResult] + ) -> None: track_template_result = updates.pop() template = track_template_result.template result = track_template_result.result - entity = event and event.data.get("entity_id") + entity: str | None = ( + None if event is None else event.data.get(CONF_ENTITY_ID) + ) if isinstance(result, TemplateError): _LOGGER.error( "TemplateError('%s') " @@ -252,7 +272,7 @@ class BayesianBinarySensor(BinarySensorEntity): # in some cases a template may update because of the absence of an entity if entity is not None: - observation.entity_id = str(entity) + observation.entity_id = entity self.current_observations[observation.id] = observation @@ -273,7 +293,7 @@ class BayesianBinarySensor(BinarySensorEntity): self.current_observations.update(self._initialize_current_observations()) self.probability = self._calculate_new_probability() - self._attr_is_on = bool(self.probability >= self._probability_threshold) + self._attr_is_on = self.probability >= self._probability_threshold # detect mirrored entries for entity, observations in self.observations_by_entity.items(): @@ -281,9 +301,9 @@ class BayesianBinarySensor(BinarySensorEntity): self.hass, observations, text=f"{self._attr_name}/{entity}" ) - all_template_observations = [] - for value in self.observations_by_template.values(): - all_template_observations.append(value[0]) + all_template_observations: list[Observation] = [] + for observations in self.observations_by_template.values(): + all_template_observations.append(observations[0]) if len(all_template_observations) == 2: raise_mirrored_entries( self.hass, @@ -292,62 +312,63 @@ class BayesianBinarySensor(BinarySensorEntity): ) @callback - def _recalculate_and_write_state(self): + def _recalculate_and_write_state(self) -> None: self.probability = self._calculate_new_probability() self._attr_is_on = bool(self.probability >= self._probability_threshold) self.async_write_ha_state() - def _initialize_current_observations(self): - local_observations = OrderedDict({}) - + def _initialize_current_observations(self) -> OrderedDict[UUID, Observation]: + local_observations: OrderedDict[UUID, Observation] = OrderedDict({}) for entity in self.observations_by_entity: local_observations.update(self._record_entity_observations(entity)) return local_observations - def _record_entity_observations(self, entity): - local_observations = OrderedDict({}) + def _record_entity_observations( + self, entity: str + ) -> OrderedDict[UUID, Observation]: + local_observations: OrderedDict[UUID, Observation] = OrderedDict({}) for observation in self.observations_by_entity[entity]: platform = observation.platform - observed = self.observation_handlers[platform](observation) - observation.observed = observed + observation.observed = self.observation_handlers[platform](observation) local_observations[observation.id] = observation return local_observations - def _calculate_new_probability(self): + def _calculate_new_probability(self) -> float: prior = self.prior for observation in self.current_observations.values(): - if observation is not None: - if observation.observed is True: - prior = update_probability( - prior, - observation.prob_given_true, - observation.prob_given_false, - ) - elif observation.observed is False: - prior = update_probability( - prior, - 1 - observation.prob_given_true, - 1 - observation.prob_given_false, - ) - elif observation.observed is None: - if observation.entity_id is not None: - _LOGGER.debug( - "Observation for entity '%s' returned None, it will not be used for Bayesian updating", - observation.entity_id, - ) - else: - _LOGGER.debug( - "Observation for template entity returned None rather than a valid boolean, it will not be used for Bayesian updating", - ) - + if observation.observed is True: + prior = update_probability( + prior, + observation.prob_given_true, + observation.prob_given_false, + ) + continue + if observation.observed is False: + prior = update_probability( + prior, + 1 - observation.prob_given_true, + 1 - observation.prob_given_false, + ) + continue + # observation.observed is None + if observation.entity_id is not None: + _LOGGER.debug( + "Observation for entity '%s' returned None, it will not be used for Bayesian updating", + observation.entity_id, + ) + continue + _LOGGER.debug( + "Observation for template entity returned None rather than a valid boolean, it will not be used for Bayesian updating", + ) + # the prior has been updated and is now the posterior return prior - def _build_observations_by_entity(self): + def _build_observations_by_entity(self) -> dict[str, list[Observation]]: """ Build and return data structure of the form below. @@ -378,7 +399,7 @@ class BayesianBinarySensor(BinarySensorEntity): return observations_by_entity - def _build_observations_by_template(self): + def _build_observations_by_template(self) -> dict[Template, list[Observation]]: """ Build and return data structure of the form below. @@ -392,7 +413,7 @@ class BayesianBinarySensor(BinarySensorEntity): for all relevant observations to be looked up via their `template`. """ - observations_by_template = {} + observations_by_template: dict[Template, list[Observation]] = {} for observation in self._observations: if observation.value_template is None: continue @@ -402,7 +423,7 @@ class BayesianBinarySensor(BinarySensorEntity): return observations_by_template - def _process_numeric_state(self, entity_observation): + def _process_numeric_state(self, entity_observation: Observation) -> bool | None: """Return True if numeric condition is met, return False if not, return None otherwise.""" entity = entity_observation.entity_id @@ -420,7 +441,7 @@ class BayesianBinarySensor(BinarySensorEntity): except ConditionError: return None - def _process_state(self, entity_observation): + def _process_state(self, entity_observation: Observation) -> bool | None: """Return True if state conditions are met, return False if they are not. Returns None if the state is unavailable. @@ -436,7 +457,7 @@ class BayesianBinarySensor(BinarySensorEntity): except ConditionError: return None - def _process_multi_state(self, entity_observation): + def _process_multi_state(self, entity_observation: Observation) -> bool | None: """Return True if state conditions are met, otherwise return None. Never return False as all other states should have their own probabilities configured. @@ -452,7 +473,7 @@ class BayesianBinarySensor(BinarySensorEntity): return None @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the sensor.""" return { diff --git a/homeassistant/components/bayesian/helpers.py b/homeassistant/components/bayesian/helpers.py index 22c5d518b46..6e78de63607 100644 --- a/homeassistant/components/bayesian/helpers.py +++ b/homeassistant/components/bayesian/helpers.py @@ -18,7 +18,10 @@ from .const import CONF_P_GIVEN_F, CONF_P_GIVEN_T, CONF_TO_STATE @dataclass class Observation: - """Representation of a sensor or template observation.""" + """Representation of a sensor or template observation. + + Either entity_id or value_template should be non-None. + """ entity_id: str | None platform: str @@ -29,7 +32,7 @@ class Observation: below: float | None value_template: Template | None observed: bool | None = None - id: str = field(default_factory=lambda: str(uuid.uuid4())) + id: uuid.UUID = field(default_factory=uuid.uuid4) def to_dict(self) -> dict[str, str | float | bool | None]: """Represent Class as a Dict for easier serialization.""" diff --git a/homeassistant/components/bayesian/repairs.py b/homeassistant/components/bayesian/repairs.py index 2b04a6a6605..9a527636948 100644 --- a/homeassistant/components/bayesian/repairs.py +++ b/homeassistant/components/bayesian/repairs.py @@ -5,9 +5,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import issue_registry from . import DOMAIN +from .helpers import Observation -def raise_mirrored_entries(hass: HomeAssistant, observations, text: str = "") -> None: +def raise_mirrored_entries( + hass: HomeAssistant, observations: list[Observation], text: str = "" +) -> None: """If there are mirrored entries, the user is probably using a workaround for a patched bug.""" if len(observations) != 2: return @@ -26,7 +29,7 @@ def raise_mirrored_entries(hass: HomeAssistant, observations, text: str = "") -> # Should deprecate in some future version (2022.10 at time of writing) & make prob_given_false required in schemas. -def raise_no_prob_given_false(hass: HomeAssistant, observation, text: str) -> None: +def raise_no_prob_given_false(hass: HomeAssistant, text: str) -> None: """In previous 2022.9 and earlier, prob_given_false was optional and had a default version.""" issue_registry.async_create_issue( hass, From fca8586fb6e130c602cfcb11fbbea376575c3b0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Oct 2022 10:50:50 -1000 Subject: [PATCH 238/985] Bump dbus-fast to 1.29.1 (#79787) * Bump dbus-fast to 1.28.0 Performance improvements changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.26.0...v1.28.0 * bump again * bump for cleanups --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 53a4125625a..54a690f7085 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.26.0" + "dbus-fast==1.29.1" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 38ff17b38c7..7cfae5f2813 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.26.0 +dbus-fast==1.29.1 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index b71c8941011..6d524e0b88e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -543,7 +543,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.26.0 +dbus-fast==1.29.1 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99d2130dc41..d3c095951e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.26.0 +dbus-fast==1.29.1 # homeassistant.components.debugpy debugpy==1.6.3 From 61901a1a60343c42385fd71274a31159f61da7cf Mon Sep 17 00:00:00 2001 From: Julian Einwag Date: Fri, 7 Oct 2022 23:56:45 +0200 Subject: [PATCH 239/985] Add device trigger for Lidl Silvercrest switch to deCONZ (#79839) * add deconz support for Lidl Silvercrest switch * Update homeassistant/components/deconz/device_trigger.py Co-authored-by: Robert Svensson * Update homeassistant/components/deconz/device_trigger.py Co-authored-by: Robert Svensson * clarify it's a button, remove turn on event Co-authored-by: Robert Svensson --- homeassistant/components/deconz/device_trigger.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index e4d9a818a4e..8c63a47f59c 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -482,6 +482,12 @@ LIDL_SILVERCREST_DOORBELL = { (CONF_SHORT_PRESS, ""): {CONF_EVENT: 1002}, } +LIDL_SILVERCREST_BUTTON_REMOTE_MODEL = "TS004F" +LIDL_SILVERCREST_BUTTON_REMOTE = { + (CONF_SHORT_PRESS, ""): {CONF_EVENT: 1002}, + (CONF_DOUBLE_PRESS, ""): {CONF_EVENT: 1004}, +} + LIGHTIFIY_FOUR_BUTTON_REMOTE_MODEL = "Switch-LIGHTIFY" LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_MODEL = "Switch 4x-LIGHTIFY" LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_EU_MODEL = "Switch 4x EU-LIGHTIFY" @@ -607,6 +613,7 @@ REMOTES = { LEGRAND_ZGP_TOGGLE_SWITCH_MODEL: LEGRAND_ZGP_TOGGLE_SWITCH, LEGRAND_ZGP_SCENE_SWITCH_MODEL: LEGRAND_ZGP_SCENE_SWITCH, LIDL_SILVERCREST_DOORBELL_MODEL: LIDL_SILVERCREST_DOORBELL, + LIDL_SILVERCREST_BUTTON_REMOTE_MODEL: LIDL_SILVERCREST_BUTTON_REMOTE, LIGHTIFIY_FOUR_BUTTON_REMOTE_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE, LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE, LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_EU_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE, From 87a22fbccad9e9263d5e074aa0ca5681c56d9c53 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 7 Oct 2022 18:25:16 -0400 Subject: [PATCH 240/985] Move Sonarr API calls to coordinators (#79826) --- homeassistant/components/sonarr/__init__.py | 59 ++--- homeassistant/components/sonarr/const.py | 9 +- .../components/sonarr/coordinator.py | 147 +++++++++++ homeassistant/components/sonarr/entity.py | 37 ++- homeassistant/components/sonarr/sensor.py | 241 +++++------------- .../sonarr/fixtures/system-status.json | 1 + 6 files changed, 261 insertions(+), 233 deletions(-) create mode 100644 homeassistant/components/sonarr/coordinator.py diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index 4447425f42a..c592e8435c2 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -1,10 +1,8 @@ """The Sonarr component.""" from __future__ import annotations -from datetime import timedelta -import logging +from typing import Any -from aiopyarr import ArrAuthenticationException, ArrException from aiopyarr.models.host_configuration import PyArrHostConfiguration from aiopyarr.sonarr_client import SonarrClient @@ -19,24 +17,29 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( CONF_BASE_PATH, CONF_UPCOMING_DAYS, CONF_WANTED_MAX_ITEMS, - DATA_HOST_CONFIG, - DATA_SONARR, - DATA_SYSTEM_STATUS, DEFAULT_UPCOMING_DAYS, DEFAULT_WANTED_MAX_ITEMS, DOMAIN, + LOGGER, +) +from .coordinator import ( + CalendarDataUpdateCoordinator, + CommandsDataUpdateCoordinator, + DiskSpaceDataUpdateCoordinator, + QueueDataUpdateCoordinator, + SeriesDataUpdateCoordinator, + SonarrDataUpdateCoordinator, + StatusDataUpdateCoordinator, + WantedDataUpdateCoordinator, ) PLATFORMS = [Platform.SENSOR] -SCAN_INTERVAL = timedelta(seconds=30) -_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -57,30 +60,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: url=entry.data[CONF_URL], verify_ssl=entry.data[CONF_VERIFY_SSL], ) - sonarr = SonarrClient( host_configuration=host_configuration, session=async_get_clientsession(hass), ) - - try: - system_status = await sonarr.async_get_system_status() - except ArrAuthenticationException as err: - raise ConfigEntryAuthFailed( - "API Key is no longer valid. Please reauthenticate" - ) from err - except ArrException as err: - raise ConfigEntryNotReady from err - entry.async_on_unload(entry.add_update_listener(_async_update_listener)) - - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_HOST_CONFIG: host_configuration, - DATA_SONARR: sonarr, - DATA_SYSTEM_STATUS: system_status, + coordinators: dict[str, SonarrDataUpdateCoordinator[Any]] = { + "upcoming": CalendarDataUpdateCoordinator(hass, host_configuration, sonarr), + "commands": CommandsDataUpdateCoordinator(hass, host_configuration, sonarr), + "diskspace": DiskSpaceDataUpdateCoordinator(hass, host_configuration, sonarr), + "queue": QueueDataUpdateCoordinator(hass, host_configuration, sonarr), + "series": SeriesDataUpdateCoordinator(hass, host_configuration, sonarr), + "status": StatusDataUpdateCoordinator(hass, host_configuration, sonarr), + "wanted": WantedDataUpdateCoordinator(hass, host_configuration, sonarr), } - + # Temporary, until we add diagnostic entities + _version = None + for coordinator in coordinators.values(): + await coordinator.async_config_entry_first_refresh() + if isinstance(coordinator, StatusDataUpdateCoordinator): + _version = coordinator.data.version + coordinator.system_version = _version + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinators await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -88,7 +89,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Migrate old entry.""" - _LOGGER.debug("Migrating from version %s", entry.version) + LOGGER.debug("Migrating from version %s", entry.version) if entry.version == 1: new_proto = "https" if entry.data[CONF_SSL] else "http" @@ -106,7 +107,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_update_entry(entry, data=data) entry.version = 2 - _LOGGER.info("Migration to version %s successful", entry.version) + LOGGER.info("Migration to version %s successful", entry.version) return True diff --git a/homeassistant/components/sonarr/const.py b/homeassistant/components/sonarr/const.py index 58f5c465716..283c7fa72f9 100644 --- a/homeassistant/components/sonarr/const.py +++ b/homeassistant/components/sonarr/const.py @@ -1,4 +1,6 @@ """Constants for Sonarr.""" +import logging + DOMAIN = "sonarr" # Config Keys @@ -9,12 +11,9 @@ CONF_UNIT = "unit" CONF_UPCOMING_DAYS = "upcoming_days" CONF_WANTED_MAX_ITEMS = "wanted_max_items" -# Data -DATA_HOST_CONFIG = "host_config" -DATA_SONARR = "sonarr" -DATA_SYSTEM_STATUS = "system_status" - # Defaults DEFAULT_UPCOMING_DAYS = 1 DEFAULT_VERIFY_SSL = False DEFAULT_WANTED_MAX_ITEMS = 50 + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/sonarr/coordinator.py b/homeassistant/components/sonarr/coordinator.py new file mode 100644 index 00000000000..9b9a06b15f8 --- /dev/null +++ b/homeassistant/components/sonarr/coordinator.py @@ -0,0 +1,147 @@ +"""Data update coordinator for the Sonarr integration.""" +from __future__ import annotations + +from datetime import timedelta +from typing import TypeVar, Union, cast + +from aiopyarr import ( + Command, + Diskspace, + SonarrCalendar, + SonarrQueue, + SonarrSeries, + SonarrWantedMissing, + SystemStatus, + exceptions, +) +from aiopyarr.models.host_configuration import PyArrHostConfiguration +from aiopyarr.sonarr_client import SonarrClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +import homeassistant.util.dt as dt_util + +from .const import CONF_UPCOMING_DAYS, CONF_WANTED_MAX_ITEMS, DOMAIN, LOGGER + +SonarrDataT = TypeVar( + "SonarrDataT", + bound=Union[ + list[SonarrCalendar], + list[Command], + list[Diskspace], + SonarrQueue, + list[SonarrSeries], + SystemStatus, + SonarrWantedMissing, + ], +) + + +class SonarrDataUpdateCoordinator(DataUpdateCoordinator[SonarrDataT]): + """Data update coordinator for the Sonarr integration.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + host_configuration: PyArrHostConfiguration, + api_client: SonarrClient, + ) -> None: + """Initialize the coordinator.""" + super().__init__( + hass=hass, + logger=LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=30), + ) + self.api_client = api_client + self.host_configuration = host_configuration + self.system_version: str | None = None + + async def _async_update_data(self) -> SonarrDataT: + """Get the latest data from Sonarr.""" + try: + return await self._fetch_data() + + except exceptions.ArrConnectionException as ex: + raise UpdateFailed(ex) from ex + except exceptions.ArrAuthenticationException as ex: + raise ConfigEntryAuthFailed( + "API Key is no longer valid. Please reauthenticate" + ) from ex + + async def _fetch_data(self) -> SonarrDataT: + """Fetch the actual data.""" + raise NotImplementedError + + +class CalendarDataUpdateCoordinator(SonarrDataUpdateCoordinator[list[SonarrCalendar]]): + """Calendar update coordinator.""" + + async def _fetch_data(self) -> list[SonarrCalendar]: + """Fetch the movies data.""" + local = dt_util.start_of_local_day().replace(microsecond=0) + start = dt_util.as_utc(local) + end = start + timedelta(days=self.config_entry.options[CONF_UPCOMING_DAYS]) + return cast( + list[SonarrCalendar], + await self.api_client.async_get_calendar( + start_date=start, end_date=end, include_series=True + ), + ) + + +class CommandsDataUpdateCoordinator(SonarrDataUpdateCoordinator[list[Command]]): + """Commands update coordinator for Sonarr.""" + + async def _fetch_data(self) -> list[Command]: + """Fetch the data.""" + return cast(list[Command], await self.api_client.async_get_commands()) + + +class DiskSpaceDataUpdateCoordinator(SonarrDataUpdateCoordinator[list[Diskspace]]): + """Disk space update coordinator for Sonarr.""" + + async def _fetch_data(self) -> list[Diskspace]: + """Fetch the data.""" + return await self.api_client.async_get_diskspace() + + +class QueueDataUpdateCoordinator(SonarrDataUpdateCoordinator[SonarrQueue]): + """Queue update coordinator.""" + + async def _fetch_data(self) -> SonarrQueue: + """Fetch the data.""" + return await self.api_client.async_get_queue( + include_series=True, include_episode=True + ) + + +class SeriesDataUpdateCoordinator(SonarrDataUpdateCoordinator[list[SonarrSeries]]): + """Series update coordinator.""" + + async def _fetch_data(self) -> list[SonarrSeries]: + """Fetch the data.""" + return cast(list[SonarrSeries], await self.api_client.async_get_series()) + + +class StatusDataUpdateCoordinator(SonarrDataUpdateCoordinator[SystemStatus]): + """Status update coordinator for Sonarr.""" + + async def _fetch_data(self) -> SystemStatus: + """Fetch the data.""" + return await self.api_client.async_get_system_status() + + +class WantedDataUpdateCoordinator(SonarrDataUpdateCoordinator[SonarrWantedMissing]): + """Wanted update coordinator.""" + + async def _fetch_data(self) -> SonarrWantedMissing: + """Fetch the data.""" + return await self.api_client.async_get_wanted( + page_size=self.config_entry.options[CONF_WANTED_MAX_ITEMS], + include_series=True, + ) diff --git a/homeassistant/components/sonarr/entity.py b/homeassistant/components/sonarr/entity.py index 852e326cdb4..70d0299765d 100644 --- a/homeassistant/components/sonarr/entity.py +++ b/homeassistant/components/sonarr/entity.py @@ -1,41 +1,36 @@ """Base Entity for Sonarr.""" -from aiopyarr import SystemStatus -from aiopyarr.models.host_configuration import PyArrHostConfiguration -from aiopyarr.sonarr_client import SonarrClient +from __future__ import annotations from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN +from .coordinator import SonarrDataT, SonarrDataUpdateCoordinator -class SonarrEntity(Entity): +class SonarrEntity(CoordinatorEntity[SonarrDataUpdateCoordinator[SonarrDataT]]): """Defines a base Sonarr entity.""" def __init__( self, - *, - sonarr: SonarrClient, - host_config: PyArrHostConfiguration, - system_status: SystemStatus, - entry_id: str, - device_id: str, + coordinator: SonarrDataUpdateCoordinator[SonarrDataT], + description: EntityDescription, ) -> None: """Initialize the Sonarr entity.""" - self._entry_id = entry_id - self._device_id = device_id - self.sonarr = sonarr - self.host_config = host_config - self.system_status = system_status + super().__init__(coordinator) + self.coordinator = coordinator + self.entity_description = description + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" @property def device_info(self) -> DeviceInfo: """Return device information about the application.""" return DeviceInfo( - identifiers={(DOMAIN, self._device_id)}, - name="Activity Sensor", - manufacturer="Sonarr", - sw_version=self.system_status.version, + configuration_url=self.coordinator.host_configuration.base_url, entry_type=DeviceEntryType.SERVICE, - configuration_url=self.host_config.base_url, + identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)}, + manufacturer="Sonarr", + name="Activity Sensor", + sw_version=self.coordinator.system_version, ) diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index cdb44dee359..186cebda79b 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -1,16 +1,17 @@ """Support for Sonarr sensors.""" from __future__ import annotations -from collections.abc import Awaitable, Callable, Coroutine -from datetime import timedelta -from functools import wraps -import logging -from typing import Any, TypeVar, cast +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any, Generic -from aiopyarr import ArrConnectionException, ArrException, SystemStatus -from aiopyarr.models.host_configuration import PyArrHostConfiguration -from aiopyarr.sonarr_client import SonarrClient -from typing_extensions import Concatenate, ParamSpec +from aiopyarr import ( + Diskspace, + SonarrCalendar, + SonarrQueue, + SonarrSeries, + SonarrWantedMissing, +) from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry @@ -20,64 +21,74 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType import homeassistant.util.dt as dt_util -from .const import ( - CONF_UPCOMING_DAYS, - CONF_WANTED_MAX_ITEMS, - DATA_HOST_CONFIG, - DATA_SONARR, - DATA_SYSTEM_STATUS, - DOMAIN, -) +from .const import DOMAIN +from .coordinator import SonarrDataT, SonarrDataUpdateCoordinator from .entity import SonarrEntity -_LOGGER = logging.getLogger(__name__) -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( +@dataclass +class SonarrSensorEntityDescriptionMixIn(Generic[SonarrDataT]): + """Mixin for Sonarr sensor.""" + + value_fn: Callable[[SonarrDataT], StateType] + + +@dataclass +class SonarrSensorEntityDescription( + SensorEntityDescription, SonarrSensorEntityDescriptionMixIn[SonarrDataT] +): + """Class to describe a Sonarr sensor.""" + + +SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = { + "commands": SonarrSensorEntityDescription( key="commands", name="Sonarr Commands", icon="mdi:code-braces", native_unit_of_measurement="Commands", entity_registry_enabled_default=False, + value_fn=len, ), - SensorEntityDescription( + "diskspace": SonarrSensorEntityDescription[list[Diskspace]]( key="diskspace", name="Sonarr Disk Space", icon="mdi:harddisk", native_unit_of_measurement=DATA_GIGABYTES, entity_registry_enabled_default=False, + value_fn=lambda data: f"{sum(disk.freeSpace for disk in data) / 1024**3:.2f}", ), - SensorEntityDescription( + "queue": SonarrSensorEntityDescription[SonarrQueue]( key="queue", name="Sonarr Queue", icon="mdi:download", native_unit_of_measurement="Episodes", entity_registry_enabled_default=False, + value_fn=lambda data: data.totalRecords, ), - SensorEntityDescription( + "series": SonarrSensorEntityDescription[list[SonarrSeries]]( key="series", name="Sonarr Shows", icon="mdi:television", native_unit_of_measurement="Series", entity_registry_enabled_default=False, + value_fn=len, ), - SensorEntityDescription( + "upcoming": SonarrSensorEntityDescription[list[SonarrCalendar]]( key="upcoming", name="Sonarr Upcoming", icon="mdi:television", native_unit_of_measurement="Episodes", + value_fn=len, ), - SensorEntityDescription( + "wanted": SonarrSensorEntityDescription[SonarrWantedMissing]( key="wanted", name="Sonarr Wanted", icon="mdi:television", native_unit_of_measurement="Episodes", entity_registry_enabled_default=False, + value_fn=lambda data: data.totalRecords, ), -) - -_SonarrSensorT = TypeVar("_SonarrSensorT", bound="SonarrSensor") -_P = ParamSpec("_P") +} async def async_setup_entry( @@ -86,134 +97,30 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Sonarr sensors based on a config entry.""" - sonarr: SonarrClient = hass.data[DOMAIN][entry.entry_id][DATA_SONARR] - host_config: PyArrHostConfiguration = hass.data[DOMAIN][entry.entry_id][ - DATA_HOST_CONFIG + coordinators: dict[str, SonarrDataUpdateCoordinator[Any]] = hass.data[DOMAIN][ + entry.entry_id ] - system_status: SystemStatus = hass.data[DOMAIN][entry.entry_id][DATA_SYSTEM_STATUS] - options: dict[str, Any] = dict(entry.options) - - entities = [ - SonarrSensor( - sonarr, - host_config, - system_status, - entry.entry_id, - description, - options, - ) - for description in SENSOR_TYPES - ] - - async_add_entities(entities, True) + async_add_entities( + SonarrSensor(coordinators[coordinator_type], description) + for coordinator_type, description in SENSOR_TYPES.items() + ) -def sonarr_exception_handler( - func: Callable[Concatenate[_SonarrSensorT, _P], Awaitable[None]] -) -> Callable[Concatenate[_SonarrSensorT, _P], Coroutine[Any, Any, None]]: - """Decorate Sonarr calls to handle Sonarr exceptions. - - A decorator that wraps the passed in function, catches Sonarr errors, - and handles the availability of the entity. - """ - - @wraps(func) - async def wrapper( - self: _SonarrSensorT, *args: _P.args, **kwargs: _P.kwargs - ) -> None: - try: - await func(self, *args, **kwargs) - self.last_update_success = True - except ArrConnectionException as error: - if self.last_update_success: - _LOGGER.error("Error communicating with API: %s", error) - self.last_update_success = False - except ArrException as error: - if self.last_update_success: - _LOGGER.error("Invalid response from API: %s", error) - self.last_update_success = False - - return wrapper - - -class SonarrSensor(SonarrEntity, SensorEntity): +class SonarrSensor(SonarrEntity[SonarrDataT], SensorEntity): """Implementation of the Sonarr sensor.""" - data: dict[str, Any] - last_update_success: bool - upcoming_days: int - wanted_max_items: int - - def __init__( - self, - sonarr: SonarrClient, - host_config: PyArrHostConfiguration, - system_status: SystemStatus, - entry_id: str, - description: SensorEntityDescription, - options: dict[str, Any], - ) -> None: - """Initialize Sonarr sensor.""" - self.entity_description = description - self._attr_unique_id = f"{entry_id}_{description.key}" - - self.data = {} - self.last_update_success = True - self.upcoming_days = options[CONF_UPCOMING_DAYS] - self.wanted_max_items = options[CONF_WANTED_MAX_ITEMS] - - super().__init__( - sonarr=sonarr, - host_config=host_config, - system_status=system_status, - entry_id=entry_id, - device_id=entry_id, - ) - - @property - def available(self) -> bool: - """Return sensor availability.""" - return self.last_update_success - - @sonarr_exception_handler - async def async_update(self) -> None: - """Update entity.""" - key = self.entity_description.key - - if key == "diskspace": - self.data[key] = await self.sonarr.async_get_diskspace() - elif key == "commands": - self.data[key] = await self.sonarr.async_get_commands() - elif key == "queue": - self.data[key] = await self.sonarr.async_get_queue( - include_series=True, include_episode=True - ) - elif key == "series": - self.data[key] = await self.sonarr.async_get_series() - elif key == "upcoming": - local = dt_util.start_of_local_day().replace(microsecond=0) - start = dt_util.as_utc(local) - end = start + timedelta(days=self.upcoming_days) - - self.data[key] = await self.sonarr.async_get_calendar( - start_date=start, - end_date=end, - include_series=True, - ) - elif key == "wanted": - self.data[key] = await self.sonarr.async_get_wanted( - page_size=self.wanted_max_items, - include_series=True, - ) + coordinator: SonarrDataUpdateCoordinator + entity_description: SonarrSensorEntityDescription[SonarrDataT] @property def extra_state_attributes(self) -> dict[str, str] | None: """Return the state attributes of the entity.""" attrs = {} key = self.entity_description.key + data = self.coordinator.data - if key == "diskspace" and self.data.get(key) is not None: - for disk in self.data[key]: + if key == "diskspace": + for disk in data: free = disk.freeSpace / 1024**3 total = disk.totalSpace / 1024**3 usage = free / total * 100 @@ -221,29 +128,29 @@ class SonarrSensor(SonarrEntity, SensorEntity): attrs[ disk.path ] = f"{free:.2f}/{total:.2f}{self.unit_of_measurement} ({usage:.2f}%)" - elif key == "commands" and self.data.get(key) is not None: - for command in self.data[key]: + elif key == "commands": + for command in data: attrs[command.name] = command.status - elif key == "queue" and self.data.get(key) is not None: - for item in self.data[key].records: + elif key == "queue": + for item in data.records: remaining = 1 if item.size == 0 else item.sizeleft / item.size remaining_pct = 100 * (1 - remaining) identifier = f"S{item.episode.seasonNumber:02d}E{item.episode. episodeNumber:02d}" name = f"{item.series.title} {identifier}" attrs[name] = f"{remaining_pct:.2f}%" - elif key == "series" and self.data.get(key) is not None: - for item in self.data[key]: + elif key == "series": + for item in data: stats = item.statistics attrs[ item.title ] = f"{getattr(stats,'episodeFileCount', 0)}/{getattr(stats, 'episodeCount', 0)} Episodes" - elif key == "upcoming" and self.data.get(key) is not None: - for episode in self.data[key]: + elif key == "upcoming": + for episode in data: identifier = f"S{episode.seasonNumber:02d}E{episode.episodeNumber:02d}" attrs[episode.series.title] = identifier - elif key == "wanted" and self.data.get(key) is not None: - for item in self.data[key].records: + elif key == "wanted": + for item in data.records: identifier = f"S{item.seasonNumber:02d}E{item.episodeNumber:02d}" name = f"{item.series.title} {identifier}" @@ -256,26 +163,4 @@ class SonarrSensor(SonarrEntity, SensorEntity): @property def native_value(self) -> StateType: """Return the state of the sensor.""" - key = self.entity_description.key - - if key == "diskspace" and self.data.get(key) is not None: - total_free = sum(disk.freeSpace for disk in self.data[key]) - free = total_free / 1024**3 - return f"{free:.2f}" - - if key == "commands" and self.data.get(key) is not None: - return len(self.data[key]) - - if key == "queue" and self.data.get(key) is not None: - return cast(int, self.data[key].totalRecords) - - if key == "series" and self.data.get(key) is not None: - return len(self.data[key]) - - if key == "upcoming" and self.data.get(key) is not None: - return len(self.data[key]) - - if key == "wanted" and self.data.get(key) is not None: - return cast(int, self.data[key].totalRecords) - - return None + return self.entity_description.value_fn(self.coordinator.data) diff --git a/tests/components/sonarr/fixtures/system-status.json b/tests/components/sonarr/fixtures/system-status.json index fe6198a0444..311cadd4ff0 100644 --- a/tests/components/sonarr/fixtures/system-status.json +++ b/tests/components/sonarr/fixtures/system-status.json @@ -1,5 +1,6 @@ { "appName": "Sonarr", + "instanceName": "Sonarr", "version": "3.0.6.1451", "buildTime": "2022-01-23T16:51:56Z", "isDebug": false, From 7132fe0ae7dbe866510c94e6a9bdad3d19a2f884 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Sat, 8 Oct 2022 01:47:24 +0200 Subject: [PATCH 241/985] Fix realtime option for hvv_departures (#79799) --- homeassistant/components/hvv_departures/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index 0a516529386..e2de1758dd5 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import Throttle from homeassistant.util.dt import get_time_zone, utcnow -from .const import ATTRIBUTION, CONF_STATION, DOMAIN, MANUFACTURER +from .const import ATTRIBUTION, CONF_REAL_TIME, CONF_STATION, DOMAIN, MANUFACTURER MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) MAX_LIST = 20 @@ -90,7 +90,7 @@ class HVVDepartureSensor(SensorEntity): }, "maxList": MAX_LIST, "maxTimeOffset": MAX_TIME_OFFSET, - "useRealtime": self.config_entry.options.get("realtime", False), + "useRealtime": self.config_entry.options.get(CONF_REAL_TIME, False), } if "filter" in self.config_entry.options: From 4ff26b4ddd4070b2fca5303ebde49751be693273 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 7 Oct 2022 19:48:29 -0400 Subject: [PATCH 242/985] Add strict typing to Google Sheets (#79801) --- .strict-typing | 1 + mypy.ini | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/.strict-typing b/.strict-typing index 2c193bb5173..61e9c53609a 100644 --- a/.strict-typing +++ b/.strict-typing @@ -118,6 +118,7 @@ homeassistant.components.geocaching.* homeassistant.components.gios.* homeassistant.components.goalzero.* homeassistant.components.google.* +homeassistant.components.google_sheets.* homeassistant.components.greeneye_monitor.* homeassistant.components.group.* homeassistant.components.guardian.* diff --git a/mypy.ini b/mypy.ini index b63931d6c79..309068bf2c1 100644 --- a/mypy.ini +++ b/mypy.ini @@ -932,6 +932,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.google_sheets.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.greeneye_monitor.*] check_untyped_defs = true disallow_incomplete_defs = true From 2452e70e291f3257ced69019cacd5d31f9afb201 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Fri, 7 Oct 2022 19:49:47 -0400 Subject: [PATCH 243/985] Add Mazda brand (#79683) --- homeassistant/brands/mazda.json | 5 +++++ homeassistant/generated/integrations.json | 11 ++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 homeassistant/brands/mazda.json diff --git a/homeassistant/brands/mazda.json b/homeassistant/brands/mazda.json new file mode 100644 index 00000000000..89b554e4279 --- /dev/null +++ b/homeassistant/brands/mazda.json @@ -0,0 +1,5 @@ +{ + "domain": "mazda", + "name": "Mazda", + "integrations": ["mazda"] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 540d5c043c5..f09e30f4a93 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2428,9 +2428,14 @@ "name": "Matrix" }, "mazda": { - "config_flow": true, - "iot_class": "cloud_polling", - "name": "Mazda Connected Services" + "name": "Mazda", + "integrations": { + "mazda": { + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Mazda Connected Services" + } + } }, "meater": { "config_flow": true, From 24e9f6285da149ef12b3b46df85d92a1837c0992 Mon Sep 17 00:00:00 2001 From: Philippe Schenker Date: Sat, 8 Oct 2022 01:51:54 +0200 Subject: [PATCH 244/985] Change shelly trv precision to what is supported (#79672) change shelly trv precision to what is supported Shelly TRVs do support half-degree steps, change this accordingly. --- homeassistant/components/shelly/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index cb89ecc4ea1..89820a289ed 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -148,7 +148,7 @@ SHBLB_1_RGB_EFFECTS: Final = { SHTRV_01_TEMPERATURE_SETTINGS: Final = { "min": 4, "max": 31, - "step": 1, + "step": 0.5, } # Kelvin value for colorTemp From 5abff314371d4cb5f3d2c1668776b01cb4cac89f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 7 Oct 2022 23:52:36 +0000 Subject: [PATCH 245/985] Use new device classes in Accuweather integration (#79717) * Add new device classes * Update tests --- homeassistant/components/accuweather/sensor.py | 7 +++++++ tests/components/accuweather/test_sensor.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index f57af15714d..4347bca5863 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -189,6 +189,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="WindGustDay", + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", name="Wind gust day", entity_registry_enabled_default=False, @@ -200,6 +201,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="WindGustNight", + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", name="Wind gust night", entity_registry_enabled_default=False, @@ -211,6 +213,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="WindDay", + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", name="Wind day", unit_fn=lambda metric: SPEED_KILOMETERS_PER_HOUR @@ -221,6 +224,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="WindNight", + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", name="Wind night", unit_fn=lambda metric: SPEED_KILOMETERS_PER_HOUR @@ -243,6 +247,7 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="Ceiling", + device_class=SensorDeviceClass.DISTANCE, icon="mdi:weather-fog", name="Cloud ceiling", state_class=SensorStateClass.MEASUREMENT, @@ -329,6 +334,7 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="Wind", + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", name="Wind", state_class=SensorStateClass.MEASUREMENT, @@ -339,6 +345,7 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="WindGust", + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", name="Wind gust", entity_registry_enabled_default=False, diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index 8612a805980..ababaea5443 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -49,6 +49,7 @@ async def test_sensor_without_forecast(hass): assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_METERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DISTANCE entry = registry.async_get("sensor.home_cloud_ceiling") assert entry @@ -435,6 +436,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SPEED entry = registry.async_get("sensor.home_wind_gust") assert entry @@ -447,6 +449,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SPEED entry = registry.async_get("sensor.home_wind") assert entry @@ -579,6 +582,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR assert state.attributes.get("direction") == "SSE" assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SPEED entry = registry.async_get("sensor.home_wind_day_0d") assert entry @@ -592,6 +596,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get("direction") == "WNW" assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" assert state.attributes.get(ATTR_STATE_CLASS) is None + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SPEED entry = registry.async_get("sensor.home_wind_night_0d") assert entry @@ -605,6 +610,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get("direction") == "S" assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" assert state.attributes.get(ATTR_STATE_CLASS) is None + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SPEED entry = registry.async_get("sensor.home_wind_gust_day_0d") assert entry @@ -618,6 +624,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get("direction") == "WSW" assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" assert state.attributes.get(ATTR_STATE_CLASS) is None + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SPEED entry = registry.async_get("sensor.home_wind_gust_night_0d") assert entry From 62aa013097b2fc7a047958855d7d0d45da1b9a6e Mon Sep 17 00:00:00 2001 From: Garrett <7310260+G-Two@users.noreply.github.com> Date: Fri, 7 Oct 2022 19:54:05 -0400 Subject: [PATCH 246/985] Add vehicle model/year to subaru device (#79484) --- homeassistant/components/subaru/__init__.py | 5 +++++ homeassistant/components/subaru/const.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/homeassistant/components/subaru/__init__.py b/homeassistant/components/subaru/__init__.py index 83a5a1c1c9d..4829cf72087 100644 --- a/homeassistant/components/subaru/__init__.py +++ b/homeassistant/components/subaru/__init__.py @@ -31,6 +31,8 @@ from .const import ( VEHICLE_HAS_REMOTE_START, VEHICLE_HAS_SAFETY_SERVICE, VEHICLE_LAST_UPDATE, + VEHICLE_MODEL_NAME, + VEHICLE_MODEL_YEAR, VEHICLE_NAME, VEHICLE_VIN, ) @@ -147,6 +149,8 @@ def get_vehicle_info(controller, vin): """Obtain vehicle identifiers and capabilities.""" info = { VEHICLE_VIN: vin, + VEHICLE_MODEL_NAME: controller.get_model_name(vin), + VEHICLE_MODEL_YEAR: controller.get_model_year(vin), VEHICLE_NAME: controller.vin_to_name(vin), VEHICLE_HAS_EV: controller.get_ev_status(vin), VEHICLE_API_GEN: controller.get_api_gen(vin), @@ -163,5 +167,6 @@ def get_device_info(vehicle_info): return DeviceInfo( identifiers={(DOMAIN, vehicle_info[VEHICLE_VIN])}, manufacturer=MANUFACTURER, + model=f"{vehicle_info[VEHICLE_MODEL_YEAR]} {vehicle_info[VEHICLE_MODEL_NAME]}", name=vehicle_info[VEHICLE_NAME], ) diff --git a/homeassistant/components/subaru/const.py b/homeassistant/components/subaru/const.py index dc9a2224860..3de4930a691 100644 --- a/homeassistant/components/subaru/const.py +++ b/homeassistant/components/subaru/const.py @@ -19,6 +19,8 @@ COORDINATOR_NAME = "subaru_data" # info fields VEHICLE_VIN = "vin" +VEHICLE_MODEL_NAME = "model_name" +VEHICLE_MODEL_YEAR = "model_year" VEHICLE_NAME = "display_name" VEHICLE_HAS_EV = "is_ev" VEHICLE_API_GEN = "api_gen" From e00f04c2c3081a0605e6dafd9d3b8f2962bb4429 Mon Sep 17 00:00:00 2001 From: Henne <65833107+HennieLP@users.noreply.github.com> Date: Sat, 8 Oct 2022 01:54:50 +0200 Subject: [PATCH 247/985] Add state class to bosch_shc energy sensor (#79470) Make That energy sensor works in Dashbord --- homeassistant/components/bosch_shc/sensor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bosch_shc/sensor.py b/homeassistant/components/bosch_shc/sensor.py index 331a5ebb5f3..cb8272e92cf 100644 --- a/homeassistant/components/bosch_shc/sensor.py +++ b/homeassistant/components/bosch_shc/sensor.py @@ -4,7 +4,11 @@ from __future__ import annotations from boschshcpy import SHCSession from boschshcpy.device import SHCDevice -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, @@ -317,6 +321,7 @@ class EnergySensor(SHCEntity, SensorEntity): """Representation of an SHC energy reporting sensor.""" _attr_device_class = SensorDeviceClass.ENERGY + _attr_state_class = SensorStateClass.TOTAL_INCREASING _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: From f58e1513e25c1982c487ae5dcd64aec4c8a99c26 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 8 Oct 2022 00:29:46 +0000 Subject: [PATCH 248/985] [ci skip] Translation update --- .../components/august/translations/bg.json | 3 +++ .../components/blink/translations/bg.json | 2 +- .../components/braviatv/translations/bg.json | 6 +++--- .../components/braviatv/translations/ja.json | 3 ++- .../components/braviatv/translations/tr.json | 11 ++++++++++- .../components/co2signal/translations/bg.json | 3 ++- .../components/firmata/translations/tr.json | 4 ++++ .../components/generic/translations/bg.json | 10 ++++++++-- .../components/generic/translations/de.json | 7 +++++++ .../components/generic/translations/es.json | 7 +++++++ .../components/generic/translations/et.json | 7 +++++++ .../components/generic/translations/fr.json | 3 +++ .../components/generic/translations/hu.json | 7 +++++++ .../components/generic/translations/ja.json | 3 +++ .../components/generic/translations/no.json | 7 +++++++ .../components/generic/translations/pt-BR.json | 7 +++++++ .../components/generic/translations/tr.json | 7 +++++++ .../components/huawei_lte/translations/bg.json | 10 +++++++++- .../components/huawei_lte/translations/de.json | 11 ++++++++++- .../components/huawei_lte/translations/el.json | 11 ++++++++++- .../components/huawei_lte/translations/es.json | 11 ++++++++++- .../components/huawei_lte/translations/et.json | 11 ++++++++++- .../huawei_lte/translations/pt-BR.json | 11 ++++++++++- .../components/huawei_lte/translations/tr.json | 11 ++++++++++- .../components/mikrotik/translations/tr.json | 10 +++++++++- .../components/moon/translations/bg.json | 6 ++++++ .../components/nam/translations/bg.json | 2 +- .../nibe_heatpump/translations/ja.json | 7 +++++++ .../components/octoprint/translations/tr.json | 6 ++++++ .../components/ovo_energy/translations/bg.json | 2 +- .../plugwise/translations/select.bg.json | 9 +++++++++ .../plugwise/translations/select.de.json | 11 +++++++++++ .../plugwise/translations/select.es.json | 9 +++++++++ .../plugwise/translations/select.et.json | 11 +++++++++++ .../plugwise/translations/select.hu.json | 11 +++++++++++ .../plugwise/translations/select.ja.json | 9 +++++++++ .../plugwise/translations/select.no.json | 11 +++++++++++ .../plugwise/translations/select.pt-BR.json | 11 +++++++++++ .../plugwise/translations/select.tr.json | 11 +++++++++++ .../components/radarr/translations/ja.json | 4 ++++ .../components/ring/translations/bg.json | 2 +- .../rtsp_to_webrtc/translations/tr.json | 9 +++++++++ .../components/scrape/translations/bg.json | 4 ++-- .../components/sensor/translations/ja.json | 4 ++++ .../components/shelly/translations/bg.json | 2 +- .../components/tautulli/translations/ja.json | 1 + .../components/upnp/translations/tr.json | 2 +- .../components/zha/translations/bg.json | 14 ++++++++++++++ .../components/zha/translations/de.json | 16 ++++++++++++++++ .../components/zha/translations/et.json | 16 ++++++++++++++++ .../components/zha/translations/hu.json | 16 ++++++++++++++++ .../components/zha/translations/no.json | 16 ++++++++++++++++ .../components/zha/translations/pt-BR.json | 16 ++++++++++++++++ .../components/zha/translations/tr.json | 16 ++++++++++++++++ 54 files changed, 413 insertions(+), 24 deletions(-) create mode 100644 homeassistant/components/nibe_heatpump/translations/ja.json create mode 100644 homeassistant/components/plugwise/translations/select.bg.json create mode 100644 homeassistant/components/plugwise/translations/select.de.json create mode 100644 homeassistant/components/plugwise/translations/select.es.json create mode 100644 homeassistant/components/plugwise/translations/select.et.json create mode 100644 homeassistant/components/plugwise/translations/select.hu.json create mode 100644 homeassistant/components/plugwise/translations/select.ja.json create mode 100644 homeassistant/components/plugwise/translations/select.no.json create mode 100644 homeassistant/components/plugwise/translations/select.pt-BR.json create mode 100644 homeassistant/components/plugwise/translations/select.tr.json diff --git a/homeassistant/components/august/translations/bg.json b/homeassistant/components/august/translations/bg.json index 224e3324cb6..f2dccb231c1 100644 --- a/homeassistant/components/august/translations/bg.json +++ b/homeassistant/components/august/translations/bg.json @@ -8,6 +8,9 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" } + }, + "validation": { + "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" } } } diff --git a/homeassistant/components/blink/translations/bg.json b/homeassistant/components/blink/translations/bg.json index 32c84eeb1dc..60e7c86f621 100644 --- a/homeassistant/components/blink/translations/bg.json +++ b/homeassistant/components/blink/translations/bg.json @@ -14,7 +14,7 @@ "2fa": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u0435\u043d \u043a\u043e\u0434" }, "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u041f\u0418\u041d \u043a\u043e\u0434\u0430, \u0438\u0437\u043f\u0440\u0430\u0442\u0435\u043d \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u0438\u043c\u0435\u0439\u043b", - "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/bg.json b/homeassistant/components/braviatv/translations/bg.json index 3192f9c39c7..3a6908b0177 100644 --- a/homeassistant/components/braviatv/translations/bg.json +++ b/homeassistant/components/braviatv/translations/bg.json @@ -4,7 +4,7 @@ "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "not_bravia_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Bravia.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", - "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", @@ -16,7 +16,7 @@ "authorize": { "data": { "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", - "use_psk": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 PSK \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + "use_psk": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 PSK \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" } }, "confirm": { @@ -25,7 +25,7 @@ "reauth_confirm": { "data": { "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", - "use_psk": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 PSK \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + "use_psk": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 PSK \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" } }, "user": { diff --git a/homeassistant/components/braviatv/translations/ja.json b/homeassistant/components/braviatv/translations/ja.json index f573b562f6c..3b541f3a424 100644 --- a/homeassistant/components/braviatv/translations/ja.json +++ b/homeassistant/components/braviatv/translations/ja.json @@ -25,7 +25,8 @@ }, "reauth_confirm": { "data": { - "pin": "PIN\u30b3\u30fc\u30c9" + "pin": "PIN\u30b3\u30fc\u30c9", + "use_psk": "PSK\u8a8d\u8a3c\u3092\u4f7f\u7528\u3059\u308b" } }, "user": { diff --git a/homeassistant/components/braviatv/translations/tr.json b/homeassistant/components/braviatv/translations/tr.json index edbbaa7ef31..939f8e71b7b 100644 --- a/homeassistant/components/braviatv/translations/tr.json +++ b/homeassistant/components/braviatv/translations/tr.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "no_ip_control": "TV'nizde IP Kontrol\u00fc devre d\u0131\u015f\u0131 veya TV desteklenmiyor.", - "not_bravia_device": "Cihaz bir Bravia TV de\u011fildir." + "not_bravia_device": "Cihaz bir Bravia TV de\u011fildir.", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "reauth_unsuccessful": "Yeniden kimlik do\u011frulama ba\u015far\u0131s\u0131z oldu, l\u00fctfen entegrasyonu kald\u0131r\u0131n ve yeniden kurun." }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", @@ -23,6 +25,13 @@ "confirm": { "description": "Kuruluma ba\u015flamak ister misiniz?" }, + "reauth_confirm": { + "data": { + "pin": "PIN Kodu", + "use_psk": "PSK kimlik do\u011frulamas\u0131n\u0131 kullan\u0131n" + }, + "description": "Sony Bravia TV'de g\u00f6sterilen PIN kodunu girin. \n\n PIN kodu g\u00f6r\u00fcnt\u00fclenmezse, TV'nizde Home Assistant kayd\u0131n\u0131 iptal etmeniz gerekir, \u015furaya gidin: Ayarlar - > A\u011f - > Uzak cihaz ayarlar\u0131 - > Uzak cihaz\u0131n kayd\u0131n\u0131 sil. \n\n PIN yerine PSK (\u00d6n Payla\u015f\u0131ml\u0131 Anahtar) kullanabilirsiniz. PSK, eri\u015fim kontrol\u00fc i\u00e7in kullan\u0131lan kullan\u0131c\u0131 tan\u0131ml\u0131 bir gizli anahtard\u0131r. Bu kimlik do\u011frulama y\u00f6nteminin daha kararl\u0131 olmas\u0131 \u00f6nerilir. TV'nizde PSK'y\u0131 etkinle\u015ftirmek i\u00e7in \u015furaya gidin: Ayarlar - > A\u011f - > Ev A\u011f\u0131 Kurulumu - > IP Kontrol\u00fc. Ard\u0131ndan \u00abPSK kimlik do\u011frulamas\u0131n\u0131 kullan\u00bb kutusunu i\u015faretleyin ve PIN yerine PSK'n\u0131z\u0131 girin." + }, "user": { "data": { "host": "Ana Bilgisayar" diff --git a/homeassistant/components/co2signal/translations/bg.json b/homeassistant/components/co2signal/translations/bg.json index bb253fb6e6b..43f43e3ae91 100644 --- a/homeassistant/components/co2signal/translations/bg.json +++ b/homeassistant/components/co2signal/translations/bg.json @@ -23,7 +23,8 @@ "user": { "data": { "location": "\u041f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u0434\u0430\u043d\u043d\u0438 \u0437\u0430" - } + }, + "description": "\u041f\u043e\u0441\u0435\u0442\u0435\u0442\u0435 https://co2signal.com/ \u0437\u0430 \u0434\u0430 \u0437\u0430\u044f\u0432\u0438\u0442\u0435 \u0442\u043e\u043a\u044a\u043d." } } } diff --git a/homeassistant/components/firmata/translations/tr.json b/homeassistant/components/firmata/translations/tr.json index b7d038a229b..1e7302b9096 100644 --- a/homeassistant/components/firmata/translations/tr.json +++ b/homeassistant/components/firmata/translations/tr.json @@ -2,6 +2,10 @@ "config": { "abort": { "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "one": "Bo\u015f", + "other": "Bo\u015f" } } } \ No newline at end of file diff --git a/homeassistant/components/generic/translations/bg.json b/homeassistant/components/generic/translations/bg.json index ebb2d32c21d..ed6779edf1d 100644 --- a/homeassistant/components/generic/translations/bg.json +++ b/homeassistant/components/generic/translations/bg.json @@ -19,11 +19,17 @@ }, "user": { "data": { - "authentication": "\u0423\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "authentication": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "rtsp_transport": "RTSP \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u0435\u043d \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "\u0422\u043e\u0432\u0430 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0438\u0437\u0433\u043b\u0435\u0436\u0434\u0430 \u0434\u043e\u0431\u0440\u0435." + }, + "title": "\u041f\u0440\u0435\u0433\u043b\u0435\u0434" } } }, @@ -40,7 +46,7 @@ }, "init": { "data": { - "authentication": "\u0423\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "authentication": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "rtsp_transport": "RTSP \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u0435\u043d \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" diff --git a/homeassistant/components/generic/translations/de.json b/homeassistant/components/generic/translations/de.json index 57d15a8efea..70ea6ac052c 100644 --- a/homeassistant/components/generic/translations/de.json +++ b/homeassistant/components/generic/translations/de.json @@ -45,6 +45,13 @@ "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, "description": "Gib die Einstellungen f\u00fcr die Verbindung mit der Kamera ein." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "Dieses Bild sieht gut aus." + }, + "description": "![Kamera-Standbildvorschau]({preview_url})", + "title": "Vorschau" } } }, diff --git a/homeassistant/components/generic/translations/es.json b/homeassistant/components/generic/translations/es.json index ff3ce5d4a91..98c2eb4bc8f 100644 --- a/homeassistant/components/generic/translations/es.json +++ b/homeassistant/components/generic/translations/es.json @@ -45,6 +45,13 @@ "verify_ssl": "Verificar el certificado SSL" }, "description": "Introduce los ajustes para conectarte a la c\u00e1mara." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "Esta imagen se ve bien." + }, + "description": "![Vista previa de imagen fija de c\u00e1mara]({preview_url})", + "title": "Vista previa" } } }, diff --git a/homeassistant/components/generic/translations/et.json b/homeassistant/components/generic/translations/et.json index 2746f84b9a1..628a77ed3a0 100644 --- a/homeassistant/components/generic/translations/et.json +++ b/homeassistant/components/generic/translations/et.json @@ -45,6 +45,13 @@ "verify_ssl": "Kontrolli SSL sertifikaati" }, "description": "Sisesta s\u00e4tted kaameraga \u00fchenduse loomiseks." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "See pilt n\u00e4eb hea v\u00e4lja." + }, + "description": "![Kaamera pildi eelvaade]( {preview_url} )", + "title": "Eelvaade" } } }, diff --git a/homeassistant/components/generic/translations/fr.json b/homeassistant/components/generic/translations/fr.json index 2c992c2fa4f..27a0e9b8c60 100644 --- a/homeassistant/components/generic/translations/fr.json +++ b/homeassistant/components/generic/translations/fr.json @@ -45,6 +45,9 @@ "verify_ssl": "V\u00e9rifier le certificat SSL" }, "description": "Saisissez les param\u00e8tres de connexion \u00e0 la cam\u00e9ra." + }, + "user_confirm_still": { + "title": "Aper\u00e7u" } } }, diff --git a/homeassistant/components/generic/translations/hu.json b/homeassistant/components/generic/translations/hu.json index bf03ec88d96..a36457999ca 100644 --- a/homeassistant/components/generic/translations/hu.json +++ b/homeassistant/components/generic/translations/hu.json @@ -45,6 +45,13 @@ "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" }, "description": "Adja meg a kamer\u00e1hoz val\u00f3 csatlakoz\u00e1s be\u00e1ll\u00edt\u00e1sait." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "A k\u00e9p megfelel\u0151" + }, + "description": "![Kamerak\u00e9p el\u0151n\u00e9zet] ({preview_url})", + "title": "El\u0151n\u00e9zet" } } }, diff --git a/homeassistant/components/generic/translations/ja.json b/homeassistant/components/generic/translations/ja.json index 8e1ba58b4df..f07da6e04fc 100644 --- a/homeassistant/components/generic/translations/ja.json +++ b/homeassistant/components/generic/translations/ja.json @@ -45,6 +45,9 @@ "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" }, "description": "\u30ab\u30e1\u30e9\u306b\u63a5\u7d9a\u3059\u308b\u305f\u3081\u306e\u8a2d\u5b9a\u3092\u5165\u529b\u3057\u307e\u3059\u3002" + }, + "user_confirm_still": { + "title": "\u30d7\u30ec\u30d3\u30e5\u30fc" } } }, diff --git a/homeassistant/components/generic/translations/no.json b/homeassistant/components/generic/translations/no.json index 23319f0a938..f85e5a189ba 100644 --- a/homeassistant/components/generic/translations/no.json +++ b/homeassistant/components/generic/translations/no.json @@ -45,6 +45,13 @@ "verify_ssl": "Verifisere SSL-sertifikat" }, "description": "Angi innstillingene for \u00e5 koble til kameraet." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "Dette bildet ser bra ut." + }, + "description": "![Camera Still Image Preview]( {preview_url} )", + "title": "Forh\u00e5ndsvisning" } } }, diff --git a/homeassistant/components/generic/translations/pt-BR.json b/homeassistant/components/generic/translations/pt-BR.json index 86ac7a01efb..52b00d29190 100644 --- a/homeassistant/components/generic/translations/pt-BR.json +++ b/homeassistant/components/generic/translations/pt-BR.json @@ -45,6 +45,13 @@ "verify_ssl": "Verifique o certificado SSL" }, "description": "Insira as configura\u00e7\u00f5es para se conectar \u00e0 c\u00e2mera." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "Essa imagem parece boa." + }, + "description": "![Visualiza\u00e7\u00e3o da imagem est\u00e1tica da c\u00e2mera]({preview_url})", + "title": "Visualizar" } } }, diff --git a/homeassistant/components/generic/translations/tr.json b/homeassistant/components/generic/translations/tr.json index efce9d014b1..3abe2af62bd 100644 --- a/homeassistant/components/generic/translations/tr.json +++ b/homeassistant/components/generic/translations/tr.json @@ -45,6 +45,13 @@ "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" }, "description": "Kameraya ba\u011flanmak i\u00e7in ayarlar\u0131 girin." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "Bu g\u00f6r\u00fcnt\u00fc iyi g\u00f6r\u00fcn\u00fcyor." + }, + "description": "![Kamera Dura\u011fan G\u00f6r\u00fcnt\u00fc \u00d6nizlemesi]( {preview_url} )", + "title": "\u00d6n izleme" } } }, diff --git a/homeassistant/components/huawei_lte/translations/bg.json b/homeassistant/components/huawei_lte/translations/bg.json index feb010b214f..8f34e808235 100644 --- a/homeassistant/components/huawei_lte/translations/bg.json +++ b/homeassistant/components/huawei_lte/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "\u041d\u0435 \u0435 Huawei LTE \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + "not_huawei_lte": "\u041d\u0435 \u0435 Huawei LTE \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "connection_timeout": "\u0412\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0438\u0437\u0442\u0435\u0447\u0435", @@ -14,6 +15,13 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/huawei_lte/translations/de.json b/homeassistant/components/huawei_lte/translations/de.json index 50e7b7a2e53..8073aef0bf6 100644 --- a/homeassistant/components/huawei_lte/translations/de.json +++ b/homeassistant/components/huawei_lte/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Kein Huawei LTE-Ger\u00e4t" + "not_huawei_lte": "Kein Huawei LTE-Ger\u00e4t", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "connection_timeout": "Verbindungszeit\u00fcberschreitung", @@ -15,6 +16,14 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Gib die Zugangsdaten f\u00fcr das Ger\u00e4t ein.", + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "password": "Passwort", diff --git a/homeassistant/components/huawei_lte/translations/el.json b/homeassistant/components/huawei_lte/translations/el.json index 8b6def091c9..f2648a50697 100644 --- a/homeassistant/components/huawei_lte/translations/el.json +++ b/homeassistant/components/huawei_lte/translations/el.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Huawei LTE" + "not_huawei_lte": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Huawei LTE", + "reauth_successful": "\u0397 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c4\u03b1\u03c5\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { "connection_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", @@ -15,6 +16,14 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "\u039f\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2.", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c4\u03b1\u03c5\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, "user": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/huawei_lte/translations/es.json b/homeassistant/components/huawei_lte/translations/es.json index a88f35ba3d5..af2a155d5e5 100644 --- a/homeassistant/components/huawei_lte/translations/es.json +++ b/homeassistant/components/huawei_lte/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "No es un dispositivo Huawei LTE" + "not_huawei_lte": "No es un dispositivo Huawei LTE", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "connection_timeout": "Tiempo de espera de la conexi\u00f3n superado", @@ -15,6 +16,14 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "description": "Introduce las credenciales de acceso del dispositivo.", + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "password": "Contrase\u00f1a", diff --git a/homeassistant/components/huawei_lte/translations/et.json b/homeassistant/components/huawei_lte/translations/et.json index 08fcca84b23..82c36cf54d9 100644 --- a/homeassistant/components/huawei_lte/translations/et.json +++ b/homeassistant/components/huawei_lte/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Pole Huawei LTE seade" + "not_huawei_lte": "Pole Huawei LTE seade", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "connection_timeout": "\u00dchenduse ajal\u00f5pp", @@ -15,6 +16,14 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Sisesta seadme juurdep\u00e4\u00e4suload.", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "password": "Salas\u00f5na", diff --git a/homeassistant/components/huawei_lte/translations/pt-BR.json b/homeassistant/components/huawei_lte/translations/pt-BR.json index a92c2de3f10..c9c453d69f2 100644 --- a/homeassistant/components/huawei_lte/translations/pt-BR.json +++ b/homeassistant/components/huawei_lte/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "N\u00e3o \u00e9 um dispositivo Huawei LTE" + "not_huawei_lte": "N\u00e3o \u00e9 um dispositivo Huawei LTE", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "connection_timeout": "Tempo limite de conex\u00e3o atingido", @@ -15,6 +16,14 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "password": "Senha", + "username": "Nome de usu\u00e1rio" + }, + "description": "Insira as credenciais de acesso ao dispositivo.", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, "user": { "data": { "password": "Senha", diff --git a/homeassistant/components/huawei_lte/translations/tr.json b/homeassistant/components/huawei_lte/translations/tr.json index 6d231efa8ed..c2791808f65 100644 --- a/homeassistant/components/huawei_lte/translations/tr.json +++ b/homeassistant/components/huawei_lte/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Huawei LTE cihaz\u0131 de\u011fil" + "not_huawei_lte": "Huawei LTE cihaz\u0131 de\u011fil", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "connection_timeout": "Ba\u011flant\u0131 zamana\u015f\u0131m\u0131", @@ -15,6 +16,14 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Cihaz eri\u015fim kimlik bilgilerini girin.", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "password": "Parola", diff --git a/homeassistant/components/mikrotik/translations/tr.json b/homeassistant/components/mikrotik/translations/tr.json index 628703168ec..bfbdad17280 100644 --- a/homeassistant/components/mikrotik/translations/tr.json +++ b/homeassistant/components/mikrotik/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", @@ -9,6 +10,13 @@ "name_exists": "Bu ad zaten var" }, "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "{username} i\u00e7n \u015fifre ge\u00e7ersiz.", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "host": "Sunucu", diff --git a/homeassistant/components/moon/translations/bg.json b/homeassistant/components/moon/translations/bg.json index 71462a123f9..47a9a365db1 100644 --- a/homeassistant/components/moon/translations/bg.json +++ b/homeassistant/components/moon/translations/bg.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Moon \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u043e.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043e\u0442 Home Assistant.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Moon \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" + } + }, "title": "\u041b\u0443\u043d\u0430" } \ No newline at end of file diff --git a/homeassistant/components/nam/translations/bg.json b/homeassistant/components/nam/translations/bg.json index 50368ce880d..9be1a75603a 100644 --- a/homeassistant/components/nam/translations/bg.json +++ b/homeassistant/components/nam/translations/bg.json @@ -4,7 +4,7 @@ "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "device_unsupported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", - "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/nibe_heatpump/translations/ja.json b/homeassistant/components/nibe_heatpump/translations/ja.json new file mode 100644 index 00000000000..9ad7fd4a7aa --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/tr.json b/homeassistant/components/octoprint/translations/tr.json index 7e3e24c7b65..5099c5b9d15 100644 --- a/homeassistant/components/octoprint/translations/tr.json +++ b/homeassistant/components/octoprint/translations/tr.json @@ -4,6 +4,7 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "auth_failed": "Uygulama API anahtar\u0131 al\u0131namad\u0131", "cannot_connect": "Ba\u011flanma hatas\u0131", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", "unknown": "Beklenmeyen hata" }, "error": { @@ -15,6 +16,11 @@ "get_api_key": "OctoPrint UI'sini a\u00e7\u0131n ve 'Ev Asistan\u0131' i\u00e7in Eri\u015fim \u0130ste\u011finde '\u0130zin Ver'i t\u0131klay\u0131n." }, "step": { + "reauth_confirm": { + "data": { + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + }, "user": { "data": { "host": "Sunucu", diff --git a/homeassistant/components/ovo_energy/translations/bg.json b/homeassistant/components/ovo_energy/translations/bg.json index 9b0d9f27ccb..b0c9e8a77cc 100644 --- a/homeassistant/components/ovo_energy/translations/bg.json +++ b/homeassistant/components/ovo_energy/translations/bg.json @@ -11,7 +11,7 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" }, - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/select.bg.json b/homeassistant/components/plugwise/translations/select.bg.json new file mode 100644 index 00000000000..646d778981e --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.bg.json @@ -0,0 +1,9 @@ +{ + "state": { + "plugwise__regulation_mode": { + "cooling": "\u041e\u0445\u043b\u0430\u0436\u0434\u0430\u043d\u0435", + "heating": "\u041e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435", + "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/select.de.json b/homeassistant/components/plugwise/translations/select.de.json new file mode 100644 index 00000000000..1f2ccd0a825 --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.de.json @@ -0,0 +1,11 @@ +{ + "state": { + "plugwise__regulation_mode": { + "bleeding_cold": "Kalt", + "bleeding_hot": "Hei\u00df", + "cooling": "K\u00fchlung", + "heating": "Heizbetrieb", + "off": "Aus" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/select.es.json b/homeassistant/components/plugwise/translations/select.es.json new file mode 100644 index 00000000000..c08ee07b64f --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.es.json @@ -0,0 +1,9 @@ +{ + "state": { + "plugwise__regulation_mode": { + "cooling": "Refrigeraci\u00f3n", + "heating": "Calefacci\u00f3n", + "off": "Apagado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/select.et.json b/homeassistant/components/plugwise/translations/select.et.json new file mode 100644 index 00000000000..a7eef041f0b --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.et.json @@ -0,0 +1,11 @@ +{ + "state": { + "plugwise__regulation_mode": { + "bleeding_cold": "Jahutuseat soenemine", + "bleeding_hot": "K\u00fcttest jahtumine", + "cooling": "Jahutamine", + "heating": "K\u00fcte", + "off": "V\u00e4ljas" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/select.hu.json b/homeassistant/components/plugwise/translations/select.hu.json new file mode 100644 index 00000000000..2d614743f16 --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.hu.json @@ -0,0 +1,11 @@ +{ + "state": { + "plugwise__regulation_mode": { + "bleeding_cold": "J\u00e9ghideg", + "bleeding_hot": "Forr\u00f3", + "cooling": "H\u0171t\u00e9s", + "heating": "F\u0171t\u00e9s", + "off": "Ki" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/select.ja.json b/homeassistant/components/plugwise/translations/select.ja.json new file mode 100644 index 00000000000..581f4ca36a3 --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.ja.json @@ -0,0 +1,9 @@ +{ + "state": { + "plugwise__regulation_mode": { + "cooling": "\u51b7\u623f(\u51b7\u5374)", + "heating": "\u6696\u623f(\u52a0\u71b1)", + "off": "\u30aa\u30d5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/select.no.json b/homeassistant/components/plugwise/translations/select.no.json new file mode 100644 index 00000000000..729d25c936c --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.no.json @@ -0,0 +1,11 @@ +{ + "state": { + "plugwise__regulation_mode": { + "bleeding_cold": "Bl\u00f8dende kaldt", + "bleeding_hot": "Bl\u00f8dende varmt", + "cooling": "Kj\u00f8ling", + "heating": "Oppvarming", + "off": "Av" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/select.pt-BR.json b/homeassistant/components/plugwise/translations/select.pt-BR.json new file mode 100644 index 00000000000..578a7c7d488 --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.pt-BR.json @@ -0,0 +1,11 @@ +{ + "state": { + "plugwise__regulation_mode": { + "bleeding_cold": "Frio congelante", + "bleeding_hot": "Queimando quente", + "cooling": "Resfriamento", + "heating": "Aquecimento", + "off": "Desligado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/select.tr.json b/homeassistant/components/plugwise/translations/select.tr.json new file mode 100644 index 00000000000..9ae8b443ebd --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.tr.json @@ -0,0 +1,11 @@ +{ + "state": { + "plugwise__regulation_mode": { + "bleeding_cold": "So\u011futma", + "bleeding_hot": "Is\u0131tma", + "cooling": "So\u011futma", + "heating": "Is\u0131tma", + "off": "Kapal\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/ja.json b/homeassistant/components/radarr/translations/ja.json index add29174eaf..26281a46d6d 100644 --- a/homeassistant/components/radarr/translations/ja.json +++ b/homeassistant/components/radarr/translations/ja.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "reauth_confirm": { "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" diff --git a/homeassistant/components/ring/translations/bg.json b/homeassistant/components/ring/translations/bg.json index b0254a1bdf6..dfe9fcc384e 100644 --- a/homeassistant/components/ring/translations/bg.json +++ b/homeassistant/components/ring/translations/bg.json @@ -12,7 +12,7 @@ "data": { "2fa": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u0435\u043d \u043a\u043e\u0434" }, - "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { "data": { diff --git a/homeassistant/components/rtsp_to_webrtc/translations/tr.json b/homeassistant/components/rtsp_to_webrtc/translations/tr.json index 1331c6dd8c4..dad60389697 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/tr.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/tr.json @@ -23,5 +23,14 @@ "title": "RTSPtoWebRTC'yi yap\u0131land\u0131r\u0131n" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Stun sunucu adresi (ana bilgisayar:ba\u011flant\u0131 noktas\u0131)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/bg.json b/homeassistant/components/scrape/translations/bg.json index 89c2ffc7880..1599a1918d7 100644 --- a/homeassistant/components/scrape/translations/bg.json +++ b/homeassistant/components/scrape/translations/bg.json @@ -7,7 +7,7 @@ "user": { "data": { "attribute": "\u0410\u0442\u0440\u0438\u0431\u0443\u0442", - "authentication": "\u0423\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "authentication": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", "index": "\u0418\u043d\u0434\u0435\u043a\u0441", "name": "\u0418\u043c\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", @@ -23,7 +23,7 @@ "init": { "data": { "attribute": "\u0410\u0442\u0440\u0438\u0431\u0443\u0442", - "authentication": "\u0423\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "authentication": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", "index": "\u0418\u043d\u0434\u0435\u043a\u0441", "name": "\u0418\u043c\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/sensor/translations/ja.json b/homeassistant/components/sensor/translations/ja.json index b7153e4b5de..568a53657aa 100644 --- a/homeassistant/components/sensor/translations/ja.json +++ b/homeassistant/components/sensor/translations/ja.json @@ -6,6 +6,7 @@ "is_carbon_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_carbon_monoxide": "\u73fe\u5728\u306e {entity_name} \u4e00\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_current": "\u73fe\u5728\u306e {entity_name} \u96fb\u6d41", + "is_distance": "\u73fe\u5728\u306e {entity_name} \u306e\u8ddd\u96e2", "is_energy": "\u73fe\u5728\u306e {entity_name} \u30a8\u30cd\u30eb\u30ae\u30fc", "is_frequency": "\u73fe\u5728\u306e {entity_name} \u983b\u5ea6(frequency)", "is_gas": "\u73fe\u5728\u306e {entity_name} \u30ac\u30b9", @@ -24,6 +25,7 @@ "is_pressure": "\u73fe\u5728\u306e {entity_name} \u5727\u529b", "is_reactive_power": "\u73fe\u5728\u306e{entity_name}\u7121\u52b9\u96fb\u529b", "is_signal_strength": "\u73fe\u5728\u306e {entity_name} \u4fe1\u53f7\u5f37\u5ea6", + "is_speed": "\u73fe\u5728\u306e {entity_name} \u306e\u901f\u5ea6", "is_sulphur_dioxide": "\u73fe\u5728\u306e {entity_name} \u4e8c\u9178\u5316\u786b\u9ec4\u6fc3\u5ea6\u30ec\u30d9\u30eb", "is_temperature": "\u73fe\u5728\u306e {entity_name} \u6e29\u5ea6", "is_value": "\u73fe\u5728\u306e {entity_name} \u5024", @@ -36,6 +38,7 @@ "carbon_dioxide": "{entity_name} \u4e8c\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", "carbon_monoxide": "{entity_name} \u4e00\u9178\u5316\u70ad\u7d20\u6fc3\u5ea6\u306e\u5909\u5316", "current": "{entity_name} \u73fe\u5728\u306e\u5909\u5316", + "distance": "{entity_name} \u306e\u8ddd\u96e2\u304c\u5909\u5316", "energy": "{entity_name} \u30a8\u30cd\u30eb\u30ae\u30fc\u306e\u5909\u5316", "frequency": "{entity_name} \u983b\u5ea6(frequency)\u304c\u5909\u5316", "gas": "{entity_name} \u30ac\u30b9\u306e\u5909\u5316", @@ -54,6 +57,7 @@ "pressure": "{entity_name} \u5727\u529b\u306e\u5909\u5316", "reactive_power": "{entity_name}\u7121\u52b9\u96fb\u529b\u306e\u5909\u66f4", "signal_strength": "{entity_name} \u4fe1\u53f7\u5f37\u5ea6\u306e\u5909\u5316", + "speed": "{entity_name} \u306e\u901f\u5ea6\u304c\u5909\u5316", "sulphur_dioxide": "{entity_name} \u4e8c\u9178\u5316\u786b\u9ec4\u6fc3\u5ea6\u306e\u5909\u5316", "temperature": "{entity_name} \u6e29\u5ea6\u5909\u5316", "value": "{entity_name} \u5024\u306e\u5909\u5316", diff --git a/homeassistant/components/shelly/translations/bg.json b/homeassistant/components/shelly/translations/bg.json index e856ebe4a54..1cdcd4e5d86 100644 --- a/homeassistant/components/shelly/translations/bg.json +++ b/homeassistant/components/shelly/translations/bg.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", - "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e.", + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e.", "unsupported_firmware": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0432\u0435\u0440\u0441\u0438\u044f \u043d\u0430 \u0444\u044a\u0440\u043c\u0443\u0435\u0440\u0430." }, "error": { diff --git a/homeassistant/components/tautulli/translations/ja.json b/homeassistant/components/tautulli/translations/ja.json index 2407bb4b984..fd51dc92c43 100644 --- a/homeassistant/components/tautulli/translations/ja.json +++ b/homeassistant/components/tautulli/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, diff --git a/homeassistant/components/upnp/translations/tr.json b/homeassistant/components/upnp/translations/tr.json index 4742549eaff..7d24215a47e 100644 --- a/homeassistant/components/upnp/translations/tr.json +++ b/homeassistant/components/upnp/translations/tr.json @@ -13,7 +13,7 @@ "step": { "init": { "one": "Bo\u015f", - "other": "" + "other": "Bo\u015f" }, "ssdp_confirm": { "description": "Bu UPnP / IGD cihaz\u0131n\u0131 kurmak istiyor musunuz?" diff --git a/homeassistant/components/zha/translations/bg.json b/homeassistant/components/zha/translations/bg.json index cd133985a32..1c4c44c9dff 100644 --- a/homeassistant/components/zha/translations/bg.json +++ b/homeassistant/components/zha/translations/bg.json @@ -139,6 +139,12 @@ "description": "ZHA \u0449\u0435 \u0431\u044a\u0434\u0435 \u0441\u043f\u0440\u044f\u043d. \u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435?", "title": "\u041f\u0440\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 ZHA" }, + "instruct_unplug": { + "title": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u0442\u0435 \u0441\u0442\u0430\u0440\u043e\u0442\u043e \u0441\u0438 \u0440\u0430\u0434\u0438\u043e" + }, + "intent_migrate": { + "title": "\u041c\u0438\u0433\u0440\u0438\u0440\u0430\u043d\u0435 \u043a\u044a\u043c \u043d\u043e\u0432\u043e \u0440\u0430\u0434\u0438\u043e" + }, "manual_pick_radio_type": { "data": { "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e" @@ -153,6 +159,14 @@ "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435 \u043d\u0430 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u044f \u043f\u043e\u0440\u0442", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u044f \u043f\u043e\u0440\u0442" }, + "prompt_migrate_or_reconfigure": { + "description": "\u041c\u0438\u0433\u0440\u0438\u0440\u0430\u0442\u0435 \u043a\u044a\u043c \u043d\u043e\u0432\u043e \u0440\u0430\u0434\u0438\u043e \u0438\u043b\u0438 \u043f\u0440\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0442\u0435\u043a\u0443\u0449\u043e\u0442\u043e \u0440\u0430\u0434\u0438\u043e?", + "menu_options": { + "intent_migrate": "\u041c\u0438\u0433\u0440\u0438\u0440\u0430\u043d\u0435 \u043a\u044a\u043c \u043d\u043e\u0432\u043e \u0440\u0430\u0434\u0438\u043e", + "intent_reconfigure": "\u041f\u0440\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0442\u0435\u043a\u0443\u0449\u043e\u0442\u043e \u0440\u0430\u0434\u0438\u043e" + }, + "title": "\u041c\u0438\u0433\u0440\u0438\u0440\u0430\u043d\u0435 \u0438\u043b\u0438 \u043f\u0440\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "\u041a\u0430\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u0444\u0430\u0439\u043b" diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index a2c7ad5956f..abe872ab75c 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -212,6 +212,14 @@ "description": "ZHA wird gestoppt. M\u00f6chtest du fortfahren?", "title": "ZHA rekonfigurieren" }, + "instruct_unplug": { + "description": "Dein altes Funkger\u00e4t wurde zur\u00fcckgesetzt. Wenn die Hardware nicht mehr ben\u00f6tigt wird, kannst du es jetzt ausstecken.", + "title": "Stecke dein altes Funkger\u00e4t aus" + }, + "intent_migrate": { + "description": "Dein altes Funkger\u00e4t wird auf die Werkseinstellungen zur\u00fcckgesetzt. Wenn du einen kombinierten Z-Wave- und Zigbee-Adapter wie den HUSBZB-1 verwendest, wird nur der Zigbee-Teil zur\u00fcckgesetzt.\n\nM\u00f6chtest du fortfahren?", + "title": "Umstellung auf ein neues Funkger\u00e4t" + }, "manual_pick_radio_type": { "data": { "radio_type": "Funktyp" @@ -235,6 +243,14 @@ "description": "Dein Backup hat eine andere IEEE-Adresse als dein Funkger\u00e4t. Damit dein Netzwerk ordnungsgem\u00e4\u00df funktioniert, sollte auch die IEEE-Adresse deines Funkger\u00e4ts ge\u00e4ndert werden.\n\nDies ist ein permanenter Vorgang.", "title": "Funk-IEEE-Adresse \u00fcberschreiben" }, + "prompt_migrate_or_reconfigure": { + "description": "Stellst du auf ein neues Funkger\u00e4t um oder konfigurierst du das aktuelle Funkger\u00e4t neu?", + "menu_options": { + "intent_migrate": "Umstellung auf ein neues Funkger\u00e4t", + "intent_reconfigure": "Das aktuelle Funkger\u00e4t neu konfigurieren" + }, + "title": "Migrieren oder neu konfigurieren" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Datei hochladen" diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index 8e49e6a335d..54cabbe4c82 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -212,6 +212,14 @@ "description": "ZHA peatatakse. Kas soovid j\u00e4tkata?", "title": "Seadista ZHA uuesti" }, + "instruct_unplug": { + "description": "Teie vana raadio on l\u00e4htestatud. Kui riistvara pole enam vaja, saad selle n\u00fc\u00fcd lahti \u00fchendada.", + "title": "\u00dchenda vana raadio lahti" + }, + "intent_migrate": { + "description": "Vana raadio l\u00e4htestatakse tehaseseadetele. Kui kasutad kombineeritud Z-Wave ja Zigbee adapterit, n\u00e4iteks HUSBZB-1, l\u00e4htestab see ainult Zigbee osa. \n\n Kas soovid j\u00e4tkata?", + "title": "Teisalda uuele seadmele" + }, "manual_pick_radio_type": { "data": { "radio_type": "Raadio t\u00fc\u00fcp" @@ -235,6 +243,14 @@ "description": "Varukoopial on erinev IEEE aadress kui raadiol. V\u00f5rgu n\u00f5uetekohaseks toimimiseks tuleks muuta ka raadio IEEE aadressi.\n\nSee on p\u00fcsiv toiming.", "title": "Kirjuta IEEE aadress \u00fcle" }, + "prompt_migrate_or_reconfigure": { + "description": "Kas l\u00e4hed \u00fcle uuele raadiole v\u00f5i seadistad praegust raadiot \u00fcmber?", + "menu_options": { + "intent_migrate": "Teisalda uuele seadmele", + "intent_reconfigure": "Taasseadista praegune seade" + }, + "title": "Teisaldamine v\u00f5i uuesti seadistamine" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Lae kirje \u00fcles" diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index dac86f55d38..b70bfcd597b 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -212,6 +212,14 @@ "description": "A ZHA le\u00e1ll. Biztos benne, hogy folytatja?", "title": "A ZHA \u00fajrakonfigur\u00e1l\u00e1sa" }, + "instruct_unplug": { + "description": "A r\u00e9gi r\u00e1di\u00f3t vissza lett \u00e1ll\u00edtva Ha a hardverre m\u00e1r nincs sz\u00fcks\u00e9g, most kih\u00fazhatja.", + "title": "H\u00fazza ki a r\u00e9gi r\u00e1di\u00f3t" + }, + "intent_migrate": { + "description": "A r\u00e9gi r\u00e1di\u00f3ja gy\u00e1ri alaphelyzetbe ker\u00fcl. Ha kombin\u00e1lt Z-Wave \u00e9s Zigbee adaptert haszn\u00e1l, mint p\u00e9ld\u00e1ul a HUSBZB-1, akkor ez csak a Zigbee r\u00e9szt \u00e1ll\u00edtja vissza.\n\nSzeretn\u00e9 folytatni?", + "title": "\u00daj r\u00e1di\u00f3ra val\u00f3 \u00e1tt\u00e9r\u00e9s" + }, "manual_pick_radio_type": { "data": { "radio_type": "R\u00e1di\u00f3 t\u00edpusa" @@ -235,6 +243,14 @@ "description": "A biztons\u00e1gi m\u00e1solat IEEE-c\u00edme elt\u00e9r a r\u00e1di\u00f3\u00e9t\u00f3l. A h\u00e1l\u00f3zat megfelel\u0151 m\u0171k\u00f6d\u00e9s\u00e9hez a r\u00e1di\u00f3 IEEE-c\u00edm\u00e9t is meg kell v\u00e1ltoztatni. \n\n Ez egy v\u00e9gleles m\u0171velet.", "title": "A r\u00e1di\u00f3 IEEE-c\u00edm\u00e9nek fel\u00fcl\u00edr\u00e1sa" }, + "prompt_migrate_or_reconfigure": { + "description": "\u00daj r\u00e1di\u00f3ra val\u00f3 \u00e1tt\u00e9r\u00e9s vagy a jelenlegi r\u00e1di\u00f3 \u00fajrakonfigur\u00e1l\u00e1sa?", + "menu_options": { + "intent_migrate": "\u00daj r\u00e1di\u00f3ra val\u00f3 \u00e1tt\u00e9r\u00e9s", + "intent_reconfigure": "Az aktu\u00e1lis r\u00e1di\u00f3 \u00fajrakonfigur\u00e1l\u00e1sa" + }, + "title": "Migr\u00e1l\u00e1s vagy \u00fajrakonfigur\u00e1l\u00e1s" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "F\u00e1jl felt\u00f6lt\u00e9se" diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json index b4c8a87aa2f..989409f7436 100644 --- a/homeassistant/components/zha/translations/no.json +++ b/homeassistant/components/zha/translations/no.json @@ -212,6 +212,14 @@ "description": "ZHA vil bli stoppet. \u00d8nsker du \u00e5 fortsette?", "title": "Konfigurer ZHA p\u00e5 nytt" }, + "instruct_unplug": { + "description": "Den gamle radioen din er tilbakestilt. Hvis maskinvaren ikke lenger er n\u00f8dvendig, kan du n\u00e5 koble den fra.", + "title": "Koble fra den gamle radioen" + }, + "intent_migrate": { + "description": "Den gamle radioen blir tilbakestilt til fabrikkstandard. Hvis du bruker en kombinert Z-Wave og Zigbee-adapter som HUSBZB-1, vil dette bare tilbakestille Zigbee-delen. \n\n \u00d8nsker du \u00e5 fortsette?", + "title": "Migrer til en ny radio" + }, "manual_pick_radio_type": { "data": { "radio_type": "Radio type" @@ -235,6 +243,14 @@ "description": "Sikkerhetskopien din har en annen IEEE-adresse enn radioen din. For at nettverket skal fungere ordentlig, b\u00f8r IEEE-adressen til radioen ogs\u00e5 endres. \n\n Dette er en permanent operasjon.", "title": "Overskriv radio IEEE-adresse" }, + "prompt_migrate_or_reconfigure": { + "description": "Migrerer du til en ny radio eller rekonfigurerer den n\u00e5v\u00e6rende radioen?", + "menu_options": { + "intent_migrate": "Migrer til en ny radio", + "intent_reconfigure": "Konfigurer gjeldende radio p\u00e5 nytt" + }, + "title": "Migrer eller rekonfigurer" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Last opp en fil" diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index 460deeac1f1..ba0ac930f1e 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -212,6 +212,14 @@ "description": "ZHA ser\u00e1 interrompido. Voc\u00ea deseja continuar?", "title": "Reconfigurar ZHA" }, + "instruct_unplug": { + "description": "Seu r\u00e1dio antigo foi reiniciado. Se o hardware n\u00e3o for mais necess\u00e1rio, agora voc\u00ea pode desconect\u00e1-lo.", + "title": "Desconecte seu r\u00e1dio antigo" + }, + "intent_migrate": { + "description": "Seu r\u00e1dio antigo ser\u00e1 redefinido de f\u00e1brica. Se voc\u00ea estiver usando um adaptador Z-Wave e Zigbee combinado, como o HUSBZB-1, isso apenas redefinir\u00e1 a parte Zigbee. \n\n Voc\u00ea deseja continuar?", + "title": "Migrar para um novo r\u00e1dio" + }, "manual_pick_radio_type": { "data": { "radio_type": "Tipo de r\u00e1dio" @@ -235,6 +243,14 @@ "description": "Seu backup tem um endere\u00e7o IEEE diferente do seu r\u00e1dio. Para que sua rede funcione corretamente, o endere\u00e7o IEEE do seu r\u00e1dio tamb\u00e9m deve ser alterado. \n\n Esta \u00e9 uma opera\u00e7\u00e3o permanente.", "title": "Sobrescrever o endere\u00e7o IEEE do r\u00e1dio" }, + "prompt_migrate_or_reconfigure": { + "description": "Voc\u00ea est\u00e1 migrando para um novo r\u00e1dio ou reconfigurando o r\u00e1dio atual?", + "menu_options": { + "intent_migrate": "Migrar para um novo r\u00e1dio", + "intent_reconfigure": "Reconfigure o r\u00e1dio atual" + }, + "title": "Migrar ou reconfigurar" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Carregar um arquivo" diff --git a/homeassistant/components/zha/translations/tr.json b/homeassistant/components/zha/translations/tr.json index 6ee4ff515ed..f309ecf92a0 100644 --- a/homeassistant/components/zha/translations/tr.json +++ b/homeassistant/components/zha/translations/tr.json @@ -212,6 +212,14 @@ "description": "ZHA durdurulacak. Devam etmek istiyor musunuz?", "title": "ZHA'y\u0131 yeniden yap\u0131land\u0131r\u0131n" }, + "instruct_unplug": { + "description": "Eski radyonuz s\u0131f\u0131rland\u0131. Donan\u0131m art\u0131k gerekli de\u011filse, \u015fimdi \u00e7\u0131kartabilirsiniz.", + "title": "Eski radyonuzu \u00e7\u0131kart\u0131n" + }, + "intent_migrate": { + "description": "Eski radyonuz fabrika ayarlar\u0131na s\u0131f\u0131rlanacak. HUSBZB-1 gibi birle\u015fik bir Z-Wave ve Zigbee adapt\u00f6r\u00fc kullan\u0131yorsan\u0131z, bu yaln\u0131zca Zigbee k\u0131sm\u0131n\u0131 s\u0131f\u0131rlayacakt\u0131r. \n\n Devam etmek istiyor musunuz?", + "title": "Yeni bir radyoya ge\u00e7i\u015f yap\u0131n" + }, "manual_pick_radio_type": { "data": { "radio_type": "Radyo Tipi" @@ -235,6 +243,14 @@ "description": "Yedeklemenizin, telsizinizden farkl\u0131 bir IEEE adresi var. A\u011f\u0131n\u0131z\u0131n d\u00fczg\u00fcn \u00e7al\u0131\u015fmas\u0131 i\u00e7in telsizinizin IEEE adresinin de de\u011fi\u015ftirilmesi gerekir. \n\n Bu kal\u0131c\u0131 bir operasyondur.", "title": "Radyo IEEE Adresinin \u00dczerine Yaz" }, + "prompt_migrate_or_reconfigure": { + "description": "Yeni bir radyoya m\u0131 ge\u00e7iyorsunuz yoksa mevcut radyoyu yeniden mi yap\u0131land\u0131r\u0131yorsunuz?", + "menu_options": { + "intent_migrate": "Yeni bir radyoya ge\u00e7i\u015f yap\u0131n", + "intent_reconfigure": "Mevcut radyoyu yeniden yap\u0131land\u0131r\u0131n" + }, + "title": "Ta\u015f\u0131ma veya yeniden yap\u0131land\u0131rma" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Bir dosya y\u00fckleyin" From c6f28f6d59d9be13a49ba28b396898e8e571cdab Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 7 Oct 2022 21:53:48 -0400 Subject: [PATCH 249/985] Migrate Sonarr to new entity naming style (#79844) --- homeassistant/components/sonarr/const.py | 1 + homeassistant/components/sonarr/entity.py | 8 +++++--- homeassistant/components/sonarr/sensor.py | 12 ++++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sonarr/const.py b/homeassistant/components/sonarr/const.py index 283c7fa72f9..5468953184a 100644 --- a/homeassistant/components/sonarr/const.py +++ b/homeassistant/components/sonarr/const.py @@ -12,6 +12,7 @@ CONF_UPCOMING_DAYS = "upcoming_days" CONF_WANTED_MAX_ITEMS = "wanted_max_items" # Defaults +DEFAULT_NAME = "Sonarr" DEFAULT_UPCOMING_DAYS = 1 DEFAULT_VERIFY_SSL = False DEFAULT_WANTED_MAX_ITEMS = 50 diff --git a/homeassistant/components/sonarr/entity.py b/homeassistant/components/sonarr/entity.py index 70d0299765d..e8a65239be7 100644 --- a/homeassistant/components/sonarr/entity.py +++ b/homeassistant/components/sonarr/entity.py @@ -5,13 +5,15 @@ from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN +from .const import DEFAULT_NAME, DOMAIN from .coordinator import SonarrDataT, SonarrDataUpdateCoordinator class SonarrEntity(CoordinatorEntity[SonarrDataUpdateCoordinator[SonarrDataT]]): """Defines a base Sonarr entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: SonarrDataUpdateCoordinator[SonarrDataT], @@ -30,7 +32,7 @@ class SonarrEntity(CoordinatorEntity[SonarrDataUpdateCoordinator[SonarrDataT]]): configuration_url=self.coordinator.host_configuration.base_url, entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)}, - manufacturer="Sonarr", - name="Activity Sensor", + manufacturer=DEFAULT_NAME, + name=DEFAULT_NAME, sw_version=self.coordinator.system_version, ) diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index 186cebda79b..da0e4c5af8c 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -43,7 +43,7 @@ class SonarrSensorEntityDescription( SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = { "commands": SonarrSensorEntityDescription( key="commands", - name="Sonarr Commands", + name="Commands", icon="mdi:code-braces", native_unit_of_measurement="Commands", entity_registry_enabled_default=False, @@ -51,7 +51,7 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = { ), "diskspace": SonarrSensorEntityDescription[list[Diskspace]]( key="diskspace", - name="Sonarr Disk Space", + name="Disk space", icon="mdi:harddisk", native_unit_of_measurement=DATA_GIGABYTES, entity_registry_enabled_default=False, @@ -59,7 +59,7 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = { ), "queue": SonarrSensorEntityDescription[SonarrQueue]( key="queue", - name="Sonarr Queue", + name="Queue", icon="mdi:download", native_unit_of_measurement="Episodes", entity_registry_enabled_default=False, @@ -67,7 +67,7 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = { ), "series": SonarrSensorEntityDescription[list[SonarrSeries]]( key="series", - name="Sonarr Shows", + name="Shows", icon="mdi:television", native_unit_of_measurement="Series", entity_registry_enabled_default=False, @@ -75,14 +75,14 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = { ), "upcoming": SonarrSensorEntityDescription[list[SonarrCalendar]]( key="upcoming", - name="Sonarr Upcoming", + name="Upcoming", icon="mdi:television", native_unit_of_measurement="Episodes", value_fn=len, ), "wanted": SonarrSensorEntityDescription[SonarrWantedMissing]( key="wanted", - name="Sonarr Wanted", + name="Wanted", icon="mdi:television", native_unit_of_measurement="Episodes", entity_registry_enabled_default=False, From e2b7e79ccb9b8bfd09e310aa87e8aed5800c85c3 Mon Sep 17 00:00:00 2001 From: spycle <48740594+spycle@users.noreply.github.com> Date: Sat, 8 Oct 2022 07:19:40 +0100 Subject: [PATCH 250/985] Fix keymitt_ble discovery (#79809) * Fix keymitt_ble discovery * Update tests * Up version * Up version keymitt_ble * Up version keymitt_ble --- homeassistant/components/keymitt_ble/manifest.json | 7 ++----- homeassistant/generated/bluetooth.py | 6 +----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/keymitt_ble/__init__.py | 4 ++-- 5 files changed, 7 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/keymitt_ble/manifest.json b/homeassistant/components/keymitt_ble/manifest.json index 445a2581bda..2a21074bb12 100644 --- a/homeassistant/components/keymitt_ble/manifest.json +++ b/homeassistant/components/keymitt_ble/manifest.json @@ -5,17 +5,14 @@ "config_flow": true, "bluetooth": [ { - "service_uuid": "00001831-0000-1000-8000-00805f9b34fb" - }, - { - "service_data_uuid": "00001831-0000-1000-8000-00805f9b34fb" + "service_uuid": "0000abcd-0000-1000-8000-00805f9b34fb" }, { "local_name": "mib*" } ], "codeowners": ["@spycle"], - "requirements": ["PyMicroBot==0.0.6"], + "requirements": ["PyMicroBot==0.0.8"], "iot_class": "assumed_state", "dependencies": ["bluetooth"], "loggers": ["keymitt_ble"] diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 43481ee48f1..b24d9e1986e 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -177,11 +177,7 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ }, { "domain": "keymitt_ble", - "service_uuid": "00001831-0000-1000-8000-00805f9b34fb", - }, - { - "domain": "keymitt_ble", - "service_data_uuid": "00001831-0000-1000-8000-00805f9b34fb", + "service_uuid": "0000abcd-0000-1000-8000-00805f9b34fb", }, { "domain": "keymitt_ble", diff --git a/requirements_all.txt b/requirements_all.txt index 6d524e0b88e..654325b19a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -23,7 +23,7 @@ PyFlick==0.0.2 PyMVGLive==1.1.4 # homeassistant.components.keymitt_ble -PyMicroBot==0.0.6 +PyMicroBot==0.0.8 # homeassistant.components.mobile_app # homeassistant.components.owntracks diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3c095951e6..f8c89e64d63 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ HAP-python==4.5.0 PyFlick==0.0.2 # homeassistant.components.keymitt_ble -PyMicroBot==0.0.6 +PyMicroBot==0.0.8 # homeassistant.components.mobile_app # homeassistant.components.owntracks diff --git a/tests/components/keymitt_ble/__init__.py b/tests/components/keymitt_ble/__init__.py index 0b145970643..7ae4c20c406 100644 --- a/tests/components/keymitt_ble/__init__.py +++ b/tests/components/keymitt_ble/__init__.py @@ -32,7 +32,7 @@ def patch_async_setup_entry(return_value=True): SERVICE_INFO = BluetoothServiceInfoBleak( name="mibp", - service_uuids=["00001831-0000-1000-8000-00805f9b34fb"], + service_uuids=["0000abcd-0000-1000-8000-00805f9b34fb"], address="aa:bb:cc:dd:ee:ff", manufacturer_data={}, service_data={}, @@ -41,7 +41,7 @@ SERVICE_INFO = BluetoothServiceInfoBleak( advertisement=AdvertisementData( local_name="mibp", manufacturer_data={}, - service_uuids=["00001831-0000-1000-8000-00805f9b34fb"], + service_uuids=["0000abcd-0000-1000-8000-00805f9b34fb"], ), device=BLEDevice("aa:bb:cc:dd:ee:ff", "mibp"), time=0, From e7b550685e451f830ebb1069c720c73381e6befb Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 8 Oct 2022 08:49:24 +0200 Subject: [PATCH 251/985] Fix POE control port_idx error in UniFi (#79838) Bump UniFi dependency --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 6bf9f8aa473..eeb974242e9 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==37"], + "requirements": ["aiounifi==38"], "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 654325b19a3..ede3dfe8f30 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,7 +276,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==37 +aiounifi==38 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8c89e64d63..c2edbfa760e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -251,7 +251,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==37 +aiounifi==38 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From c6df823b357150a760a3ef8b491deae40e1bfcad Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 8 Oct 2022 12:34:41 +0200 Subject: [PATCH 252/985] Use value_fn in WLED number (#79865) --- homeassistant/components/wled/number.py | 31 ++++++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wled/number.py b/homeassistant/components/wled/number.py index 60317003f19..05c517192a0 100644 --- a/homeassistant/components/wled/number.py +++ b/homeassistant/components/wled/number.py @@ -1,8 +1,12 @@ """Support for LED numbers.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from functools import partial +from wled import Segment + from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -35,8 +39,20 @@ async def async_setup_entry( update_segments() +@dataclass +class WLEDNumberDescriptionMixin: + """Mixin for WLED number.""" + + value_fn: Callable[[Segment], float | None] + + +@dataclass +class WLEDNumberEntityDescription(NumberEntityDescription, WLEDNumberDescriptionMixin): + """Class describing WLED number entities.""" + + NUMBERS = [ - NumberEntityDescription( + WLEDNumberEntityDescription( key=ATTR_SPEED, name="Speed", icon="mdi:speedometer", @@ -44,14 +60,16 @@ NUMBERS = [ native_step=1, native_min_value=0, native_max_value=255, + value_fn=lambda segment: segment.speed, ), - NumberEntityDescription( + WLEDNumberEntityDescription( key=ATTR_INTENSITY, name="Intensity", entity_category=EntityCategory.CONFIG, native_step=1, native_min_value=0, native_max_value=255, + value_fn=lambda segment: segment.intensity, ), ] @@ -59,11 +77,13 @@ NUMBERS = [ class WLEDNumber(WLEDEntity, NumberEntity): """Defines a WLED speed number.""" + entity_description: WLEDNumberEntityDescription + def __init__( self, coordinator: WLEDDataUpdateCoordinator, segment: int, - description: NumberEntityDescription, + description: WLEDNumberEntityDescription, ) -> None: """Initialize WLED .""" super().__init__(coordinator=coordinator) @@ -92,9 +112,8 @@ class WLEDNumber(WLEDEntity, NumberEntity): @property def native_value(self) -> float | None: """Return the current WLED segment number value.""" - return getattr( # type: ignore[no-any-return] - self.coordinator.data.state.segments[self._segment], - self.entity_description.key, + return self.entity_description.value_fn( + self.coordinator.data.state.segments[self._segment] ) @wled_exception_handler From 6546bba2330e2b46243491a7eaaa144ebfd5841e Mon Sep 17 00:00:00 2001 From: Bert Melis Date: Sat, 8 Oct 2022 15:36:49 +0200 Subject: [PATCH 253/985] Process abbreviated availability options in mqtt discovery payload (#79712) Expand availability in mqtt discovery payload --- homeassistant/components/mqtt/discovery.py | 8 ++++++++ tests/components/mqtt/test_discovery.py | 12 ++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 23453e146ed..92ad50b7b4a 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -139,6 +139,14 @@ async def async_start( # noqa: C901 key = DEVICE_ABBREVIATIONS.get(key, key) device[key] = device.pop(abbreviated_key) + if CONF_AVAILABILITY in payload: + for availability_conf in cv.ensure_list(payload[CONF_AVAILABILITY]): + if isinstance(availability_conf, dict): + for key in list(availability_conf): + abbreviated_key = key + key = ABBREVIATIONS.get(key, key) + availability_conf[key] = availability_conf.pop(abbreviated_key) + if TOPIC_BASE in payload: base = payload.pop(TOPIC_BASE) for key, value in payload.items(): diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 50c0a50bd40..77d7093830e 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -945,9 +945,9 @@ async def test_discovery_expansion(hass, mqtt_mock_entry_no_yaml_config, caplog) ' "payload_not_available": "not_available"' " }," " {" - ' "topic":"avail_item2/~",' - ' "payload_available": "available",' - ' "payload_not_available": "not_available"' + ' "t":"avail_item2/~",' + ' "pl_avail": "available",' + ' "pl_not_avail": "not_available"' " }" " ]," ' "dev":{' @@ -999,9 +999,9 @@ async def test_discovery_expansion_2(hass, mqtt_mock_entry_no_yaml_config, caplo ' "stat_t": "test_topic/~",' ' "cmd_t": "~/test_topic",' ' "availability": {' - ' "topic":"~/avail_item1",' - ' "payload_available": "available",' - ' "payload_not_available": "not_available"' + ' "t":"~/avail_item1",' + ' "pl_avail": "available",' + ' "pl_not_avail": "not_available"' " }," ' "dev":{' ' "ids":["5706DF"],' From 5dde93b429ea3ecd594abc271416b3434b517661 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 8 Oct 2022 17:13:00 +0200 Subject: [PATCH 254/985] Bump pytrafikverket to 0.2.1 (#79872) --- homeassistant/components/trafikverket_ferry/manifest.json | 2 +- homeassistant/components/trafikverket_train/manifest.json | 2 +- .../components/trafikverket_weatherstation/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/trafikverket_ferry/manifest.json b/homeassistant/components/trafikverket_ferry/manifest.json index d333473f169..47b5784296d 100644 --- a/homeassistant/components/trafikverket_ferry/manifest.json +++ b/homeassistant/components/trafikverket_ferry/manifest.json @@ -2,7 +2,7 @@ "domain": "trafikverket_ferry", "name": "Trafikverket Ferry", "documentation": "https://www.home-assistant.io/integrations/trafikverket_ferry", - "requirements": ["pytrafikverket==0.2.0.1"], + "requirements": ["pytrafikverket==0.2.1"], "codeowners": ["@gjohansson-ST"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/trafikverket_train/manifest.json b/homeassistant/components/trafikverket_train/manifest.json index 0432670f15c..d8ccd62f956 100644 --- a/homeassistant/components/trafikverket_train/manifest.json +++ b/homeassistant/components/trafikverket_train/manifest.json @@ -2,7 +2,7 @@ "domain": "trafikverket_train", "name": "Trafikverket Train", "documentation": "https://www.home-assistant.io/integrations/trafikverket_train", - "requirements": ["pytrafikverket==0.2.0.1"], + "requirements": ["pytrafikverket==0.2.1"], "codeowners": ["@endor-force", "@gjohansson-ST"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/trafikverket_weatherstation/manifest.json b/homeassistant/components/trafikverket_weatherstation/manifest.json index e7efca9b24a..fbe2435f841 100644 --- a/homeassistant/components/trafikverket_weatherstation/manifest.json +++ b/homeassistant/components/trafikverket_weatherstation/manifest.json @@ -2,7 +2,7 @@ "domain": "trafikverket_weatherstation", "name": "Trafikverket Weather Station", "documentation": "https://www.home-assistant.io/integrations/trafikverket_weatherstation", - "requirements": ["pytrafikverket==0.2.0.1"], + "requirements": ["pytrafikverket==0.2.1"], "codeowners": ["@endor-force", "@gjohansson-ST"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index ede3dfe8f30..6f26737caa0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2060,7 +2060,7 @@ pytradfri[async]==9.0.0 # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation -pytrafikverket==0.2.0.1 +pytrafikverket==0.2.1 # homeassistant.components.usb pyudev==0.23.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2edbfa760e..ec4725d98c2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1423,7 +1423,7 @@ pytradfri[async]==9.0.0 # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation -pytrafikverket==0.2.0.1 +pytrafikverket==0.2.1 # homeassistant.components.usb pyudev==0.23.2 From 647a4ac13184094bb27bdc1f1f4d960bc0ba14c8 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 8 Oct 2022 17:32:46 +0200 Subject: [PATCH 255/985] Update typing-extensions constraint to >=4.4.0 (#79860) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7cfae5f2813..269e4b9e573 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ pyyaml==6.0 requests==2.28.1 scapy==2.4.5 sqlalchemy==1.4.41 -typing-extensions>=3.10.0.2,<5.0 +typing-extensions>=4.4.0,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 yarl==1.8.1 diff --git a/pyproject.toml b/pyproject.toml index 7838e3f7503..b5488631eac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ dependencies = [ "python-slugify==4.0.1", "pyyaml==6.0", "requests==2.28.1", - "typing-extensions>=3.10.0.2,<5.0", + "typing-extensions>=4.4.0,<5.0", "voluptuous==0.13.1", "voluptuous-serialize==2.5.0", "yarl==1.8.1", diff --git a/requirements.txt b/requirements.txt index 28d3c11081b..0dfc353823a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ pip>=21.0,<22.3 python-slugify==4.0.1 pyyaml==6.0 requests==2.28.1 -typing-extensions>=3.10.0.2,<5.0 +typing-extensions>=4.4.0,<5.0 voluptuous==0.13.1 voluptuous-serialize==2.5.0 yarl==1.8.1 From 4baba777801765b0ce9025c9ef170d3465d874fc Mon Sep 17 00:00:00 2001 From: Patrick ZAJDA Date: Sat, 8 Oct 2022 20:02:26 +0200 Subject: [PATCH 256/985] Add state class measurement to SwitchBot signal strength sensors (#79886) --- homeassistant/components/switchbot/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index e435e71efbd..8e5d0e92d5a 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -28,6 +28,7 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { key="rssi", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -35,6 +36,7 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { key="wifi_rssi", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), From 506695fdc5c57583b3c58beaa111d55c05ccc258 Mon Sep 17 00:00:00 2001 From: John Levermore Date: Sat, 8 Oct 2022 19:53:32 +0100 Subject: [PATCH 257/985] Fix london_underground TUBE_LINES to match current API output (#79410) Fix: Update london_underground component with updated TUBE_LINES list to match current API output --- homeassistant/components/london_underground/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/london_underground/sensor.py b/homeassistant/components/london_underground/sensor.py index 96ff9bc5056..b111fb8be6c 100644 --- a/homeassistant/components/london_underground/sensor.py +++ b/homeassistant/components/london_underground/sensor.py @@ -39,13 +39,13 @@ TUBE_LINES = [ "Circle", "District", "DLR", + "Elizabeth line", "Hammersmith & City", "Jubilee", "London Overground", "Metropolitan", "Northern", "Piccadilly", - "TfL Rail", "Victoria", "Waterloo & City", ] From 6010672e2f02692550222da196de1761f4849db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 8 Oct 2022 21:54:16 +0300 Subject: [PATCH 258/985] Add syncthru active alerts sensor, set default manufacturer (#79418) * Use Samsung as default manufacturer * Sensor docstring fixes * Add active alerts sensor --- homeassistant/components/syncthru/__init__.py | 1 + homeassistant/components/syncthru/sensor.py | 24 ++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index 988c92de593..5031f485ab3 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -69,6 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: config_entry_id=entry.entry_id, configuration_url=printer.url, connections=device_connections(printer), + default_manufacturer="Samsung", identifiers=device_identifiers(printer), model=printer.model(), name=printer.hostname(), diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index 788b1cb5761..11e1403816e 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -55,7 +55,10 @@ async def async_setup_entry( supp_output_tray = printer.output_tray_status() name = config_entry.data[CONF_NAME] - entities: list[SyncThruSensor] = [SyncThruMainSensor(coordinator, name)] + entities: list[SyncThruSensor] = [ + SyncThruMainSensor(coordinator, name), + SyncThruActiveAlertSensor(coordinator, name), + ] for key in supp_toner: entities.append(SyncThruTonerSensor(coordinator, name, key)) @@ -166,7 +169,7 @@ class SyncThruTonerSensor(SyncThruSensor): class SyncThruDrumSensor(SyncThruSensor): - """Implementation of a Samsung Printer toner sensor platform.""" + """Implementation of a Samsung Printer drum sensor platform.""" def __init__(self, syncthru, name, color): """Initialize the sensor.""" @@ -214,7 +217,7 @@ class SyncThruInputTraySensor(SyncThruSensor): class SyncThruOutputTraySensor(SyncThruSensor): - """Implementation of a Samsung Printer input tray sensor platform.""" + """Implementation of a Samsung Printer output tray sensor platform.""" def __init__(self, syncthru, name, number): """Initialize the sensor.""" @@ -237,3 +240,18 @@ class SyncThruOutputTraySensor(SyncThruSensor): if tray_state == "": tray_state = "Ready" return tray_state + + +class SyncThruActiveAlertSensor(SyncThruSensor): + """Implementation of a Samsung Printer active alerts sensor platform.""" + + def __init__(self, syncthru, name): + """Initialize the sensor.""" + super().__init__(syncthru, name) + self._name = f"{name} Active Alerts" + self._id_suffix = "_active_alerts" + + @property + def native_value(self): + """Show number of active alerts.""" + return self.syncthru.raw().get("GXI_ACTIVE_ALERT_TOTAL") From d06e064e9ea7a21b79a23de1f5093d928520a036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sat, 8 Oct 2022 20:54:37 +0200 Subject: [PATCH 259/985] Correct unit for Opengarage rssi sensor (#79403) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/opengarage/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/opengarage/sensor.py b/homeassistant/components/opengarage/sensor.py index bf75cd34998..5e9591e8b6c 100644 --- a/homeassistant/components/opengarage/sensor.py +++ b/homeassistant/components/opengarage/sensor.py @@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( LENGTH_CENTIMETERS, PERCENTAGE, - SIGNAL_STRENGTH_DECIBELS, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback @@ -37,7 +37,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( From 7bc2d97aca4f364e8d2d692ef38a8759414a1bae Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 8 Oct 2022 20:55:56 +0200 Subject: [PATCH 260/985] Add Roborock as supported brand of xiaomi miio (#79312) * Add Roborock as supported brand * Update supported_brands.py --- homeassistant/components/xiaomi_miio/manifest.json | 5 ++++- homeassistant/generated/supported_brands.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 0f1a9dd92aa..c84c6edc2e8 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -7,5 +7,8 @@ "codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"], "zeroconf": ["_miio._udp.local."], "iot_class": "local_polling", - "loggers": ["micloud", "miio"] + "loggers": ["micloud", "miio"], + "supported_brands": { + "roborock": "Roborock" + } } diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py index 0efd1982c61..bb996813bba 100644 --- a/homeassistant/generated/supported_brands.py +++ b/homeassistant/generated/supported_brands.py @@ -15,5 +15,6 @@ HAS_SUPPORTED_BRANDS = [ "thermobeacon", "upb", "wemo", + "xiaomi_miio", "yalexs_ble", ] From c81bf1103fa84d325a5571b3c50ad579b285faa4 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 8 Oct 2022 20:57:57 +0200 Subject: [PATCH 261/985] Add supported brands for Motion Blinds (#79301) * Add ScreenAway * Add aditional brands --- homeassistant/components/motion_blinds/manifest.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index e6e4c50c7fe..16f87dcf2ff 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -26,12 +26,16 @@ "bloc_blinds": "Bloc Blinds", "brel_home": "Brel Home", "3_day_blinds": "3 Day Blinds", + "diaz": "Diaz", "dooya": "Dooya", "gaviota": "Gaviota", + "havana_shade": "Havana Shade", "hurrican_shutters_wholesale": "Hurrican Shutters Wholesale", + "inspired_shades": "Inspired Shades", "ismartwindow": "iSmartWindow", "martec": "Martec", "raven_rock_mfg": "Raven Rock MFG", + "screenaway": "ScreenAway", "smart_blinds": "Smart Blinds", "smart_home": "Smart Home", "uprise_smart_shades": "Uprise Smart Shades" From f65dcf3c35a8e863623fb6ac0e430aad3f3d64f5 Mon Sep 17 00:00:00 2001 From: majuss Date: Sat, 8 Oct 2022 20:59:59 +0200 Subject: [PATCH 262/985] Bump lupupy to support XT2 and up (#79289) * Bumped lupupy to support XT2 and up * requirements script --- homeassistant/components/lupusec/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index 53ab1e6af47..cb526b004de 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -2,7 +2,7 @@ "domain": "lupusec", "name": "Lupus Electronics LUPUSEC", "documentation": "https://www.home-assistant.io/integrations/lupusec", - "requirements": ["lupupy==0.0.24"], + "requirements": ["lupupy==0.1.9"], "codeowners": ["@majuss"], "iot_class": "local_polling", "loggers": ["lupupy"] diff --git a/requirements_all.txt b/requirements_all.txt index 6f26737caa0..0f19dc84ea4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1030,7 +1030,7 @@ london-tube-status==0.5 luftdaten==0.7.2 # homeassistant.components.lupusec -lupupy==0.0.24 +lupupy==0.1.9 # homeassistant.components.lw12wifi lw12==0.9.2 From 9019fcb5c54a4e5dd067479fd1109392732d400e Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 8 Oct 2022 22:24:19 +0300 Subject: [PATCH 263/985] Migrate Shelly to use kelvin for color temperature (#79880) --- .coveragerc | 1 - homeassistant/components/shelly/light.py | 56 ++- tests/components/shelly/conftest.py | 39 ++ tests/components/shelly/test_cover.py | 2 +- tests/components/shelly/test_diagnostics.py | 2 +- tests/components/shelly/test_light.py | 385 ++++++++++++++++++++ tests/components/shelly/test_switch.py | 2 +- 7 files changed, 450 insertions(+), 37 deletions(-) create mode 100644 tests/components/shelly/test_light.py diff --git a/.coveragerc b/.coveragerc index cdbfd57024b..01c36d8a836 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1109,7 +1109,6 @@ omit = homeassistant/components/shelly/climate.py homeassistant/components/shelly/coordinator.py homeassistant/components/shelly/entity.py - homeassistant/components/shelly/light.py homeassistant/components/shelly/number.py homeassistant/components/shelly/sensor.py homeassistant/components/shelly/utils.py diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 6c479ebc63f..dda9a41bb89 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -7,7 +7,7 @@ from aioshelly.block_device import Block from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, + ATTR_COLOR_TEMP_KELVIN, ATTR_EFFECT, ATTR_RGB_COLOR, ATTR_RGBW_COLOR, @@ -20,10 +20,6 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util.color import ( - color_temperature_kelvin_to_mired, - color_temperature_mired_to_kelvin, -) from .const import ( DUAL_MODE_LIGHT_MODELS, @@ -49,10 +45,6 @@ from .utils import ( is_rpc_channel_type_light, ) -MIRED_MAX_VALUE_WHITE = color_temperature_kelvin_to_mired(KELVIN_MIN_VALUE_WHITE) -MIRED_MIN_VALUE = color_temperature_kelvin_to_mired(KELVIN_MAX_VALUE) -MIRED_MAX_VALUE_COLOR = color_temperature_kelvin_to_mired(KELVIN_MIN_VALUE_COLOR) - async def async_setup_entry( hass: HomeAssistant, @@ -133,14 +125,11 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): super().__init__(coordinator, block) self.control_result: dict[str, Any] | None = None self._attr_supported_color_modes = set() - self._attr_min_mireds = MIRED_MIN_VALUE - self._min_kelvin: int = KELVIN_MIN_VALUE_WHITE - self._attr_max_mireds = MIRED_MAX_VALUE_WHITE - self._max_kelvin: int = KELVIN_MAX_VALUE + self._attr_min_color_temp_kelvin = KELVIN_MIN_VALUE_WHITE + self._attr_max_color_temp_kelvin = KELVIN_MAX_VALUE if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"): - self._attr_max_mireds = MIRED_MAX_VALUE_COLOR - self._min_kelvin = KELVIN_MIN_VALUE_COLOR + self._attr_min_color_temp_kelvin = KELVIN_MIN_VALUE_COLOR if coordinator.model in RGBW_MODELS: self._attr_supported_color_modes.add(ColorMode.RGBW) else: @@ -248,23 +237,20 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): return (*self.rgb_color, white) @property - def color_temp(self) -> int: - """Return the CT color value in mireds.""" + def color_temp_kelvin(self) -> int: + """Return the CT color value in kelvin.""" + color_temp = cast(int, self.block.colorTemp) if self.control_result: color_temp = self.control_result["temp"] - else: - color_temp = self.block.colorTemp - color_temp = min(self._max_kelvin, max(self._min_kelvin, color_temp)) - - return int(color_temperature_kelvin_to_mired(color_temp)) + return min( + self.max_color_temp_kelvin, + max(self.min_color_temp_kelvin, color_temp), + ) @property def effect_list(self) -> list[str] | None: """Return the list of supported effects.""" - if not self.supported_features & LightEntityFeature.EFFECT: - return None - if self.coordinator.model == "SHBLB-1": return list(SHBLB_1_RGB_EFFECTS.values()) @@ -273,9 +259,6 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): @property def effect(self) -> str | None: """Return the current effect.""" - if not self.supported_features & LightEntityFeature.EFFECT: - return None - if self.control_result: effect_index = self.control_result["effect"] else: @@ -309,12 +292,19 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): if hasattr(self.block, "brightness"): params["brightness"] = brightness_pct - if ATTR_COLOR_TEMP in kwargs and ColorMode.COLOR_TEMP in supported_color_modes: - color_temp = color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) - color_temp = min(self._max_kelvin, max(self._min_kelvin, color_temp)) + if ( + ATTR_COLOR_TEMP_KELVIN in kwargs + and ColorMode.COLOR_TEMP in supported_color_modes + ): # Color temperature change - used only in white mode, switch device mode to white + color_temp = kwargs[ATTR_COLOR_TEMP_KELVIN] set_mode = "white" - params["temp"] = int(color_temp) + params["temp"] = int( + min( + self.max_color_temp_kelvin, + max(self.min_color_temp_kelvin, color_temp), + ) + ) if ATTR_RGB_COLOR in kwargs and ColorMode.RGB in supported_color_modes: # Color channels change - used only in color mode, switch device mode to color @@ -328,7 +318,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): ATTR_RGBW_COLOR ] - if ATTR_EFFECT in kwargs and ATTR_COLOR_TEMP not in kwargs: + if ATTR_EFFECT in kwargs and ATTR_COLOR_TEMP_KELVIN not in kwargs: # Color effect change - used only in color mode, switch device mode to color set_mode = "color" if self.coordinator.model == "SHBLB-1": diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 8890201ad6d..cca4aebb9ea 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -25,6 +25,36 @@ MOCK_SETTINGS = { "rollers": [{"positioning": True}], } + +def mock_light_set_state( + turn="on", + mode="color", + red=45, + green=55, + blue=65, + white=70, + gain=19, + temp=4050, + brightness=50, + effect=0, + transition=0, +): + """Mock light block set_state.""" + return { + "ison": turn == "on", + "mode": mode, + "red": red, + "green": green, + "blue": blue, + "white": white, + "gain": gain, + "temp": temp, + "brightness": brightness, + "effect": effect, + "transition": transition, + } + + MOCK_BLOCKS = [ Mock( sensor_ids={"inputEvent": "S", "inputEventCnt": 2}, @@ -43,6 +73,15 @@ MOCK_BLOCKS = [ } ), ), + Mock( + sensor_ids={}, + channel="0", + output=mock_light_set_state()["ison"], + colorTemp=mock_light_set_state()["temp"], + **mock_light_set_state(), + type="light", + set_state=AsyncMock(side_effect=mock_light_set_state), + ), ] MOCK_CONFIG = { diff --git a/tests/components/shelly/test_cover.py b/tests/components/shelly/test_cover.py index 3a032a9de20..51fef7dc030 100644 --- a/tests/components/shelly/test_cover.py +++ b/tests/components/shelly/test_cover.py @@ -1,4 +1,4 @@ -"""The scene tests for the myq platform.""" +"""Tests for Shelly cover platform.""" from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, diff --git a/tests/components/shelly/test_diagnostics.py b/tests/components/shelly/test_diagnostics.py index 93d56027fab..a99b28d48e0 100644 --- a/tests/components/shelly/test_diagnostics.py +++ b/tests/components/shelly/test_diagnostics.py @@ -1,4 +1,4 @@ -"""The scene tests for the myq platform.""" +"""Tests for Shelly diagnostics platform.""" from aiohttp import ClientSession from homeassistant.components.diagnostics import REDACTED diff --git a/tests/components/shelly/test_light.py b/tests/components/shelly/test_light.py new file mode 100644 index 00000000000..b0162f43e13 --- /dev/null +++ b/tests/components/shelly/test_light.py @@ -0,0 +1,385 @@ +"""Tests for Shelly light platform.""" + +import pytest + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, + ATTR_COLOR_TEMP_KELVIN, + ATTR_EFFECT, + ATTR_EFFECT_LIST, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_SUPPORTED_COLOR_MODES, + ATTR_TRANSITION, + DOMAIN as LIGHT_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + ColorMode, + LightEntityFeature, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + STATE_OFF, + STATE_ON, +) + +from . import init_integration + +RELAY_BLOCK_ID = 0 +LIGHT_BLOCK_ID = 2 + + +async def test_block_device_rgbw_bulb(hass, mock_block_device): + """Test block device RGBW bulb.""" + await init_integration(hass, 1, model="SHBLB-1") + + # Test initial + state = hass.states.get("light.test_name_channel_1") + attributes = state.attributes + assert state.state == STATE_ON + assert attributes[ATTR_RGBW_COLOR] == (45, 55, 65, 70) + assert attributes[ATTR_BRIGHTNESS] == 48 + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.COLOR_TEMP, + ColorMode.RGBW, + ] + assert attributes[ATTR_SUPPORTED_FEATURES] == LightEntityFeature.EFFECT + assert len(attributes[ATTR_EFFECT_LIST]) == 7 + assert attributes[ATTR_EFFECT] == "Off" + + # Turn off + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.test_name_channel_1"}, + blocking=True, + ) + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( + turn="off" + ) + state = hass.states.get("light.test_name_channel_1") + assert state.state == STATE_OFF + + # Turn on, RGBW = [70, 80, 90, 20], brightness = 33, effect = Flash + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.test_name_channel_1", + ATTR_RGBW_COLOR: [70, 80, 90, 30], + ATTR_BRIGHTNESS: 33, + ATTR_EFFECT: "Flash", + }, + blocking=True, + ) + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( + turn="on", gain=13, brightness=13, red=70, green=80, blue=90, white=30, effect=3 + ) + state = hass.states.get("light.test_name_channel_1") + attributes = state.attributes + assert state.state == STATE_ON + assert attributes[ATTR_COLOR_MODE] == ColorMode.RGBW + assert attributes[ATTR_RGBW_COLOR] == (70, 80, 90, 30) + assert attributes[ATTR_BRIGHTNESS] == 33 + assert attributes[ATTR_EFFECT] == "Flash" + + # Turn on, COLOR_TEMP_KELVIN = 3500 + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_name_channel_1", ATTR_COLOR_TEMP_KELVIN: 3500}, + blocking=True, + ) + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( + turn="on", temp=3500, mode="white" + ) + state = hass.states.get("light.test_name_channel_1") + attributes = state.attributes + assert state.state == STATE_ON + assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP + assert attributes[ATTR_COLOR_TEMP_KELVIN] == 3500 + + +async def test_block_device_rgb_bulb(hass, mock_block_device, monkeypatch, caplog): + """Test block device RGB bulb.""" + monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "mode") + await init_integration(hass, 1, model="SHCB-1") + + # Test initial + state = hass.states.get("light.test_name_channel_1") + attributes = state.attributes + assert state.state == STATE_ON + assert attributes[ATTR_RGB_COLOR] == (45, 55, 65) + assert attributes[ATTR_BRIGHTNESS] == 48 + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.COLOR_TEMP, + ColorMode.RGB, + ] + assert attributes[ATTR_SUPPORTED_FEATURES] == LightEntityFeature.EFFECT + assert len(attributes[ATTR_EFFECT_LIST]) == 4 + assert attributes[ATTR_EFFECT] == "Off" + + # Turn off + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.test_name_channel_1"}, + blocking=True, + ) + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( + turn="off" + ) + state = hass.states.get("light.test_name_channel_1") + assert state.state == STATE_OFF + + # Turn on, RGB = [70, 80, 90], brightness = 33, effect = Flash + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.test_name_channel_1", + ATTR_RGB_COLOR: [70, 80, 90], + ATTR_BRIGHTNESS: 33, + ATTR_EFFECT: "Flash", + }, + blocking=True, + ) + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( + turn="on", gain=13, brightness=13, red=70, green=80, blue=90, effect=3 + ) + state = hass.states.get("light.test_name_channel_1") + attributes = state.attributes + assert state.state == STATE_ON + assert attributes[ATTR_COLOR_MODE] == ColorMode.RGB + assert attributes[ATTR_RGB_COLOR] == (70, 80, 90) + assert attributes[ATTR_BRIGHTNESS] == 33 + assert attributes[ATTR_EFFECT] == "Flash" + + # Turn on, COLOR_TEMP_KELVIN = 3500 + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_name_channel_1", ATTR_COLOR_TEMP_KELVIN: 3500}, + blocking=True, + ) + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( + turn="on", temp=3500, mode="white" + ) + state = hass.states.get("light.test_name_channel_1") + attributes = state.attributes + assert state.state == STATE_ON + assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP + assert attributes[ATTR_COLOR_TEMP_KELVIN] == 3500 + + # Turn on with unsupported effect + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_name_channel_1", ATTR_EFFECT: "Breath"}, + blocking=True, + ) + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( + turn="on", mode="color" + ) + state = hass.states.get("light.test_name_channel_1") + attributes = state.attributes + assert state.state == STATE_ON + assert attributes[ATTR_EFFECT] == "Off" + assert "Effect 'Breath' not supported" in caplog.text + + +async def test_block_device_white_bulb(hass, mock_block_device, monkeypatch, caplog): + """Test block device white bulb.""" + monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "red") + monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "green") + monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "blue") + monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "mode") + monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "colorTemp") + monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "effect") + await init_integration(hass, 1, model="SHVIN-1") + + # Test initial + state = hass.states.get("light.test_name_channel_1") + attributes = state.attributes + assert state.state == STATE_ON + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.BRIGHTNESS] + assert attributes[ATTR_SUPPORTED_FEATURES] == 0 + + # Turn off + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.test_name_channel_1"}, + blocking=True, + ) + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( + turn="off" + ) + state = hass.states.get("light.test_name_channel_1") + assert state.state == STATE_OFF + + # Turn on, brightness = 33 + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_name_channel_1", ATTR_BRIGHTNESS: 33}, + blocking=True, + ) + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( + turn="on", gain=13, brightness=13 + ) + state = hass.states.get("light.test_name_channel_1") + attributes = state.attributes + assert state.state == STATE_ON + assert attributes[ATTR_BRIGHTNESS] == 33 + + +@pytest.mark.parametrize( + "model", + [ + "SHBDUO-1", + "SHCB-1", + "SHDM-1", + "SHDM-2", + "SHRGBW2", + "SHVIN-1", + ], +) +async def test_block_device_support_transition( + hass, mock_block_device, model, monkeypatch +): + """Test block device supports transition.""" + monkeypatch.setitem( + mock_block_device.settings, "fw", "20220809-122808/v1.12-g99f7e0b" + ) + await init_integration(hass, 1, model=model) + + # Test initial + state = hass.states.get("light.test_name_channel_1") + attributes = state.attributes + assert attributes[ATTR_SUPPORTED_FEATURES] & LightEntityFeature.TRANSITION + + # Turn on, TRANSITION = 4 + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_name_channel_1", ATTR_TRANSITION: 4}, + blocking=True, + ) + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( + turn="on", transition=4000 + ) + state = hass.states.get("light.test_name_channel_1") + assert state.state == STATE_ON + + # Turn off, TRANSITION = 6, limit to 5000ms + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.test_name_channel_1", ATTR_TRANSITION: 6}, + blocking=True, + ) + mock_block_device.blocks[LIGHT_BLOCK_ID].set_state.assert_called_once_with( + turn="off", transition=5000 + ) + state = hass.states.get("light.test_name_channel_1") + assert state.state == STATE_OFF + + +async def test_block_device_relay_app_type_light(hass, mock_block_device, monkeypatch): + """Test block device relay in app type set to light mode.""" + monkeypatch.delattr(mock_block_device.blocks[RELAY_BLOCK_ID], "red") + monkeypatch.delattr(mock_block_device.blocks[RELAY_BLOCK_ID], "green") + monkeypatch.delattr(mock_block_device.blocks[RELAY_BLOCK_ID], "blue") + monkeypatch.delattr(mock_block_device.blocks[RELAY_BLOCK_ID], "mode") + monkeypatch.delattr(mock_block_device.blocks[RELAY_BLOCK_ID], "gain") + monkeypatch.delattr(mock_block_device.blocks[RELAY_BLOCK_ID], "brightness") + monkeypatch.delattr(mock_block_device.blocks[RELAY_BLOCK_ID], "effect") + monkeypatch.delattr(mock_block_device.blocks[RELAY_BLOCK_ID], "colorTemp") + monkeypatch.setitem( + mock_block_device.settings["relays"][RELAY_BLOCK_ID], "appliance_type", "light" + ) + await init_integration(hass, 1) + assert hass.states.get("switch.test_name_channel_1") is None + + # Test initial + state = hass.states.get("light.test_name_channel_1") + attributes = state.attributes + assert state.state == STATE_ON + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.ONOFF] + assert attributes[ATTR_SUPPORTED_FEATURES] == 0 + + # Turn off + mock_block_device.blocks[RELAY_BLOCK_ID].set_state.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.test_name_channel_1"}, + blocking=True, + ) + mock_block_device.blocks[RELAY_BLOCK_ID].set_state.assert_called_once_with( + turn="off" + ) + state = hass.states.get("light.test_name_channel_1") + assert state.state == STATE_OFF + + # Turn on + mock_block_device.blocks[RELAY_BLOCK_ID].set_state.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_name_channel_1"}, + blocking=True, + ) + mock_block_device.blocks[RELAY_BLOCK_ID].set_state.assert_called_once_with( + turn="on" + ) + state = hass.states.get("light.test_name_channel_1") + assert state.state == STATE_ON + + +async def test_block_device_no_light_blocks(hass, mock_block_device, monkeypatch): + """Test block device without light blocks.""" + monkeypatch.setattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "type", "roller") + await init_integration(hass, 1) + assert hass.states.get("light.test_name_channel_1") is None + + +async def test_rpc_device_switch_type_lights_mode(hass, mock_rpc_device, monkeypatch): + """Test RPC device with switch in consumption type lights mode.""" + monkeypatch.setitem( + mock_rpc_device.config["sys"]["ui_data"], "consumption_types", ["lights"] + ) + await init_integration(hass, 2) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_switch_0"}, + blocking=True, + ) + assert hass.states.get("light.test_switch_0").state == STATE_ON + + monkeypatch.setitem(mock_rpc_device.status["switch:0"], "output", False) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.test_switch_0"}, + blocking=True, + ) + mock_rpc_device.mock_update() + assert hass.states.get("light.test_switch_0").state == STATE_OFF diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index c2c7d90943d..458de9c655b 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -1,4 +1,4 @@ -"""The scene tests for the myq platform.""" +"""Tests for Shelly switch platform.""" from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, From 1e13433d4f942a0e71c84eaae0259aaec521b31d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 8 Oct 2022 19:25:58 +0000 Subject: [PATCH 264/985] Rework Brother sensor platform (#79864) * Rework BrotherSensorEntityDescription * Rework state attributes * Cleaning * Add _handle_coordinator_update() * Suggested change * Re-add consts --- homeassistant/components/brother/sensor.py | 584 ++++++++++----------- 1 file changed, 291 insertions(+), 293 deletions(-) diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 86f1d2d40ec..73d7c2710b5 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -1,10 +1,13 @@ """Support for the Brother service.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from datetime import datetime import logging -from typing import Any, cast +from typing import Any + +from brother import BrotherSensors from homeassistant.components.sensor import ( DOMAIN as PLATFORM, @@ -15,7 +18,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, PERCENTAGE -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -25,69 +28,285 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import BrotherDataUpdateCoordinator from .const import DATA_CONFIG_ENTRY, DOMAIN -ATTR_BELT_UNIT_REMAINING_LIFE = "belt_unit_remaining_life" -ATTR_BLACK_DRUM_COUNTER = "black_drum_counter" -ATTR_BLACK_DRUM_REMAINING_LIFE = "black_drum_remaining_life" -ATTR_BLACK_DRUM_REMAINING_PAGES = "black_drum_remaining_pages" -ATTR_BLACK_INK_REMAINING = "black_ink_remaining" -ATTR_BLACK_TONER_REMAINING = "black_toner_remaining" -ATTR_BW_COUNTER = "bw_counter" -ATTR_COLOR_COUNTER = "color_counter" ATTR_COUNTER = "counter" -ATTR_CYAN_DRUM_COUNTER = "cyan_drum_counter" -ATTR_CYAN_DRUM_REMAINING_LIFE = "cyan_drum_remaining_life" -ATTR_CYAN_DRUM_REMAINING_PAGES = "cyan_drum_remaining_pages" -ATTR_CYAN_INK_REMAINING = "cyan_ink_remaining" -ATTR_CYAN_TONER_REMAINING = "cyan_toner_remaining" -ATTR_DRUM_COUNTER = "drum_counter" -ATTR_DRUM_REMAINING_LIFE = "drum_remaining_life" -ATTR_DRUM_REMAINING_PAGES = "drum_remaining_pages" -ATTR_DUPLEX_COUNTER = "duplex_unit_pages_counter" -ATTR_FUSER_REMAINING_LIFE = "fuser_remaining_life" -ATTR_LASER_REMAINING_LIFE = "laser_remaining_life" -ATTR_MAGENTA_DRUM_COUNTER = "magenta_drum_counter" -ATTR_MAGENTA_DRUM_REMAINING_LIFE = "magenta_drum_remaining_life" -ATTR_MAGENTA_DRUM_REMAINING_PAGES = "magenta_drum_remaining_pages" -ATTR_MAGENTA_INK_REMAINING = "magenta_ink_remaining" -ATTR_MAGENTA_TONER_REMAINING = "magenta_toner_remaining" -ATTR_MANUFACTURER = "Brother" -ATTR_PAGE_COUNTER = "page_counter" -ATTR_PF_KIT_1_REMAINING_LIFE = "pf_kit_1_remaining_life" -ATTR_PF_KIT_MP_REMAINING_LIFE = "pf_kit_mp_remaining_life" ATTR_REMAINING_PAGES = "remaining_pages" -ATTR_STATUS = "status" -ATTR_UPTIME = "uptime" -ATTR_YELLOW_DRUM_COUNTER = "yellow_drum_counter" -ATTR_YELLOW_DRUM_REMAINING_LIFE = "yellow_drum_remaining_life" -ATTR_YELLOW_DRUM_REMAINING_PAGES = "yellow_drum_remaining_pages" -ATTR_YELLOW_INK_REMAINING = "yellow_ink_remaining" -ATTR_YELLOW_TONER_REMAINING = "yellow_toner_remaining" UNIT_PAGES = "p" -ATTRS_MAP: dict[str, tuple[str, str]] = { - ATTR_DRUM_REMAINING_LIFE: (ATTR_DRUM_REMAINING_PAGES, ATTR_DRUM_COUNTER), - ATTR_BLACK_DRUM_REMAINING_LIFE: ( - ATTR_BLACK_DRUM_REMAINING_PAGES, - ATTR_BLACK_DRUM_COUNTER, - ), - ATTR_CYAN_DRUM_REMAINING_LIFE: ( - ATTR_CYAN_DRUM_REMAINING_PAGES, - ATTR_CYAN_DRUM_COUNTER, - ), - ATTR_MAGENTA_DRUM_REMAINING_LIFE: ( - ATTR_MAGENTA_DRUM_REMAINING_PAGES, - ATTR_MAGENTA_DRUM_COUNTER, - ), - ATTR_YELLOW_DRUM_REMAINING_LIFE: ( - ATTR_YELLOW_DRUM_REMAINING_PAGES, - ATTR_YELLOW_DRUM_COUNTER, - ), -} - _LOGGER = logging.getLogger(__name__) +@dataclass +class BrotherSensorRequiredKeysMixin: + """Class for Brother entity required keys.""" + + value: Callable[[BrotherSensors], StateType | datetime] + extra_state_attrs: Callable[[BrotherSensors], dict[str, Any]] + + +@dataclass +class BrotherSensorEntityDescription( + SensorEntityDescription, BrotherSensorRequiredKeysMixin +): + """A class that describes sensor entities.""" + + +SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( + BrotherSensorEntityDescription( + key="status", + icon="mdi:printer", + name="Status", + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.status, + extra_state_attrs=lambda _: {}, + ), + BrotherSensorEntityDescription( + key="page_counter", + icon="mdi:file-document-outline", + name="Page counter", + native_unit_of_measurement=UNIT_PAGES, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.page_counter, + extra_state_attrs=lambda _: {}, + ), + BrotherSensorEntityDescription( + key="bw_counter", + icon="mdi:file-document-outline", + name="B/W counter", + native_unit_of_measurement=UNIT_PAGES, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.bw_counter, + extra_state_attrs=lambda _: {}, + ), + BrotherSensorEntityDescription( + key="color_counter", + icon="mdi:file-document-outline", + name="Color counter", + native_unit_of_measurement=UNIT_PAGES, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.color_counter, + extra_state_attrs=lambda _: {}, + ), + BrotherSensorEntityDescription( + key="duplex_unit_pages_counter", + icon="mdi:file-document-outline", + name="Duplex unit pages counter", + native_unit_of_measurement=UNIT_PAGES, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.duplex_unit_pages_counter, + extra_state_attrs=lambda _: {}, + ), + BrotherSensorEntityDescription( + key="drum_remaining_life", + icon="mdi:chart-donut", + name="Drum remaining life", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.drum_remaining_life, + extra_state_attrs=lambda data: { + ATTR_REMAINING_PAGES: data.drum_remaining_pages, + ATTR_COUNTER: data.drum_counter, + }, + ), + BrotherSensorEntityDescription( + key="black_drum_remaining_life", + icon="mdi:chart-donut", + name="Black drum remaining life", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.black_drum_remaining_life, + extra_state_attrs=lambda data: { + ATTR_REMAINING_PAGES: data.black_drum_remaining_pages, + ATTR_COUNTER: data.black_drum_counter, + }, + ), + BrotherSensorEntityDescription( + key="cyan_drum_remaining_life", + icon="mdi:chart-donut", + name="Cyan drum remaining life", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.cyan_drum_remaining_life, + extra_state_attrs=lambda data: { + ATTR_REMAINING_PAGES: data.cyan_drum_remaining_pages, + ATTR_COUNTER: data.cyan_drum_counter, + }, + ), + BrotherSensorEntityDescription( + key="magenta_drum_remaining_life", + icon="mdi:chart-donut", + name="Magenta drum remaining life", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.magenta_drum_remaining_life, + extra_state_attrs=lambda data: { + ATTR_REMAINING_PAGES: data.magenta_drum_remaining_pages, + ATTR_COUNTER: data.magenta_drum_counter, + }, + ), + BrotherSensorEntityDescription( + key="yellow_drum_remaining_life", + icon="mdi:chart-donut", + name="Yellow drum remaining life", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.yellow_drum_remaining_life, + extra_state_attrs=lambda data: { + ATTR_REMAINING_PAGES: data.yellow_drum_remaining_pages, + ATTR_COUNTER: data.yellow_drum_counter, + }, + ), + BrotherSensorEntityDescription( + key="belt_unit_remaining_life", + icon="mdi:current-ac", + name="Belt unit remaining life", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.belt_unit_remaining_life, + extra_state_attrs=lambda _: {}, + ), + BrotherSensorEntityDescription( + key="fuser_remaining_life", + icon="mdi:water-outline", + name="Fuser remaining life", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.fuser_remaining_life, + extra_state_attrs=lambda _: {}, + ), + BrotherSensorEntityDescription( + key="laser_remaining_life", + icon="mdi:spotlight-beam", + name="Laser remaining life", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.laser_remaining_life, + extra_state_attrs=lambda _: {}, + ), + BrotherSensorEntityDescription( + key="pf_kit_1_remaining_life", + icon="mdi:printer-3d", + name="PF Kit 1 remaining life", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.pf_kit_1_remaining_life, + extra_state_attrs=lambda _: {}, + ), + BrotherSensorEntityDescription( + key="pf_kit_mp_remaining_life", + icon="mdi:printer-3d", + name="PF Kit MP remaining life", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.pf_kit_mp_remaining_life, + extra_state_attrs=lambda _: {}, + ), + BrotherSensorEntityDescription( + key="black_toner_remaining", + icon="mdi:printer-3d-nozzle", + name="Black toner remaining", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.black_toner_remaining, + extra_state_attrs=lambda _: {}, + ), + BrotherSensorEntityDescription( + key="cyan_toner_remaining", + icon="mdi:printer-3d-nozzle", + name="Cyan toner remaining", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.cyan_toner_remaining, + extra_state_attrs=lambda _: {}, + ), + BrotherSensorEntityDescription( + key="magenta_toner_remaining", + icon="mdi:printer-3d-nozzle", + name="Magenta toner remaining", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.magenta_toner_remaining, + extra_state_attrs=lambda _: {}, + ), + BrotherSensorEntityDescription( + key="yellow_toner_remaining", + icon="mdi:printer-3d-nozzle", + name="Yellow toner remaining", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.yellow_toner_remaining, + extra_state_attrs=lambda _: {}, + ), + BrotherSensorEntityDescription( + key="black_ink_remaining", + icon="mdi:printer-3d-nozzle", + name="Black ink remaining", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.black_ink_remaining, + extra_state_attrs=lambda _: {}, + ), + BrotherSensorEntityDescription( + key="cyan_ink_remaining", + icon="mdi:printer-3d-nozzle", + name="Cyan ink remaining", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.cyan_ink_remaining, + extra_state_attrs=lambda _: {}, + ), + BrotherSensorEntityDescription( + key="magenta_ink_remaining", + icon="mdi:printer-3d-nozzle", + name="Magenta ink remaining", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.magenta_ink_remaining, + extra_state_attrs=lambda _: {}, + ), + BrotherSensorEntityDescription( + key="yellow_ink_remaining", + icon="mdi:printer-3d-nozzle", + name="Yellow ink remaining", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.yellow_ink_remaining, + extra_state_attrs=lambda _: {}, + ), + BrotherSensorEntityDescription( + key="uptime", + name="Uptime", + entity_registry_enabled_default=False, + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.uptime, + extra_state_attrs=lambda _: {}, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -115,17 +334,15 @@ async def async_setup_entry( device_info = DeviceInfo( configuration_url=f"http://{entry.data[CONF_HOST]}/", identifiers={(DOMAIN, coordinator.data.serial)}, - manufacturer=ATTR_MANUFACTURER, + manufacturer="Brother", model=coordinator.data.model, name=coordinator.data.model, sw_version=coordinator.data.firmware, ) for description in SENSOR_TYPES: - if getattr(coordinator.data, description.key) is not None: - sensors.append( - description.entity_class(coordinator, description, device_info) - ) + if description.value(coordinator.data) is not None: + sensors.append(BrotherPrinterSensor(coordinator, description, device_info)) async_add_entities(sensors, False) @@ -133,6 +350,7 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): """Define an Brother Printer sensor.""" _attr_has_entity_name = True + entity_description: BrotherSensorEntityDescription def __init__( self, @@ -142,239 +360,19 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): ) -> None: """Initialize.""" super().__init__(coordinator) - self._attrs: dict[str, Any] = {} self._attr_device_info = device_info + self._attr_extra_state_attributes = description.extra_state_attrs( + coordinator.data + ) + self._attr_native_value = description.value(coordinator.data) self._attr_unique_id = f"{coordinator.data.serial.lower()}_{description.key}" self.entity_description = description - @property - def native_value(self) -> StateType | datetime: - """Return the state.""" - return cast( - StateType, getattr(self.coordinator.data, self.entity_description.key) + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_native_value = self.entity_description.value(self.coordinator.data) + self._attr_extra_state_attributes = self.entity_description.extra_state_attrs( + self.coordinator.data ) - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the state attributes.""" - remaining_pages, drum_counter = ATTRS_MAP.get( - self.entity_description.key, (None, None) - ) - if remaining_pages and drum_counter: - self._attrs[ATTR_REMAINING_PAGES] = getattr( - self.coordinator.data, remaining_pages - ) - self._attrs[ATTR_COUNTER] = getattr(self.coordinator.data, drum_counter) - return self._attrs - - -class BrotherPrinterUptimeSensor(BrotherPrinterSensor): - """Define an Brother Printer Uptime sensor.""" - - @property - def native_value(self) -> datetime: - """Return the state.""" - return cast( - datetime, getattr(self.coordinator.data, self.entity_description.key) - ) - - -@dataclass -class BrotherSensorEntityDescription(SensorEntityDescription): - """A class that describes sensor entities.""" - - entity_class: type[BrotherPrinterSensor] = BrotherPrinterSensor - - -SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( - BrotherSensorEntityDescription( - key=ATTR_STATUS, - icon="mdi:printer", - name="Status", - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_PAGE_COUNTER, - icon="mdi:file-document-outline", - name="Page counter", - native_unit_of_measurement=UNIT_PAGES, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_BW_COUNTER, - icon="mdi:file-document-outline", - name="B/W counter", - native_unit_of_measurement=UNIT_PAGES, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_COLOR_COUNTER, - icon="mdi:file-document-outline", - name="Color counter", - native_unit_of_measurement=UNIT_PAGES, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_DUPLEX_COUNTER, - icon="mdi:file-document-outline", - name="Duplex unit pages counter", - native_unit_of_measurement=UNIT_PAGES, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_DRUM_REMAINING_LIFE, - icon="mdi:chart-donut", - name="Drum remaining life", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_BLACK_DRUM_REMAINING_LIFE, - icon="mdi:chart-donut", - name="Black drum remaining life", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_CYAN_DRUM_REMAINING_LIFE, - icon="mdi:chart-donut", - name="Cyan drum remaining life", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_MAGENTA_DRUM_REMAINING_LIFE, - icon="mdi:chart-donut", - name="Magenta drum remaining life", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_YELLOW_DRUM_REMAINING_LIFE, - icon="mdi:chart-donut", - name="Yellow drum remaining life", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_BELT_UNIT_REMAINING_LIFE, - icon="mdi:current-ac", - name="Belt unit remaining life", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_FUSER_REMAINING_LIFE, - icon="mdi:water-outline", - name="Fuser remaining life", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_LASER_REMAINING_LIFE, - icon="mdi:spotlight-beam", - name="Laser remaining life", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_PF_KIT_1_REMAINING_LIFE, - icon="mdi:printer-3d", - name="PF Kit 1 remaining life", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_PF_KIT_MP_REMAINING_LIFE, - icon="mdi:printer-3d", - name="PF Kit MP remaining life", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_BLACK_TONER_REMAINING, - icon="mdi:printer-3d-nozzle", - name="Black toner remaining", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_CYAN_TONER_REMAINING, - icon="mdi:printer-3d-nozzle", - name="Cyan toner remaining", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_MAGENTA_TONER_REMAINING, - icon="mdi:printer-3d-nozzle", - name="Magenta toner remaining", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_YELLOW_TONER_REMAINING, - icon="mdi:printer-3d-nozzle", - name="Yellow toner remaining", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_BLACK_INK_REMAINING, - icon="mdi:printer-3d-nozzle", - name="Black ink remaining", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_CYAN_INK_REMAINING, - icon="mdi:printer-3d-nozzle", - name="Cyan ink remaining", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_MAGENTA_INK_REMAINING, - icon="mdi:printer-3d-nozzle", - name="Magenta ink remaining", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_YELLOW_INK_REMAINING, - icon="mdi:printer-3d-nozzle", - name="Yellow ink remaining", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - BrotherSensorEntityDescription( - key=ATTR_UPTIME, - name="Uptime", - entity_registry_enabled_default=False, - device_class=SensorDeviceClass.TIMESTAMP, - entity_category=EntityCategory.DIAGNOSTIC, - entity_class=BrotherPrinterUptimeSensor, - ), -) + self.async_write_ha_state() From e5a532629855fc5ba0072296e9e9bab79eccf35b Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 8 Oct 2022 15:40:25 -0400 Subject: [PATCH 265/985] Bump ZHA dependencies (#79898) --- homeassistant/components/zha/manifest.json | 10 +++++----- requirements_all.txt | 10 +++++----- requirements_test_all.txt | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 803a7daabbe..426ac24bbe3 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,15 +4,15 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.34.1", + "bellows==0.34.2", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.82", "zigpy-deconz==0.19.0", - "zigpy==0.51.2", - "zigpy-xbee==0.16.0", - "zigpy-zigate==0.10.0", - "zigpy-znp==0.9.0" + "zigpy==0.51.3", + "zigpy-xbee==0.16.1", + "zigpy-zigate==0.10.1", + "zigpy-znp==0.9.1" ], "usb": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 0f19dc84ea4..62ae0760223 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.34.1 +bellows==0.34.2 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.4 @@ -2607,16 +2607,16 @@ ziggo-mediabox-xl==1.1.0 zigpy-deconz==0.19.0 # homeassistant.components.zha -zigpy-xbee==0.16.0 +zigpy-xbee==0.16.1 # homeassistant.components.zha -zigpy-zigate==0.10.0 +zigpy-zigate==0.10.1 # homeassistant.components.zha -zigpy-znp==0.9.0 +zigpy-znp==0.9.1 # homeassistant.components.zha -zigpy==0.51.2 +zigpy==0.51.3 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec4725d98c2..a8a4cbbc1b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -331,7 +331,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.34.1 +bellows==0.34.2 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.4 @@ -1802,16 +1802,16 @@ zha-quirks==0.0.82 zigpy-deconz==0.19.0 # homeassistant.components.zha -zigpy-xbee==0.16.0 +zigpy-xbee==0.16.1 # homeassistant.components.zha -zigpy-zigate==0.10.0 +zigpy-zigate==0.10.1 # homeassistant.components.zha -zigpy-znp==0.9.0 +zigpy-znp==0.9.1 # homeassistant.components.zha -zigpy==0.51.2 +zigpy==0.51.3 # homeassistant.components.zwave_js zwave-js-server-python==0.43.0 From 50911af835ba267d2ef3848e07ade40a03c4ad1b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 8 Oct 2022 15:05:45 -0600 Subject: [PATCH 266/985] Remove redundant OpenUV test fixture (#79905) --- tests/components/openuv/conftest.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/components/openuv/conftest.py b/tests/components/openuv/conftest.py index c39a84b8b4c..8d9c1efb1b1 100644 --- a/tests/components/openuv/conftest.py +++ b/tests/components/openuv/conftest.py @@ -17,11 +17,11 @@ from tests.common import MockConfigEntry, load_fixture @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config, unique_id): +def config_entry_fixture(hass, config): """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id=unique_id, + unique_id=f"{config[CONF_LATITUDE]}, {config[CONF_LONGITUDE]}", data=config, options={CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 3.5}, ) @@ -68,9 +68,3 @@ async def setup_openuv_fixture(hass, config, data_protection_window, data_uv_ind assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() yield - - -@pytest.fixture(name="unique_id") -def unique_id_fixture(hass): - """Define a config entry unique ID fixture.""" - return "51.528308, -0.3817765" From b4d525f6a37a59ac7ea45c9408f6650d61f9f6fe Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 8 Oct 2022 15:05:57 -0600 Subject: [PATCH 267/985] Remove redundant Ridwell test fixture (#79906) --- tests/components/ridwell/conftest.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/components/ridwell/conftest.py b/tests/components/ridwell/conftest.py index 221ce5be8d2..c4ff094638b 100644 --- a/tests/components/ridwell/conftest.py +++ b/tests/components/ridwell/conftest.py @@ -47,9 +47,9 @@ def client_fixture(account): @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config, unique_id): +def config_entry_fixture(hass, config): """Define a config entry fixture.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=unique_id, data=config) + entry = MockConfigEntry(domain=DOMAIN, unique_id=config[CONF_USERNAME], data=config) entry.add_to_hass(hass) return entry @@ -77,9 +77,3 @@ async def setup_ridwell_fixture(hass, client, config): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() yield - - -@pytest.fixture(name="unique_id") -def unique_id_fixture(hass): - """Define a config entry unique ID fixture.""" - return "user@email.com" From 5e32fdff266b546a6190a898fbab375051e01448 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 8 Oct 2022 15:06:10 -0600 Subject: [PATCH 268/985] Remove redundant ReCollect Waste test fixture (#79907) --- tests/components/recollect_waste/conftest.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/components/recollect_waste/conftest.py b/tests/components/recollect_waste/conftest.py index a0d002e9d9a..9373a9aa969 100644 --- a/tests/components/recollect_waste/conftest.py +++ b/tests/components/recollect_waste/conftest.py @@ -16,9 +16,13 @@ from tests.common import MockConfigEntry @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config, unique_id): +def config_entry_fixture(hass, config): """Define a config entry fixture.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=unique_id, data=config) + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=f"{config[CONF_PLACE_ID]}, {config[CONF_SERVICE_ID]}", + data=config, + ) entry.add_to_hass(hass) return entry @@ -51,9 +55,3 @@ async def setup_recollect_waste_fixture(hass, config): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() yield - - -@pytest.fixture(name="unique_id") -def unique_id_fixture(hass): - """Define a config entry unique ID fixture.""" - return "12345, 12345" From d90359b4246877bfdbff0e985d4110e9ad4d57ab Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 8 Oct 2022 15:06:20 -0600 Subject: [PATCH 269/985] Remove redundant WattTime test fixture (#79909) --- tests/components/watttime/conftest.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/components/watttime/conftest.py b/tests/components/watttime/conftest.py index 6483778c153..c7a4f6cf32d 100644 --- a/tests/components/watttime/conftest.py +++ b/tests/components/watttime/conftest.py @@ -62,11 +62,13 @@ def config_location_type_fixture(hass): @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config_auth, config_coordinates, unique_id): +def config_entry_fixture(hass, config_auth, config_coordinates): """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id=unique_id, + unique_id=( + f"{config_coordinates[CONF_LATITUDE]}, {config_coordinates[CONF_LONGITUDE]}" + ), data={ **config_auth, **config_coordinates, @@ -112,9 +114,3 @@ async def setup_watttime_fixture(hass, client, config_auth, config_coordinates): ) await hass.async_block_till_done() yield - - -@pytest.fixture(name="unique_id") -def unique_id_fixture(hass): - """Define a config entry unique ID fixture.""" - return "32.87336, -117.22743" From 63379bcafff48d4e2d9ea34a93eb38a126b977dd Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 8 Oct 2022 15:06:32 -0600 Subject: [PATCH 270/985] Remove redundant Notion test fixture (#79910) --- tests/components/notion/conftest.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/components/notion/conftest.py b/tests/components/notion/conftest.py index 77efdacf943..603e9bc7823 100644 --- a/tests/components/notion/conftest.py +++ b/tests/components/notion/conftest.py @@ -22,9 +22,9 @@ def client_fixture(data_bridge, data_sensor, data_task): @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config, unique_id): +def config_entry_fixture(hass, config): """Define a config entry fixture.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=unique_id, data=config) + entry = MockConfigEntry(domain=DOMAIN, unique_id=config[CONF_USERNAME], data=config) entry.add_to_hass(hass) return entry @@ -65,9 +65,3 @@ async def setup_notion_fixture(hass, client, config): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() yield - - -@pytest.fixture(name="unique_id") -def unique_id_fixture(hass): - """Define a config entry unique ID fixture.""" - return "user@host.com" From 33d2bec6f1e195752b175bc164ae83438347925b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 8 Oct 2022 15:06:45 -0600 Subject: [PATCH 271/985] Remove redundant IQVIA test fixture (#79911) --- tests/components/iqvia/conftest.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/components/iqvia/conftest.py b/tests/components/iqvia/conftest.py index 5b6a76e7e57..65ad21cc813 100644 --- a/tests/components/iqvia/conftest.py +++ b/tests/components/iqvia/conftest.py @@ -11,9 +11,9 @@ from tests.common import MockConfigEntry, load_fixture @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config, unique_id): +def config_entry_fixture(hass, config): """Define a config entry fixture.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=unique_id, data=config) + entry = MockConfigEntry(domain=DOMAIN, unique_id=config[CONF_ZIP_CODE], data=config) entry.add_to_hass(hass) return entry @@ -101,9 +101,3 @@ async def setup_iqvia_fixture( assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() yield - - -@pytest.fixture(name="unique_id") -def unique_id_fixture(hass): - """Define a config entry unique ID fixture.""" - return "12345" From 6297a28507ce5b77d7ab36511c2aa345fc7ac5b9 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 8 Oct 2022 15:06:57 -0600 Subject: [PATCH 272/985] Remove redundant Tile test fixture (#79912) --- tests/components/tile/conftest.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/components/tile/conftest.py b/tests/components/tile/conftest.py index 0cb9a0080f6..aa90f1e44de 100644 --- a/tests/components/tile/conftest.py +++ b/tests/components/tile/conftest.py @@ -26,9 +26,9 @@ def api_fixture(hass, data_tile_details): @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config, unique_id): +def config_entry_fixture(hass, config): """Define a config entry fixture.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=unique_id, data=config) + entry = MockConfigEntry(domain=DOMAIN, unique_id=config[CONF_USERNAME], data=config) entry.add_to_hass(hass) return entry @@ -59,9 +59,3 @@ async def setup_tile_fixture(hass, api, config): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() yield - - -@pytest.fixture(name="unique_id") -def unique_id_fixture(hass): - """Define a config entry unique ID fixture.""" - return "user@host.com" From 8471a71b60de3fa5974a8871e359ace96ade82f3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 8 Oct 2022 16:32:51 -0600 Subject: [PATCH 273/985] Move AirNow test fixtures to `conftest.py` (#79902) * Move AirNow test fixtures to `conftest.py` * Unnecessary fixture * Better * Linting --- tests/components/airnow/conftest.py | 57 +++++++ tests/components/airnow/fixtures/__init__.py | 1 + .../components/airnow/fixtures/response.json | 47 ++++++ tests/components/airnow/test_config_flow.py | 146 +++--------------- 4 files changed, 124 insertions(+), 127 deletions(-) create mode 100644 tests/components/airnow/conftest.py create mode 100644 tests/components/airnow/fixtures/__init__.py create mode 100644 tests/components/airnow/fixtures/response.json diff --git a/tests/components/airnow/conftest.py b/tests/components/airnow/conftest.py new file mode 100644 index 00000000000..47f20ccd883 --- /dev/null +++ b/tests/components/airnow/conftest.py @@ -0,0 +1,57 @@ +"""Define fixtures for AirNow tests.""" +import json +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.airnow import DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture(name="config_entry") +def config_entry_fixture(hass, config): + """Define a config entry fixture.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=f"{config[CONF_LATITUDE]}-{config[CONF_LONGITUDE]}", + data=config, + ) + entry.add_to_hass(hass) + return entry + + +@pytest.fixture(name="config") +def config_fixture(hass): + """Define a config entry data fixture.""" + return { + CONF_API_KEY: "abc123", + CONF_LATITUDE: 34.053718, + CONF_LONGITUDE: -118.244842, + CONF_RADIUS: 75, + } + + +@pytest.fixture(name="data", scope="session") +def data_fixture(): + """Define a fixture for response data.""" + return json.loads(load_fixture("response.json", "airnow")) + + +@pytest.fixture(name="mock_api_get") +def mock_api_get_fixture(data): + """Define a fixture for a mock "get" coroutine function.""" + return AsyncMock(return_value=data) + + +@pytest.fixture(name="setup_airnow") +async def setup_airnow_fixture(hass, config, mock_api_get): + """Define a fixture to set up AirNow.""" + with patch("pyairnow.WebServiceAPI._get", mock_api_get), patch( + "homeassistant.components.airnow.config_flow.WebServiceAPI._get", mock_api_get + ), patch("homeassistant.components.airnow.PLATFORMS", []): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + yield diff --git a/tests/components/airnow/fixtures/__init__.py b/tests/components/airnow/fixtures/__init__.py new file mode 100644 index 00000000000..328b7a792e2 --- /dev/null +++ b/tests/components/airnow/fixtures/__init__.py @@ -0,0 +1 @@ +"""Define AirNow response fixture data.""" diff --git a/tests/components/airnow/fixtures/response.json b/tests/components/airnow/fixtures/response.json new file mode 100644 index 00000000000..91029f5531f --- /dev/null +++ b/tests/components/airnow/fixtures/response.json @@ -0,0 +1,47 @@ +[ + { + "DateObserved": "2020-12-20", + "HourObserved": 15, + "LocalTimeZone": "PST", + "ReportingArea": "Central LA CO", + "StateCode": "CA", + "Latitude": 34.0663, + "Longitude": -118.2266, + "ParameterName": "O3", + "AQI": 44, + "Category": { + "Number": 1, + "Name": "Good" + } + }, + { + "DateObserved": "2020-12-20", + "HourObserved": 15, + "LocalTimeZone": "PST", + "ReportingArea": "Central LA CO", + "StateCode": "CA", + "Latitude": 34.0663, + "Longitude": -118.2266, + "ParameterName": "PM2.5", + "AQI": 37, + "Category": { + "Number": 1, + "Name": "Good" + } + }, + { + "DateObserved": "2020-12-20", + "HourObserved": 15, + "LocalTimeZone": "PST", + "ReportingArea": "Central LA CO", + "StateCode": "CA", + "Latitude": 34.0663, + "Longitude": -118.2266, + "ParameterName": "PM10", + "AQI": 11, + "Category": { + "Number": 1, + "Name": "Good" + } + } +] diff --git a/tests/components/airnow/test_config_flow.py b/tests/components/airnow/test_config_flow.py index 02236e826e5..dddd51c8450 100644 --- a/tests/components/airnow/test_config_flow.py +++ b/tests/components/airnow/test_config_flow.py @@ -1,183 +1,75 @@ """Test the AirNow config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock from pyairnow.errors import AirNowError, InvalidKeyError +import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components.airnow.const import DOMAIN -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS - -from tests.common import MockConfigEntry - -CONFIG = { - CONF_API_KEY: "abc123", - CONF_LATITUDE: 34.053718, - CONF_LONGITUDE: -118.244842, - CONF_RADIUS: 75, -} - -# Mock AirNow Response -MOCK_RESPONSE = [ - { - "DateObserved": "2020-12-20", - "HourObserved": 15, - "LocalTimeZone": "PST", - "ReportingArea": "Central LA CO", - "StateCode": "CA", - "Latitude": 34.0663, - "Longitude": -118.2266, - "ParameterName": "O3", - "AQI": 44, - "Category": { - "Number": 1, - "Name": "Good", - }, - }, - { - "DateObserved": "2020-12-20", - "HourObserved": 15, - "LocalTimeZone": "PST", - "ReportingArea": "Central LA CO", - "StateCode": "CA", - "Latitude": 34.0663, - "Longitude": -118.2266, - "ParameterName": "PM2.5", - "AQI": 37, - "Category": { - "Number": 1, - "Name": "Good", - }, - }, - { - "DateObserved": "2020-12-20", - "HourObserved": 15, - "LocalTimeZone": "PST", - "ReportingArea": "Central LA CO", - "StateCode": "CA", - "Latitude": 34.0663, - "Longitude": -118.2266, - "ParameterName": "PM10", - "AQI": 11, - "Category": { - "Number": 1, - "Name": "Good", - }, - }, -] -async def test_form(hass): +async def test_form(hass, config, setup_airnow): """Test we get the form.""" - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} - with patch("pyairnow.WebServiceAPI._get", return_value=MOCK_RESPONSE), patch( - "homeassistant.components.airnow.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - CONFIG, - ) - - await hass.async_block_till_done() - + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result2["data"] == CONFIG - assert len(mock_setup_entry.mock_calls) == 1 + assert result2["data"] == config -async def test_form_invalid_auth(hass): +@pytest.mark.parametrize("mock_api_get", [AsyncMock(side_effect=InvalidKeyError)]) +async def test_form_invalid_auth(hass, config, setup_airnow): """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - - with patch( - "pyairnow.WebServiceAPI._get", - side_effect=InvalidKeyError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - CONFIG, - ) - + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) assert result2["type"] == "form" assert result2["errors"] == {"base": "invalid_auth"} -async def test_form_invalid_location(hass): +@pytest.mark.parametrize("data", [{}]) +async def test_form_invalid_location(hass, config, setup_airnow): """Test we handle invalid location.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - - with patch("pyairnow.WebServiceAPI._get", return_value={}): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - CONFIG, - ) - + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) assert result2["type"] == "form" assert result2["errors"] == {"base": "invalid_location"} -async def test_form_cannot_connect(hass): +@pytest.mark.parametrize("mock_api_get", [AsyncMock(side_effect=AirNowError)]) +async def test_form_cannot_connect(hass, config, setup_airnow): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - - with patch( - "pyairnow.WebServiceAPI._get", - side_effect=AirNowError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - CONFIG, - ) - + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} -async def test_form_unexpected(hass): +@pytest.mark.parametrize("mock_api_get", [AsyncMock(side_effect=RuntimeError)]) +async def test_form_unexpected(hass, config, setup_airnow): """Test we handle an unexpected error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - - with patch( - "homeassistant.components.airnow.config_flow.validate_input", - side_effect=RuntimeError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - CONFIG, - ) - + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) assert result2["type"] == "form" assert result2["errors"] == {"base": "unknown"} -async def test_entry_already_exists(hass): +async def test_entry_already_exists(hass, config, config_entry): """Test that the form aborts if the Lat/Lng is already configured.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - - mock_id = f"{CONFIG[CONF_LATITUDE]}-{CONFIG[CONF_LONGITUDE]}" - mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=mock_id) - mock_entry.add_to_hass(hass) - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - CONFIG, - ) - + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) assert result2["type"] == "abort" assert result2["reason"] == "already_configured" From 132ff2c41055fbff78afe2fe553cce932a052a9d Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sun, 9 Oct 2022 00:37:03 +0200 Subject: [PATCH 274/985] Update Config Flow to show message about unsupported Overkiz hardware (#79503) * Update Config Flow to show cozytouch unsupported hardware error * Apply feedback * Remove vague unknown user exception * Fix test * Code coverage back to 100% --- .../components/overkiz/config_flow.py | 20 ++++++++++-- homeassistant/components/overkiz/strings.json | 2 +- .../components/overkiz/translations/en.json | 2 +- tests/components/overkiz/test_config_flow.py | 32 ++++++++++++++++++- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/overkiz/config_flow.py b/homeassistant/components/overkiz/config_flow.py index d3ab9722fca..eac749f1bc0 100644 --- a/homeassistant/components/overkiz/config_flow.py +++ b/homeassistant/components/overkiz/config_flow.py @@ -9,6 +9,7 @@ from pyoverkiz.client import OverkizClient from pyoverkiz.const import SUPPORTED_SERVERS from pyoverkiz.exceptions import ( BadCredentialsException, + CozyTouchBadCredentialsException, MaintenanceException, TooManyAttemptsBannedException, TooManyRequestsException, @@ -67,6 +68,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle the initial step via config flow.""" errors = {} + description_placeholders = {} if user_input: self._default_user = user_input[CONF_USERNAME] @@ -76,8 +78,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_validate_input(user_input) except TooManyRequestsException: errors["base"] = "too_many_requests" - except BadCredentialsException: - errors["base"] = "invalid_auth" + except BadCredentialsException as exception: + # If authentication with CozyTouch auth server is valid, but token is invalid + # for Overkiz API server, the hardware is not supported. + if user_input[CONF_HUB] == "atlantic_cozytouch" and not isinstance( + exception, CozyTouchBadCredentialsException + ): + description_placeholders["unsupported_device"] = "CozyTouch" + errors["base"] = "unsupported_hardware" + else: + errors["base"] = "invalid_auth" except (TimeoutError, ClientError): errors["base"] = "cannot_connect" except MaintenanceException: @@ -85,7 +95,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except TooManyAttemptsBannedException: errors["base"] = "too_many_attempts" except UnknownUserException: - errors["base"] = "unknown_user" + # Somfy Protect accounts are not supported since they don't use + # the Overkiz API server. Login will return unknown user. + description_placeholders["unsupported_device"] = "Somfy Protect" + errors["base"] = "unsupported_hardware" except Exception as exception: # pylint: disable=broad-except errors["base"] = "unknown" LOGGER.exception(exception) @@ -129,6 +142,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ), } ), + description_placeholders=description_placeholders, errors=errors, ) diff --git a/homeassistant/components/overkiz/strings.json b/homeassistant/components/overkiz/strings.json index ecc0329eb2a..440ed154cfe 100644 --- a/homeassistant/components/overkiz/strings.json +++ b/homeassistant/components/overkiz/strings.json @@ -19,7 +19,7 @@ "too_many_attempts": "Too many attempts with an invalid token, temporarily banned", "too_many_requests": "Too many requests, try again later", "unknown": "[%key:common::config_flow::error::unknown%]", - "unknown_user": "Unknown user. Somfy Protect accounts are not supported by this integration." + "unsupported_hardware": "Your {unsupported_device} hardware is not supported by this integration." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", diff --git a/homeassistant/components/overkiz/translations/en.json b/homeassistant/components/overkiz/translations/en.json index 9c8ad538695..2c534a64cb6 100644 --- a/homeassistant/components/overkiz/translations/en.json +++ b/homeassistant/components/overkiz/translations/en.json @@ -12,7 +12,7 @@ "too_many_attempts": "Too many attempts with an invalid token, temporarily banned", "too_many_requests": "Too many requests, try again later", "unknown": "Unexpected error", - "unknown_user": "Unknown user. Somfy Protect accounts are not supported by this integration." + "unsupported_hardware": "Your {unsupported_device} hardware is not supported by this integration." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/tests/components/overkiz/test_config_flow.py b/tests/components/overkiz/test_config_flow.py index 940da7b39c2..dc50896626d 100644 --- a/tests/components/overkiz/test_config_flow.py +++ b/tests/components/overkiz/test_config_flow.py @@ -27,6 +27,7 @@ TEST_PASSWORD = "test-password" TEST_PASSWORD2 = "test-password2" TEST_HUB = "somfy_europe" TEST_HUB2 = "hi_kumo_europe" +TEST_HUB_COZYTOUCH = "atlantic_cozytouch" TEST_GATEWAY_ID = "1234-5678-9123" TEST_GATEWAY_ID2 = "4321-5678-9123" @@ -89,7 +90,7 @@ async def test_form(hass: HomeAssistant) -> None: (ClientError, "cannot_connect"), (MaintenanceException, "server_in_maintenance"), (TooManyAttemptsBannedException, "too_many_attempts"), - (UnknownUserException, "unknown_user"), + (UnknownUserException, "unsupported_hardware"), (Exception, "unknown"), ], ) @@ -112,6 +113,35 @@ async def test_form_invalid_auth( assert result2["errors"] == {"base": error} +@pytest.mark.parametrize( + "side_effect, error", + [ + (BadCredentialsException, "unsupported_hardware"), + ], +) +async def test_form_invalid_cozytouch_auth( + hass: HomeAssistant, side_effect: Exception, error: str +) -> None: + """Test we handle invalid auth from CozyTouch.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("pyoverkiz.client.OverkizClient.login", side_effect=side_effect): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": TEST_EMAIL, + "password": TEST_PASSWORD, + "hub": TEST_HUB_COZYTOUCH, + }, + ) + + assert result["step_id"] == config_entries.SOURCE_USER + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result2["errors"] == {"base": error} + + async def test_abort_on_duplicate_entry(hass: HomeAssistant) -> None: """Test config flow aborts Config Flow on duplicate entries.""" MockConfigEntry( From d9d614d97f58143392688b05479f05fdb97ace2e Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 9 Oct 2022 01:30:48 +0200 Subject: [PATCH 275/985] Bump pyatmo to 7.1.1 (#79918) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 74d34056241..1e3354f1c27 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -2,7 +2,7 @@ "domain": "netatmo", "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==7.1.0"], + "requirements": ["pyatmo==7.1.1"], "after_dependencies": ["cloud", "media_source"], "dependencies": ["application_credentials", "webhook"], "codeowners": ["@cgtobi"], diff --git a/requirements_all.txt b/requirements_all.txt index 62ae0760223..534683db0a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1439,7 +1439,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.1.0 +pyatmo==7.1.1 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a8a4cbbc1b5..e0ca76f41ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1027,7 +1027,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.1.0 +pyatmo==7.1.1 # homeassistant.components.apple_tv pyatv==0.10.3 From ed565a6eb12ecc100ebc78a4892379eac1f183b5 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 9 Oct 2022 00:30:44 +0000 Subject: [PATCH 276/985] [ci skip] Translation update --- .../accuweather/translations/sensor.pl.json | 6 +- .../components/braviatv/translations/it.json | 11 +- .../components/generic/translations/id.json | 7 + .../components/generic/translations/it.json | 7 + .../components/generic/translations/pl.json | 7 + .../components/generic/translations/ru.json | 7 + .../google_sheets/translations/it.json | 2 +- .../translations/select.pl.json | 6 +- .../huawei_lte/translations/fr.json | 11 +- .../huawei_lte/translations/hu.json | 11 +- .../huawei_lte/translations/id.json | 11 +- .../huawei_lte/translations/it.json | 11 +- .../huawei_lte/translations/ja.json | 11 +- .../huawei_lte/translations/pl.json | 11 +- .../huawei_lte/translations/ru.json | 11 +- .../media_player/translations/pl.json | 2 +- .../components/nest/translations/it.json | 2 +- .../components/overkiz/translations/el.json | 3 +- .../components/overkiz/translations/en.json | 1 + .../overkiz/translations/select.pl.json | 10 +- .../plugwise/translations/select.es.json | 2 + .../plugwise/translations/select.id.json | 11 ++ .../plugwise/translations/select.it.json | 11 ++ .../plugwise/translations/select.pl.json | 11 ++ .../plugwise/translations/select.ru.json | 9 ++ .../rtsp_to_webrtc/translations/it.json | 9 ++ .../tuya/translations/select.pl.json | 136 +++++++++--------- .../components/weather/translations/pl.json | 6 +- .../wled/translations/select.pl.json | 2 +- .../xiaomi_miio/translations/select.pl.json | 12 +- .../translations/select.pl.json | 40 +++--- .../components/zha/translations/id.json | 16 +++ .../components/zha/translations/it.json | 16 +++ .../components/zha/translations/pl.json | 16 +++ 34 files changed, 324 insertions(+), 121 deletions(-) create mode 100644 homeassistant/components/plugwise/translations/select.id.json create mode 100644 homeassistant/components/plugwise/translations/select.it.json create mode 100644 homeassistant/components/plugwise/translations/select.pl.json create mode 100644 homeassistant/components/plugwise/translations/select.ru.json diff --git a/homeassistant/components/accuweather/translations/sensor.pl.json b/homeassistant/components/accuweather/translations/sensor.pl.json index 68d7f8ac8ee..cc7ba9b873c 100644 --- a/homeassistant/components/accuweather/translations/sensor.pl.json +++ b/homeassistant/components/accuweather/translations/sensor.pl.json @@ -1,9 +1,9 @@ { "state": { "accuweather__pressure_tendency": { - "falling": "Spada", - "rising": "Ro\u015bnie", - "steady": "Bez zmian" + "falling": "spada", + "rising": "ro\u015bnie", + "steady": "bez zmian" } } } \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/it.json b/homeassistant/components/braviatv/translations/it.json index 8ac0b2d7df9..7bf9bb98b5a 100644 --- a/homeassistant/components/braviatv/translations/it.json +++ b/homeassistant/components/braviatv/translations/it.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "no_ip_control": "Il controllo IP \u00e8 disabilitato sulla TV o la TV non \u00e8 supportata.", - "not_bravia_device": "Il dispositivo non \u00e8 una TV Bravia." + "not_bravia_device": "Il dispositivo non \u00e8 una TV Bravia.", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "reauth_unsuccessful": "La nuova autenticazione non ha avuto esito positivo, rimuovere l'integrazione e configurarla di nuovo." }, "error": { "cannot_connect": "Impossibile connettersi", @@ -23,6 +25,13 @@ "confirm": { "description": "Vuoi iniziare la configurazione?" }, + "reauth_confirm": { + "data": { + "pin": "Codice PIN", + "use_psk": "Usa l'autenticazione PSK" + }, + "description": "Inserisci il codice PIN mostrato sul Sony Bravia TV. \n\nSe il codice PIN non viene visualizzato, devi annullare la registrazione di Home Assistant sulla TV, vai su: Impostazioni -> Rete -> Impostazioni dispositivo remoto -> Annulla registrazione dispositivo remoto. \n\nPuoi usare PSK (Pre-Shared-Key) invece del PIN. PSK \u00e8 una chiave segreta definita dall'utente utilizzata per il controllo degli accessi. Questo metodo di autenticazione \u00e8 consigliato poich\u00e9 pi\u00f9 stabile. Per abilitare PSK sulla tua TV, vai su: Impostazioni -> Rete -> Configurazione rete domestica -> Controllo IP. Quindi seleziona la casella \u00abUtilizza l'autenticazione PSK\u00bb e inserisci la tua chiave PSK anzich\u00e9 il PIN." + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/generic/translations/id.json b/homeassistant/components/generic/translations/id.json index 5222c111c58..8cc0ca6aefc 100644 --- a/homeassistant/components/generic/translations/id.json +++ b/homeassistant/components/generic/translations/id.json @@ -45,6 +45,13 @@ "verify_ssl": "Verifikasi sertifikat SSL" }, "description": "Masukkan pengaturan untuk terhubung ke kamera." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "Gambar ini terlihat bagus." + }, + "description": "![Pratinjau Gambar Diam Kamera]({preview_url})", + "title": "Pratinjau" } } }, diff --git a/homeassistant/components/generic/translations/it.json b/homeassistant/components/generic/translations/it.json index 14d4b6e8720..a06dca30df8 100644 --- a/homeassistant/components/generic/translations/it.json +++ b/homeassistant/components/generic/translations/it.json @@ -45,6 +45,13 @@ "verify_ssl": "Verifica il certificato SSL" }, "description": "Inserisci le impostazioni per connetterti alla fotocamera." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "Questa immagine sembra buona." + }, + "description": "![Anteprima immagine fissa fotocamera]({preview_url})", + "title": "Anteprima" } } }, diff --git a/homeassistant/components/generic/translations/pl.json b/homeassistant/components/generic/translations/pl.json index 71c50148957..50de1532da6 100644 --- a/homeassistant/components/generic/translations/pl.json +++ b/homeassistant/components/generic/translations/pl.json @@ -45,6 +45,13 @@ "verify_ssl": "Weryfikacja certyfikatu SSL" }, "description": "Wprowad\u017a ustawienia, aby po\u0142\u0105czy\u0107 si\u0119 z kamer\u0105." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "Ten obraz wygl\u0105da dobrze." + }, + "description": "![Podgl\u0105d nieruchomego obrazu z kamery]({preview_url})", + "title": "Podgl\u0105d" } } }, diff --git a/homeassistant/components/generic/translations/ru.json b/homeassistant/components/generic/translations/ru.json index 022af07b58b..ad7126d85b6 100644 --- a/homeassistant/components/generic/translations/ru.json +++ b/homeassistant/components/generic/translations/ru.json @@ -45,6 +45,13 @@ "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043a\u0430\u043c\u0435\u0440\u0435." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "\u042d\u0442\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0445\u043e\u0440\u043e\u0448\u043e." + }, + "description": "![\u041f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0441 \u043a\u0430\u043c\u0435\u0440\u044b]({preview_url})", + "title": "\u041f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440" } } }, diff --git a/homeassistant/components/google_sheets/translations/it.json b/homeassistant/components/google_sheets/translations/it.json index 296899ad741..4a63c95e4a1 100644 --- a/homeassistant/components/google_sheets/translations/it.json +++ b/homeassistant/components/google_sheets/translations/it.json @@ -27,7 +27,7 @@ "title": "Scegli il metodo di autenticazione" }, "reauth_confirm": { - "description": "L'integrazione di Fogli Google deve riautenticare il tuo account", + "description": "L'integrazione di Fogli Google deve autenticare nuovamente il tuo account", "title": "Autentica nuovamente l'integrazione" } } diff --git a/homeassistant/components/homekit_controller/translations/select.pl.json b/homeassistant/components/homekit_controller/translations/select.pl.json index 0a59529b6ba..7a9139d109f 100644 --- a/homeassistant/components/homekit_controller/translations/select.pl.json +++ b/homeassistant/components/homekit_controller/translations/select.pl.json @@ -1,9 +1,9 @@ { "state": { "homekit_controller__ecobee_mode": { - "away": "Poza domem", - "home": "W domu", - "sleep": "U\u015bpiony" + "away": "poza domem", + "home": "w domu", + "sleep": "u\u015bpiony" } } } \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/fr.json b/homeassistant/components/huawei_lte/translations/fr.json index 117514985c9..e6cddfe0063 100644 --- a/homeassistant/components/huawei_lte/translations/fr.json +++ b/homeassistant/components/huawei_lte/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Pas un appareil Huawei LTE" + "not_huawei_lte": "Pas un appareil Huawei LTE", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "connection_timeout": "D\u00e9lai de connexion d\u00e9pass\u00e9", @@ -15,6 +16,14 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Saisissez les informations d'identification permettant d'acc\u00e9der \u00e0 l'appareil.", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, "user": { "data": { "password": "Mot de passe", diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json index e1a26405396..34653dcfb72 100644 --- a/homeassistant/components/huawei_lte/translations/hu.json +++ b/homeassistant/components/huawei_lte/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Nem Huawei LTE eszk\u00f6z" + "not_huawei_lte": "Nem Huawei LTE eszk\u00f6z", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "connection_timeout": "Kapcsolat id\u0151t\u00fall\u00e9p\u00e9s", @@ -15,6 +16,14 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "Adja meg az eszk\u00f6z hozz\u00e1f\u00e9r\u00e9si hiteles\u00edt\u0151 adatait.", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/huawei_lte/translations/id.json b/homeassistant/components/huawei_lte/translations/id.json index ac925d0b460..5bb08d626d0 100644 --- a/homeassistant/components/huawei_lte/translations/id.json +++ b/homeassistant/components/huawei_lte/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Bukan perangkat Huawei LTE" + "not_huawei_lte": "Bukan perangkat Huawei LTE", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "connection_timeout": "Tenggang waktu terhubung habis", @@ -15,6 +16,14 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Masukkan kredensial akses perangkat.", + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "password": "Kata Sandi", diff --git a/homeassistant/components/huawei_lte/translations/it.json b/homeassistant/components/huawei_lte/translations/it.json index 6a90b67d307..9db6aea063a 100644 --- a/homeassistant/components/huawei_lte/translations/it.json +++ b/homeassistant/components/huawei_lte/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Non \u00e8 un dispositivo Huawei LTE" + "not_huawei_lte": "Non \u00e8 un dispositivo Huawei LTE", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "connection_timeout": "Timeout di connessione", @@ -15,6 +16,14 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Immettere le credenziali di accesso al dispositivo.", + "title": "Autentica nuovamente l'integrazione" + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/huawei_lte/translations/ja.json b/homeassistant/components/huawei_lte/translations/ja.json index c3b41fbbcfc..25cf9d1b0e8 100644 --- a/homeassistant/components/huawei_lte/translations/ja.json +++ b/homeassistant/components/huawei_lte/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Huawei LTE\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093" + "not_huawei_lte": "Huawei LTE\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "connection_timeout": "\u63a5\u7d9a\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", @@ -15,6 +16,14 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + }, + "description": "\u30c7\u30d0\u30a4\u30b9\u306e\u30a2\u30af\u30bb\u30b9\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" + }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", diff --git a/homeassistant/components/huawei_lte/translations/pl.json b/homeassistant/components/huawei_lte/translations/pl.json index 76e7c92a010..1f66183a762 100644 --- a/homeassistant/components/huawei_lte/translations/pl.json +++ b/homeassistant/components/huawei_lte/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "To nie jest urz\u0105dzenie Huawei LTE" + "not_huawei_lte": "To nie jest urz\u0105dzenie Huawei LTE", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "connection_timeout": "Przekroczono limit czasu pr\u00f3by po\u0142\u0105czenia", @@ -15,6 +16,14 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce dost\u0119pu do urz\u0105dzenia.", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "password": "Has\u0142o", diff --git a/homeassistant/components/huawei_lte/translations/ru.json b/homeassistant/components/huawei_lte/translations/ru.json index 6c27673b556..feb6209cc81 100644 --- a/homeassistant/components/huawei_lte/translations/ru.json +++ b/homeassistant/components/huawei_lte/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Huawei LTE" + "not_huawei_lte": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Huawei LTE", + "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": { "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", @@ -15,6 +16,14 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/media_player/translations/pl.json b/homeassistant/components/media_player/translations/pl.json index 886cb07f93f..8eb1accf33a 100644 --- a/homeassistant/components/media_player/translations/pl.json +++ b/homeassistant/components/media_player/translations/pl.json @@ -20,7 +20,7 @@ }, "state": { "_": { - "buffering": "Buforowanie", + "buffering": "buforowanie", "idle": "nieaktywny", "off": "wy\u0142.", "on": "w\u0142.", diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 73095bf0930..6cdfa35b194 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -52,7 +52,7 @@ "data": { "project_id": "ID progetto di accesso al dispositivo" }, - "description": "Crea un progetto di accesso al dispositivo Nest la cui configurazione **richiede una commissione di 5 $ USD**.\n 1. Vai alla [Console di accesso al dispositivo]({device_access_console_url}) e attraverso il flusso di pagamento.\n 2. Clicca su **Crea progetto**\n 3. Assegna un nome al progetto di accesso al dispositivo e fai clic su **Avanti**.\n 4. Inserisci il tuo ID Client OAuth\n 5. Abilita gli eventi facendo clic su **Abilita** e **Crea progetto**. \n\nInserisci il tuo ID progetto di accesso al dispositivo di seguito ([maggiori informazioni]({more_info_url})).\n", + "description": "Crea un progetto di accesso al dispositivo Nest la cui configurazione **richiede una commissione di 5 $ USD**.\n 1. Vai alla [Console di accesso al dispositivo]({device_access_console_url}) e attraverso il flusso di pagamento.\n 2. Clicca su **Crea progetto**\n 3. Assegna un nome al progetto di accesso al dispositivo e fai clic su **Avanti**.\n 4. Inserisci il tuo ID Client OAuth\n 5. Abilita gli eventi facendo clic su **Abilita** e **Crea progetto**. \n\nInserisci il tuo ID progetto di accesso al dispositivo di seguito ([maggiori informazioni]({more_info_url})).\n", "title": "Nest: crea un progetto di accesso al dispositivo" }, "device_project_upgrade": { diff --git a/homeassistant/components/overkiz/translations/el.json b/homeassistant/components/overkiz/translations/el.json index e9862479c27..eb308c470d3 100644 --- a/homeassistant/components/overkiz/translations/el.json +++ b/homeassistant/components/overkiz/translations/el.json @@ -12,7 +12,8 @@ "too_many_attempts": "\u03a0\u03ac\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03ad\u03c2 \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b5\u03c2 \u03bc\u03b5 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc, \u03c0\u03c1\u03bf\u03c3\u03c9\u03c1\u03b9\u03bd\u03ac \u03b1\u03c0\u03bf\u03ba\u03bb\u03b5\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2", "too_many_requests": "\u03a0\u03ac\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03ac \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1, \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", - "unknown_user": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2. \u039f\u03b9 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03af Somfy Protect \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7." + "unknown_user": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2. \u039f\u03b9 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03af Somfy Protect \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7.", + "unsupported_hardware": "\u03a4\u03bf \u03c5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b1\u03c2 {unsupported_device} \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7." }, "flow_title": "\u03a0\u03cd\u03bb\u03b7: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/en.json b/homeassistant/components/overkiz/translations/en.json index 2c534a64cb6..d7dcd2a79ac 100644 --- a/homeassistant/components/overkiz/translations/en.json +++ b/homeassistant/components/overkiz/translations/en.json @@ -12,6 +12,7 @@ "too_many_attempts": "Too many attempts with an invalid token, temporarily banned", "too_many_requests": "Too many requests, try again later", "unknown": "Unexpected error", + "unknown_user": "Unknown user. Somfy Protect accounts are not supported by this integration.", "unsupported_hardware": "Your {unsupported_device} hardware is not supported by this integration." }, "flow_title": "Gateway: {gateway_id}", diff --git a/homeassistant/components/overkiz/translations/select.pl.json b/homeassistant/components/overkiz/translations/select.pl.json index 0989aec66fe..b925c0f5de6 100644 --- a/homeassistant/components/overkiz/translations/select.pl.json +++ b/homeassistant/components/overkiz/translations/select.pl.json @@ -1,13 +1,13 @@ { "state": { "overkiz__memorized_simple_volume": { - "highest": "Najwy\u017csze", - "standard": "Normalnie" + "highest": "najwy\u017csze", + "standard": "normalnie" }, "overkiz__open_closed_pedestrian": { - "closed": "Zamkni\u0119ta", - "open": "Otwarte", - "pedestrian": "Pieszy" + "closed": "zamkni\u0119ta", + "open": "otwarte", + "pedestrian": "pieszy" } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/select.es.json b/homeassistant/components/plugwise/translations/select.es.json index c08ee07b64f..38fcab309d2 100644 --- a/homeassistant/components/plugwise/translations/select.es.json +++ b/homeassistant/components/plugwise/translations/select.es.json @@ -1,6 +1,8 @@ { "state": { "plugwise__regulation_mode": { + "bleeding_cold": "Purgando fr\u00edo", + "bleeding_hot": "Purgando caliente", "cooling": "Refrigeraci\u00f3n", "heating": "Calefacci\u00f3n", "off": "Apagado" diff --git a/homeassistant/components/plugwise/translations/select.id.json b/homeassistant/components/plugwise/translations/select.id.json new file mode 100644 index 00000000000..0be50360a0f --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.id.json @@ -0,0 +1,11 @@ +{ + "state": { + "plugwise__regulation_mode": { + "bleeding_cold": "Dingin sekali", + "bleeding_hot": "Panas sekali", + "cooling": "Mendinginkan", + "heating": "Memanaskan", + "off": "Mati" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/select.it.json b/homeassistant/components/plugwise/translations/select.it.json new file mode 100644 index 00000000000..64f15b3915b --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.it.json @@ -0,0 +1,11 @@ +{ + "state": { + "plugwise__regulation_mode": { + "bleeding_cold": "Sfiatamento freddo", + "bleeding_hot": "Sfiatamento caldo", + "cooling": "Raffreddamento", + "heating": "Riscaldamento", + "off": "Spento" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/select.pl.json b/homeassistant/components/plugwise/translations/select.pl.json new file mode 100644 index 00000000000..120801ff712 --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.pl.json @@ -0,0 +1,11 @@ +{ + "state": { + "plugwise__regulation_mode": { + "bleeding_cold": "strasznie zimno", + "bleeding_hot": "strasznie ciep\u0142o", + "cooling": "ch\u0142odzenie", + "heating": "grzanie", + "off": "wy\u0142." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/select.ru.json b/homeassistant/components/plugwise/translations/select.ru.json new file mode 100644 index 00000000000..a72746cc983 --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.ru.json @@ -0,0 +1,9 @@ +{ + "state": { + "plugwise__regulation_mode": { + "cooling": "\u041e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u0435", + "heating": "\u041e\u0431\u043e\u0433\u0440\u0435\u0432", + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rtsp_to_webrtc/translations/it.json b/homeassistant/components/rtsp_to_webrtc/translations/it.json index c91e0bc34e8..319bdaafbd4 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/it.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/it.json @@ -23,5 +23,14 @@ "title": "Configura RTSPtoWebRTC" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Indirizzo del server Stun (host:porta)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.pl.json b/homeassistant/components/tuya/translations/select.pl.json index d75bbd8a8be..109b62a6e34 100644 --- a/homeassistant/components/tuya/translations/select.pl.json +++ b/homeassistant/components/tuya/translations/select.pl.json @@ -1,12 +1,12 @@ { "state": { "tuya__basic_anti_flickr": { - "0": "Wy\u0142\u0105czone", + "0": "wy\u0142\u0105czone", "1": "50 Hz", "2": "60 Hz" }, "tuya__basic_nightvision": { - "0": "Automatycznie", + "0": "automatycznie", "1": "wy\u0142.", "2": "w\u0142." }, @@ -17,19 +17,19 @@ "4h": "4 godziny", "5h": "5 godzin", "6h": "6 godzin", - "cancel": "Anuluj" + "cancel": "anuluj" }, "tuya__curtain_mode": { - "morning": "Ranek", - "night": "Noc" + "morning": "ranek", + "night": "noc" }, "tuya__curtain_motor_mode": { - "back": "Do ty\u0142u", - "forward": "Do przodu" + "back": "do ty\u0142u", + "forward": "do przodu" }, "tuya__decibel_sensitivity": { - "0": "Niska czu\u0142o\u015b\u0107", - "1": "Wysoka czu\u0142o\u015b\u0107" + "0": "niska czu\u0142o\u015b\u0107", + "1": "wysoka czu\u0142o\u015b\u0107" }, "tuya__fan_angle": { "30": "30\u00b0", @@ -37,61 +37,61 @@ "90": "90\u00b0" }, "tuya__fingerbot_mode": { - "click": "Naci\u015bni\u0119cie", - "switch": "Prze\u0142\u0105cznik" + "click": "naci\u015bni\u0119cie", + "switch": "prze\u0142\u0105cznik" }, "tuya__humidifier_level": { - "level_1": "Poziom 1", - "level_10": "Poziom 10", - "level_2": "Poziom 2", - "level_3": "Poziom 3", - "level_4": "Poziom 4", - "level_5": "Poziom 5", - "level_6": "Poziom 6", - "level_7": "Poziom 7", - "level_8": "Poziom 8", - "level_9": "Poziom 9" + "level_1": "poziom 1", + "level_10": "poziom 10", + "level_2": "poziom 2", + "level_3": "poziom 3", + "level_4": "poziom 4", + "level_5": "poziom 5", + "level_6": "poziom 6", + "level_7": "poziom 7", + "level_8": "poziom 8", + "level_9": "poziom 9" }, "tuya__humidifier_moodlighting": { - "1": "Nastr\u00f3j 1", - "2": "Nastr\u00f3j 2", - "3": "Nastr\u00f3j 3", - "4": "Nastr\u00f3j 4", - "5": "Nastr\u00f3j 5" + "1": "nastr\u00f3j 1", + "2": "nastr\u00f3j 2", + "3": "nastr\u00f3j 3", + "4": "nastr\u00f3j 4", + "5": "nastr\u00f3j 5" }, "tuya__humidifier_spray_mode": { - "auto": "Auto", - "health": "Zdrowotny", - "humidity": "Wilgotno\u015b\u0107", - "sleep": "U\u015bpiony", + "auto": "automatyczny", + "health": "zdrowotny", + "humidity": "wilgotno\u015b\u0107", + "sleep": "u\u015bpiony", "work": "Praca" }, "tuya__ipc_work_mode": { - "0": "Tryb niskiego poboru mocy", - "1": "Tryb pracy ci\u0105g\u0142ej" + "0": "tryb niskiego poboru mocy", + "1": "tryb pracy ci\u0105g\u0142ej" }, "tuya__led_type": { - "halogen": "Halogen", - "incandescent": "Jarzeni\u00f3wka", + "halogen": "halogen", + "incandescent": "jarzeni\u00f3wka", "led": "LED" }, "tuya__light_mode": { "none": "wy\u0142.", - "pos": "Wska\u017c lokalizacj\u0119 prze\u0142\u0105cznika", - "relay": "Wska\u017c stan w\u0142./wy\u0142." + "pos": "wska\u017c lokalizacj\u0119 prze\u0142\u0105cznika", + "relay": "wska\u017c stan w\u0142./wy\u0142." }, "tuya__motion_sensitivity": { - "0": "Niska czu\u0142o\u015b\u0107", - "1": "\u015arednia czu\u0142o\u015b\u0107", - "2": "Wysoka czu\u0142o\u015b\u0107" + "0": "niska czu\u0142o\u015b\u0107", + "1": "\u015brednia czu\u0142o\u015b\u0107", + "2": "wysoka czu\u0142o\u015b\u0107" }, "tuya__record_mode": { - "1": "Nagrywaj tylko zdarzenia", - "2": "Nagrywanie ci\u0105g\u0142e" + "1": "nagrywaj tylko zdarzenia", + "2": "nagrywanie ci\u0105g\u0142e" }, "tuya__relay_status": { - "last": "Zapami\u0119taj ostatni stan", - "memory": "Zapami\u0119taj ostatni stan", + "last": "zapami\u0119taj ostatni stan", + "memory": "zapami\u0119taj ostatni stan", "off": "wy\u0142.", "on": "w\u0142.", "power_off": "wy\u0142.", @@ -99,35 +99,35 @@ }, "tuya__vacuum_cistern": { "closed": "zamkni\u0119ta", - "high": "Du\u017ce", - "low": "Ma\u0142e", - "middle": "\u015arednie" + "high": "du\u017ce", + "low": "ma\u0142e", + "middle": "\u015brednie" }, "tuya__vacuum_collection": { - "large": "Du\u017ce", - "middle": "\u015arednie", - "small": "Ma\u0142e" + "large": "du\u017ce", + "middle": "\u015brednie", + "small": "ma\u0142e" }, "tuya__vacuum_mode": { - "bow": "\u0141uk", - "chargego": "Powr\u00f3t do stacji dokuj\u0105cej", - "left_bow": "\u0141uk w lewo", - "left_spiral": "Spirala w lewo", - "mop": "Mop", - "part": "Cz\u0119\u015bciowe", - "partial_bow": "Cz\u0119\u015bciowy \u0142uk", - "pick_zone": "Wybierz stref\u0119", - "point": "Punkt", - "pose": "Pozycja", - "random": "Losowo", - "right_bow": "\u0141uk w prawo", - "right_spiral": "Spirala w prawo", - "single": "Pojedyncze", - "smart": "Smart", - "spiral": "Spirala", - "standby": "Tryb czuwania", - "wall_follow": "Wzd\u0142u\u017c \u015bciany", - "zone": "Strefa" + "bow": "\u0142uk", + "chargego": "powr\u00f3t do stacji dokuj\u0105cej", + "left_bow": "\u0142uk w lewo", + "left_spiral": "spirala w lewo", + "mop": "mop", + "part": "cz\u0119\u015bciowe", + "partial_bow": "cz\u0119\u015bciowy \u0142uk", + "pick_zone": "wybierz stref\u0119", + "point": "punkt", + "pose": "pozycja", + "random": "losowo", + "right_bow": "\u0142uk w prawo", + "right_spiral": "spirala w prawo", + "single": "pojedyncze", + "smart": "smart", + "spiral": "spirala", + "standby": "tryb czuwania", + "wall_follow": "wzd\u0142u\u017c \u015bciany", + "zone": "strefa" } } } \ No newline at end of file diff --git a/homeassistant/components/weather/translations/pl.json b/homeassistant/components/weather/translations/pl.json index c284c361623..43cc15533eb 100644 --- a/homeassistant/components/weather/translations/pl.json +++ b/homeassistant/components/weather/translations/pl.json @@ -1,15 +1,15 @@ { "state": { "_": { - "clear-night": "Pogodna noc", - "cloudy": "Pochmurno", + "clear-night": "pogodna noc", + "cloudy": "pochmurno", "exceptional": "warunki nadzwyczajne", "fog": "mg\u0142a", "hail": "grad", "lightning": "b\u0142yskawice", "lightning-rainy": "burza", "partlycloudy": "cz\u0119\u015bciowe zachmurzenie", - "pouring": "Ulewa", + "pouring": "ulewa", "rainy": "deszczowo", "snowy": "opady \u015bniegu", "snowy-rainy": "\u015bnieg z deszczem", diff --git a/homeassistant/components/wled/translations/select.pl.json b/homeassistant/components/wled/translations/select.pl.json index 381f2306d26..20017c51c42 100644 --- a/homeassistant/components/wled/translations/select.pl.json +++ b/homeassistant/components/wled/translations/select.pl.json @@ -3,7 +3,7 @@ "wled__live_override": { "0": "wy\u0142.", "1": "w\u0142.", - "2": "Do czasu ponownego uruchomienia urz\u0105dzenia" + "2": "do czasu ponownego uruchomienia urz\u0105dzenia" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.pl.json b/homeassistant/components/xiaomi_miio/translations/select.pl.json index 92a1539b9ce..09197cc33f1 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.pl.json +++ b/homeassistant/components/xiaomi_miio/translations/select.pl.json @@ -1,9 +1,9 @@ { "state": { "xiaomi_miio__display_orientation": { - "forward": "Do przodu", - "left": "W lewo", - "right": "W prawo" + "forward": "do przodu", + "left": "w lewo", + "right": "w prawo" }, "xiaomi_miio__led_brightness": { "bright": "jasne", @@ -11,9 +11,9 @@ "off": "wy\u0142\u0105czone" }, "xiaomi_miio__ptc_level": { - "high": "Wysoki", - "low": "Niski", - "medium": "\u015aredni" + "high": "wysoki", + "low": "niski", + "medium": "\u015bredni" } } } \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.pl.json b/homeassistant/components/yamaha_musiccast/translations/select.pl.json index a6e9bde7c4f..ee42d8a74fb 100644 --- a/homeassistant/components/yamaha_musiccast/translations/select.pl.json +++ b/homeassistant/components/yamaha_musiccast/translations/select.pl.json @@ -1,38 +1,38 @@ { "state": { "yamaha_musiccast__dimmer": { - "auto": "Automatyczny" + "auto": "automatyczny" }, "yamaha_musiccast__zone_equalizer_mode": { - "auto": "Automatycznie", - "bypass": "Pomijanie", - "manual": "R\u0119cznie" + "auto": "automatycznie", + "bypass": "pomijanie", + "manual": "r\u0119cznie" }, "yamaha_musiccast__zone_link_audio_delay": { - "audio_sync": "Synchronizacja d\u017awi\u0119ku", - "audio_sync_off": "Synchronizacja d\u017awi\u0119ku wy\u0142\u0105czona", - "audio_sync_on": "Synchronizacja d\u017awi\u0119ku w\u0142\u0105czona", - "balanced": "Zr\u00f3wnowa\u017cone", - "lip_sync": "Synchronizacja ust" + "audio_sync": "synchronizacja d\u017awi\u0119ku", + "audio_sync_off": "synchronizacja d\u017awi\u0119ku wy\u0142\u0105czona", + "audio_sync_on": "synchronizacja d\u017awi\u0119ku w\u0142\u0105czona", + "balanced": "zr\u00f3wnowa\u017cone", + "lip_sync": "synchronizacja ust" }, "yamaha_musiccast__zone_link_audio_quality": { - "compressed": "Skompresowane", - "uncompressed": "Nieskompresowane" + "compressed": "skompresowane", + "uncompressed": "nieskompresowane" }, "yamaha_musiccast__zone_link_control": { - "speed": "Pr\u0119dko\u015b\u0107", - "stability": "Stabilno\u015b\u0107", - "standard": "Normalnie" + "speed": "pr\u0119dko\u015b\u0107", + "stability": "stabilno\u015b\u0107", + "standard": "normalnie" }, "yamaha_musiccast__zone_sleep": { "120 min": "120 minut", "30 min": "30 minut", "60 min": "60 minut", "90 min": "90 minut", - "off": "Wy\u0142\u0105czone" + "off": "wy\u0142\u0105czone" }, "yamaha_musiccast__zone_surr_decoder_type": { - "auto": "Automatycznie", + "auto": "automatycznie", "dolby_pl": "Dolby ProLogic", "dolby_pl2x_game": "Dolby ProLogic 2x (Gra)", "dolby_pl2x_movie": "Dolby ProLogic 2x (Film)", @@ -41,12 +41,12 @@ "dts_neo6_cinema": "DTS Neo:6 (Kino)", "dts_neo6_music": "DTS Neo:6 (Muzyka)", "dts_neural_x": "DTS Neural:X", - "toggle": "Prze\u0142\u0105cz" + "toggle": "prze\u0142\u0105cz" }, "yamaha_musiccast__zone_tone_control_mode": { - "auto": "Automatyczna", - "bypass": "Pomijanie", - "manual": "R\u0119czna" + "auto": "automatyczna", + "bypass": "pomijanie", + "manual": "r\u0119czna" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/id.json b/homeassistant/components/zha/translations/id.json index 65f7588cb60..ab496b3be53 100644 --- a/homeassistant/components/zha/translations/id.json +++ b/homeassistant/components/zha/translations/id.json @@ -210,6 +210,14 @@ "description": "ZHA akan dihentikan. Ingin melanjutkan?", "title": "Konfigurasi Ulang ZHA" }, + "instruct_unplug": { + "description": "Radio lama Anda telah disetel ulang. Jika perangkat keras tidak lagi diperlukan, Anda dapat mencabutnya sekarang.", + "title": "Cabut radio lama Anda" + }, + "intent_migrate": { + "description": "Radio lama Anda akan disetel ulang ke setelan pabrikan. Jika Anda menggunakan adaptor gabungan Z-Wave dan Zigbee seperti HUSBZB-1, ini hanya akan mengatur ulang bagian Zigbee.\n\nApakah Anda ingin melanjutkan?", + "title": "Migrasikan ke radio baru" + }, "manual_pick_radio_type": { "data": { "radio_type": "Jenis Radio" @@ -233,6 +241,14 @@ "description": "Cadangan Anda memiliki alamat IEEE yang berbeda dari radio Anda. Agar jaringan berfungsi dengan baik, alamat IEEE radio Anda juga harus diubah.\n\nOperasi ini bersifat permanen.", "title": "Timpa Alamat IEEE Radio" }, + "prompt_migrate_or_reconfigure": { + "description": "Apakah Anda memigrasikan ke radio baru atau mengkonfigurasi ulang radio yang sekarang?", + "menu_options": { + "intent_migrate": "Migrasikan ke radio baru", + "intent_reconfigure": "Mengkonfigurasi ulang radio yang sekarang" + }, + "title": "Migrasi atau konfigurasi ulang" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Unggah file" diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index edda54ca6bf..02b3549d263 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -212,6 +212,14 @@ "description": "ZHA verr\u00e0 interrotto. Vuoi continuare?", "title": "Riconfigura ZHA" }, + "instruct_unplug": { + "description": "La tua vecchia radio \u00e8 stata ripristinata. Se l'hardware non \u00e8 pi\u00f9 necessario, ora \u00e8 possibile scollegarlo.", + "title": "Scollega la tua vecchia radio" + }, + "intent_migrate": { + "description": "La tua vecchia radio verr\u00e0 ripristinata alle impostazioni di fabbrica. Se stai usando un adattatore combinato Z-Wave e Zigbee come HUSBZB-1, questo ripristiner\u00e0 solo la parte Zigbee. \n\nVuoi continuare?", + "title": "Migra a una nuova radio" + }, "manual_pick_radio_type": { "data": { "radio_type": "Tipo di radio" @@ -235,6 +243,14 @@ "description": "Il tuo backup ha un indirizzo IEEE diverso dalla tua radio. Affinch\u00e9 la rete funzioni correttamente, \u00e8 necessario modificare anche l'indirizzo IEEE della radio. \n\nQuesta \u00e8 un'operazione permanente.", "title": "Sovrascrivi indirizzo IEEE radio" }, + "prompt_migrate_or_reconfigure": { + "description": "Stai migrando a una nuova radio o riconfigurando la radio attuale?", + "menu_options": { + "intent_migrate": "Migra a una nuova radio", + "intent_reconfigure": "Riconfigura la radio attuale" + }, + "title": "Migrare o riconfigurare" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Carica un file" diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index 5928e60a139..b2046848f0a 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -212,6 +212,14 @@ "description": "ZHA zostanie zatrzymany. Czy chcesz kontynuowa\u0107?", "title": "Zmiana konfiguracji ZHA" }, + "instruct_unplug": { + "description": "Tw\u00f3j stary typ radia zosta\u0142 zresetowany. Je\u015bli sprz\u0119t nie jest ju\u017c potrzebny, mo\u017cesz go teraz od\u0142\u0105czy\u0107.", + "title": "Od\u0142\u0105cz stary typ radia" + }, + "intent_migrate": { + "description": "Twoje stare radio zostanie zresetowane do ustawie\u0144 fabrycznych. Je\u015bli u\u017cywasz po\u0142\u0105czonego adaptera Z-Wave i Zigbee, takiego jak HUSBZB-1, zresetuje to tylko cz\u0119\u015b\u0107 Zigbee. \n\nCzy chcesz kontynuowa\u0107?", + "title": "Migracja do nowego typu radia" + }, "manual_pick_radio_type": { "data": { "radio_type": "Typ radia" @@ -235,6 +243,14 @@ "description": "Twoja kopia zapasowa ma inny adres IEEE ni\u017c twoje radio. Aby sie\u0107 dzia\u0142a\u0142a prawid\u0142owo, nale\u017cy r\u00f3wnie\u017c zmieni\u0107 adres IEEE radia. \n\nTo jest trwa\u0142a operacja.", "title": "Nadpisanie adresu IEEE radia" }, + "prompt_migrate_or_reconfigure": { + "description": "Czy migrujesz do nowego typu radia czy ponownie konfigurujesz obecne radio?", + "menu_options": { + "intent_migrate": "Migracja do nowego", + "intent_reconfigure": "Ponowna konfiguracja" + }, + "title": "Migracja czy ponowna konfiguracja" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Prze\u015blij plik" From 2decb85ee61ad16aae376edd15e179de842e54a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Oct 2022 15:51:45 -1000 Subject: [PATCH 277/985] Bump dbus-fast to 1.33.0 (#79921) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 54a690f7085..acdfce5acfd 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.29.1" + "dbus-fast==1.33.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 269e4b9e573..a1133f64c0d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.29.1 +dbus-fast==1.33.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 534683db0a1..cb52c6c52cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -543,7 +543,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.29.1 +dbus-fast==1.33.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e0ca76f41ec..8a219b7bbd5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.29.1 +dbus-fast==1.33.0 # homeassistant.components.debugpy debugpy==1.6.3 From 5b0a37a44752edbbf785d6a200e3b7a3f5fa2047 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sat, 8 Oct 2022 21:12:30 -0500 Subject: [PATCH 278/985] Use persistent device id for jellyfin requests (#79840) --- CODEOWNERS | 4 +- homeassistant/components/jellyfin/__init__.py | 10 +- .../components/jellyfin/client_wrapper.py | 21 +- .../components/jellyfin/config_flow.py | 20 +- homeassistant/components/jellyfin/const.py | 2 + .../components/jellyfin/manifest.json | 2 +- tests/components/jellyfin/conftest.py | 89 ++++++++ tests/components/jellyfin/test_config_flow.py | 204 +++++++++++------- 8 files changed, 254 insertions(+), 98 deletions(-) create mode 100644 tests/components/jellyfin/conftest.py diff --git a/CODEOWNERS b/CODEOWNERS index c7bc33d244f..9890a8f3502 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -568,8 +568,8 @@ build.json @home-assistant/supervisor /tests/components/isy994/ @bdraco @shbatm /homeassistant/components/izone/ @Swamp-Ig /tests/components/izone/ @Swamp-Ig -/homeassistant/components/jellyfin/ @j-stienstra -/tests/components/jellyfin/ @j-stienstra +/homeassistant/components/jellyfin/ @j-stienstra @ctalkington +/tests/components/jellyfin/ @j-stienstra @ctalkington /homeassistant/components/jewish_calendar/ @tsvi /tests/components/jewish_calendar/ @tsvi /homeassistant/components/juicenet/ @jesserockz diff --git a/homeassistant/components/jellyfin/__init__.py b/homeassistant/components/jellyfin/__init__.py index a58108b05ab..0126c05e4f2 100644 --- a/homeassistant/components/jellyfin/__init__.py +++ b/homeassistant/components/jellyfin/__init__.py @@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input -from .const import DATA_CLIENT, DOMAIN +from .const import CONF_CLIENT_DEVICE_ID, DATA_CLIENT, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -15,7 +15,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Jellyfin from a config entry.""" hass.data.setdefault(DOMAIN, {}) - client = create_client() + if CONF_CLIENT_DEVICE_ID not in entry.data: + entry_data = entry.data.copy() + entry_data[CONF_CLIENT_DEVICE_ID] = entry.entry_id + hass.config_entries.async_update_entry(entry, data=entry_data) + + client = create_client(device_id=entry.data[CONF_CLIENT_DEVICE_ID]) + try: await validate_input(hass, dict(entry.data), client) except CannotConnect as ex: diff --git a/homeassistant/components/jellyfin/client_wrapper.py b/homeassistant/components/jellyfin/client_wrapper.py index 9f6380e2181..65de5d4232e 100644 --- a/homeassistant/components/jellyfin/client_wrapper.py +++ b/homeassistant/components/jellyfin/client_wrapper.py @@ -3,7 +3,6 @@ from __future__ import annotations import socket from typing import Any -import uuid from jellyfin_apiclient_python import Jellyfin, JellyfinClient from jellyfin_apiclient_python.api import API @@ -34,22 +33,19 @@ async def validate_input( return userid -def create_client() -> JellyfinClient: +def create_client(device_id: str, device_name: str | None = None) -> JellyfinClient: """Create a new Jellyfin client.""" + if device_name is None: + device_name = socket.gethostname() + jellyfin = Jellyfin() + client = jellyfin.get_client() - _setup_client(client) - return client - - -def _setup_client(client: JellyfinClient) -> None: - """Configure the Jellyfin client with a number of required properties.""" - player_name = socket.gethostname() - client_uuid = str(uuid.uuid4()) - - client.config.app(USER_APP_NAME, CLIENT_VERSION, player_name, client_uuid) + client.config.app(USER_APP_NAME, CLIENT_VERSION, device_name, device_id) client.config.http(USER_AGENT) + return client + def _connect(client: JellyfinClient, url: str, username: str, password: str) -> str: """Connect to the Jellyfin server and assert that the user can login.""" @@ -75,6 +71,7 @@ def _login( ) -> None: """Assert that the user can log in to the Jellyfin server.""" response = connection_manager.login(url, username, password) + if "AccessToken" not in response: raise InvalidAuth diff --git a/homeassistant/components/jellyfin/config_flow.py b/homeassistant/components/jellyfin/config_flow.py index 6820031ed7b..51553f1a6f2 100644 --- a/homeassistant/components/jellyfin/config_flow.py +++ b/homeassistant/components/jellyfin/config_flow.py @@ -9,9 +9,10 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult +from homeassistant.util.uuid import random_uuid_hex from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input -from .const import DOMAIN +from .const import CONF_CLIENT_DEVICE_ID, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -24,11 +25,20 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) +def _generate_client_device_id() -> str: + """Generate a random UUID4 string to identify ourselves.""" + return random_uuid_hex() + + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Jellyfin.""" VERSION = 1 + def __init__(self) -> None: + """Initialize the Jellyfin config flow.""" + self.client_device_id: str | None = None + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -39,7 +49,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: - client = create_client() + if self.client_device_id is None: + self.client_device_id = _generate_client_device_id() + + client = create_client(device_id=self.client_device_id) try: userid = await validate_input(self.hass, user_input, client) except CannotConnect: @@ -54,7 +67,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return self.async_create_entry( - title=user_input[CONF_URL], data=user_input + title=user_input[CONF_URL], + data={CONF_CLIENT_DEVICE_ID: self.client_device_id, **user_input}, ) return self.async_show_form( diff --git a/homeassistant/components/jellyfin/const.py b/homeassistant/components/jellyfin/const.py index 1f679fd43c8..182144806d2 100644 --- a/homeassistant/components/jellyfin/const.py +++ b/homeassistant/components/jellyfin/const.py @@ -9,6 +9,8 @@ CLIENT_VERSION: Final = "1.0" COLLECTION_TYPE_MOVIES: Final = "movies" COLLECTION_TYPE_MUSIC: Final = "music" +CONF_CLIENT_DEVICE_ID: Final = "client_device_id" + DATA_CLIENT: Final = "client" ITEM_KEY_COLLECTION_TYPE: Final = "CollectionType" diff --git a/homeassistant/components/jellyfin/manifest.json b/homeassistant/components/jellyfin/manifest.json index 48f4cf0c837..e2189bed2cb 100644 --- a/homeassistant/components/jellyfin/manifest.json +++ b/homeassistant/components/jellyfin/manifest.json @@ -5,6 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/jellyfin", "requirements": ["jellyfin-apiclient-python==1.8.1"], "iot_class": "local_polling", - "codeowners": ["@j-stienstra"], + "codeowners": ["@j-stienstra", "@ctalkington"], "loggers": ["jellyfin_apiclient_python"] } diff --git a/tests/components/jellyfin/conftest.py b/tests/components/jellyfin/conftest.py new file mode 100644 index 00000000000..c1d9634aede --- /dev/null +++ b/tests/components/jellyfin/conftest.py @@ -0,0 +1,89 @@ +"""Fixtures for Jellyfin integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, create_autospec, patch + +from jellyfin_apiclient_python import JellyfinClient +from jellyfin_apiclient_python.api import API +from jellyfin_apiclient_python.configuration import Config +from jellyfin_apiclient_python.connection_manager import ConnectionManager +import pytest + +from .const import ( + MOCK_SUCCESFUL_CONNECTION_STATE, + MOCK_SUCCESFUL_LOGIN_RESPONSE, + MOCK_USER_SETTINGS, +) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.jellyfin.async_setup_entry", return_value=True + ) as setup_mock: + yield setup_mock + + +@pytest.fixture +def mock_client_device_id() -> Generator[None, MagicMock, None]: + """Mock generating device id.""" + with patch( + "homeassistant.components.jellyfin.config_flow._generate_client_device_id" + ) as id_mock: + id_mock.return_value = "TEST-UUID" + yield id_mock + + +@pytest.fixture +def mock_auth() -> MagicMock: + """Return a mocked ConnectionManager.""" + jf_auth = create_autospec(ConnectionManager) + jf_auth.connect_to_address.return_value = MOCK_SUCCESFUL_CONNECTION_STATE + jf_auth.login.return_value = MOCK_SUCCESFUL_LOGIN_RESPONSE + + return jf_auth + + +@pytest.fixture +def mock_api() -> MagicMock: + """Return a mocked API.""" + jf_api = create_autospec(API) + jf_api.get_user_settings.return_value = MOCK_USER_SETTINGS + + return jf_api + + +@pytest.fixture +def mock_config() -> MagicMock: + """Return a mocked JellyfinClient.""" + jf_config = create_autospec(Config) + jf_config.data = {} + + return jf_config + + +@pytest.fixture +def mock_client( + mock_config: MagicMock, mock_auth: MagicMock, mock_api: MagicMock +) -> MagicMock: + """Return a mocked JellyfinClient.""" + jf_client = create_autospec(JellyfinClient) + jf_client.auth = mock_auth + jf_client.config = mock_config + jf_client.jellyfin = mock_api + + return jf_client + + +@pytest.fixture +def mock_jellyfin(mock_client: MagicMock) -> Generator[None, MagicMock, None]: + """Return a mocked Jellyfin.""" + with patch( + "homeassistant.components.jellyfin.client_wrapper.Jellyfin", autospec=True + ) as jellyfin_mock: + jf = jellyfin_mock.return_value + jf.get_client.return_value = mock_client + + yield jf diff --git a/tests/components/jellyfin/test_config_flow.py b/tests/components/jellyfin/test_config_flow.py index e898f8ac5ce..be90e521ac1 100644 --- a/tests/components/jellyfin/test_config_flow.py +++ b/tests/components/jellyfin/test_config_flow.py @@ -1,17 +1,15 @@ """Test the jellyfin config flow.""" -from unittest.mock import patch +from unittest.mock import MagicMock from homeassistant import config_entries, data_entry_flow -from homeassistant.components.jellyfin.const import DOMAIN +from homeassistant.components.jellyfin.const import CONF_CLIENT_DEVICE_ID, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant from .const import ( - MOCK_SUCCESFUL_CONNECTION_STATE, MOCK_SUCCESFUL_LOGIN_RESPONSE, MOCK_UNSUCCESFUL_CONNECTION_STATE, MOCK_UNSUCCESFUL_LOGIN_RESPONSE, - MOCK_USER_SETTINGS, TEST_PASSWORD, TEST_URL, TEST_USERNAME, @@ -31,52 +29,52 @@ async def test_abort_if_existing_entry(hass: HomeAssistant): assert result["reason"] == "single_instance_allowed" -async def test_form(hass: HomeAssistant): +async def test_form( + hass: HomeAssistant, + mock_jellyfin: MagicMock, + mock_client: MagicMock, + mock_client_device_id: MagicMock, + mock_setup_entry: MagicMock, +): """Test the complete configuration form.""" 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.jellyfin.client_wrapper.ConnectionManager.connect_to_address", - return_value=MOCK_SUCCESFUL_CONNECTION_STATE, - ) as mock_connect, patch( - "homeassistant.components.jellyfin.client_wrapper.ConnectionManager.login", - return_value=MOCK_SUCCESFUL_LOGIN_RESPONSE, - ) as mock_login, patch( - "homeassistant.components.jellyfin.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.jellyfin.client_wrapper.API.get_user_settings", - return_value=MOCK_USER_SETTINGS, - ) as mock_set_id: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_URL: TEST_URL, - CONF_USERNAME: TEST_USERNAME, - CONF_PASSWORD: TEST_PASSWORD, - }, - ) - await hass.async_block_till_done() + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + await hass.async_block_till_done() assert result2["type"] == "create_entry" assert result2["title"] == TEST_URL assert result2["data"] == { + CONF_CLIENT_DEVICE_ID: "TEST-UUID", CONF_URL: TEST_URL, CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, } - assert len(mock_connect.mock_calls) == 1 - assert len(mock_login.mock_calls) == 1 + assert len(mock_client.auth.connect_to_address.mock_calls) == 1 + assert len(mock_client.auth.login.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_set_id.mock_calls) == 1 + assert len(mock_client.jellyfin.get_user_settings.mock_calls) == 1 -async def test_form_cannot_connect(hass: HomeAssistant): +async def test_form_cannot_connect( + hass: HomeAssistant, + mock_jellyfin: MagicMock, + mock_client: MagicMock, + mock_client_device_id: MagicMock, +): """Test we handle an unreachable server.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -84,27 +82,30 @@ async def test_form_cannot_connect(hass: HomeAssistant): assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.jellyfin.client_wrapper.ConnectionManager.connect_to_address", - return_value=MOCK_UNSUCCESFUL_CONNECTION_STATE, - ) as mock_connect: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_URL: TEST_URL, - CONF_USERNAME: TEST_USERNAME, - CONF_PASSWORD: TEST_PASSWORD, - }, - ) + mock_client.auth.connect_to_address.return_value = MOCK_UNSUCCESFUL_CONNECTION_STATE + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) await hass.async_block_till_done() assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} - assert len(mock_connect.mock_calls) == 1 + assert len(mock_client.auth.connect_to_address.mock_calls) == 1 -async def test_form_invalid_auth(hass: HomeAssistant): +async def test_form_invalid_auth( + hass: HomeAssistant, + mock_jellyfin: MagicMock, + mock_client: MagicMock, + mock_client_device_id: MagicMock, +): """Test that we can handle invalid credentials.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -112,31 +113,28 @@ async def test_form_invalid_auth(hass: HomeAssistant): assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.jellyfin.client_wrapper.ConnectionManager.connect_to_address", - return_value=MOCK_SUCCESFUL_CONNECTION_STATE, - ) as mock_connect, patch( - "homeassistant.components.jellyfin.client_wrapper.ConnectionManager.login", - return_value=MOCK_UNSUCCESFUL_LOGIN_RESPONSE, - ) as mock_login: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_URL: TEST_URL, - CONF_USERNAME: TEST_USERNAME, - CONF_PASSWORD: TEST_PASSWORD, - }, - ) - await hass.async_block_till_done() + mock_client.auth.login.return_value = MOCK_UNSUCCESFUL_LOGIN_RESPONSE + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + await hass.async_block_till_done() assert result2["type"] == "form" assert result2["errors"] == {"base": "invalid_auth"} - assert len(mock_connect.mock_calls) == 1 - assert len(mock_login.mock_calls) == 1 + assert len(mock_client.auth.connect_to_address.mock_calls) == 1 + assert len(mock_client.auth.login.mock_calls) == 1 -async def test_form_exception(hass: HomeAssistant): +async def test_form_exception( + hass: HomeAssistant, mock_jellyfin: MagicMock, mock_client: MagicMock +): """Test we handle an unexpected exception during server setup.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -144,21 +142,71 @@ async def test_form_exception(hass: HomeAssistant): assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.jellyfin.client_wrapper.ConnectionManager.connect_to_address", - side_effect=Exception("UnknownException"), - ) as mock_connect: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_URL: TEST_URL, - CONF_USERNAME: TEST_USERNAME, - CONF_PASSWORD: TEST_PASSWORD, - }, - ) - await hass.async_block_till_done() + mock_client.auth.connect_to_address.side_effect = Exception("UnknownException") + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + await hass.async_block_till_done() assert result2["type"] == "form" assert result2["errors"] == {"base": "unknown"} - assert len(mock_connect.mock_calls) == 1 + assert len(mock_client.auth.connect_to_address.mock_calls) == 1 + + +async def test_form_persists_device_id_on_error( + hass: HomeAssistant, + mock_jellyfin: MagicMock, + mock_client: MagicMock, + mock_client_device_id: MagicMock, +): + """Test that we can handle invalid credentials.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mock_client_device_id.return_value = "TEST-UUID-1" + mock_client.auth.login.return_value = MOCK_UNSUCCESFUL_LOGIN_RESPONSE + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + mock_client_device_id.return_value = "TEST-UUID-2" + mock_client.auth.login.return_value = MOCK_SUCCESFUL_LOGIN_RESPONSE + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + await hass.async_block_till_done() + + assert result3 + assert result3["type"] == "create_entry" + assert result3["data"] == { + CONF_CLIENT_DEVICE_ID: "TEST-UUID-1", + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + } From 6eb2c96d32b57d7afa897cdec0e326c112ec8dff Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 9 Oct 2022 14:41:30 +0200 Subject: [PATCH 279/985] Correct use of ConfigType in MQTT config flow code (#79934) Correct use of ConfigType --- homeassistant/components/mqtt/config_flow.py | 32 +++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index df7b6137549..47eeceb56c2 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -58,7 +58,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return MQTTOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -66,13 +68,13 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_broker() async def async_step_broker( - self, user_input: ConfigType | None = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm the setup.""" yaml_config: ConfigType = get_mqtt_data(self.hass, True).config or {} errors: dict[str, str] = {} fields: OrderedDict[Any, Any] = OrderedDict() - validated_user_input: ConfigType = {} + validated_user_input: dict[str, Any] = {} if await async_get_broker_settings( self.hass, fields, @@ -82,7 +84,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): validated_user_input, errors, ): - test_config: ConfigType = yaml_config.copy() + test_config: dict[str, Any] = yaml_config.copy() test_config.update(validated_user_input) can_connect = await self.hass.async_add_executor_job( try_connection, @@ -118,7 +120,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): assert self._hassio_discovery if user_input is not None: - data: ConfigType = self._hassio_discovery.copy() + data: dict[str, Any] = self._hassio_discovery.copy() data[CONF_BROKER] = data.pop(CONF_HOST) can_connect = await self.hass.async_add_executor_job( try_connection, @@ -166,7 +168,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): errors: dict[str, str] = {} yaml_config: ConfigType = get_mqtt_data(self.hass, True).config or {} fields: OrderedDict[Any, Any] = OrderedDict() - validated_user_input: ConfigType = {} + validated_user_input: dict[str, Any] = {} if await async_get_broker_settings( self.hass, fields, @@ -176,7 +178,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): validated_user_input, errors, ): - test_config: ConfigType = yaml_config.copy() + test_config: dict[str, Any] = yaml_config.copy() test_config.update(validated_user_input) can_connect = await self.hass.async_add_executor_job( try_connection, @@ -197,13 +199,13 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): ) async def async_step_options( - self, user_input: ConfigType | None = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the MQTT options.""" errors = {} current_config = self.config_entry.data yaml_config = get_mqtt_data(self.hass, True).config or {} - options_config: ConfigType = {} + options_config: dict[str, Any] = {} bad_input: bool = False def _birth_will(birt_or_will: str) -> dict: @@ -217,7 +219,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): } def _validate( - field: str, values: ConfigType, error_code: str, schema: Callable + field: str, values: dict[str, Any], error_code: str, schema: Callable ): """Validate the user input.""" nonlocal bad_input @@ -337,16 +339,16 @@ async def async_get_broker_settings( fields: OrderedDict[Any, Any], yaml_config: ConfigType, entry_config: MappingProxyType[str, Any] | None, - user_input: ConfigType | None, - validated_user_input: ConfigType, + user_input: dict[str, Any] | None, + validated_user_input: dict[str, Any], errors: dict[str, str], ) -> bool: """Build the config flow schema to collect the broker settings. Returns True when settings are collected successfully. """ - user_input_basic: ConfigType = ConfigType() - current_config = entry_config.copy() if entry_config is not None else ConfigType() + user_input_basic: dict[str, Any] = {} + current_config = entry_config.copy() if entry_config is not None else {} if user_input is not None: validated_user_input.update(user_input) @@ -384,7 +386,7 @@ async def async_get_broker_settings( def try_connection( - user_input: ConfigType, + user_input: dict[str, Any], ) -> bool: """Test if we can connect to an MQTT broker.""" # We don't import on the top because some integrations From 8176400cfd8985213dc6f6bb02d7ef840db168a0 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 9 Oct 2022 12:48:01 +0000 Subject: [PATCH 280/985] Migrate attributes to separate sensors in Brother integration (#79932) Migrate attributes to sensors --- homeassistant/components/brother/sensor.py | 137 ++++++++++++++------- tests/components/brother/test_sensor.py | 120 ++++++++++++++++-- 2 files changed, 200 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 73d7c2710b5..2b82ac0cdb8 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -5,7 +5,6 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import datetime import logging -from typing import Any from brother import BrotherSensors @@ -41,7 +40,6 @@ class BrotherSensorRequiredKeysMixin: """Class for Brother entity required keys.""" value: Callable[[BrotherSensors], StateType | datetime] - extra_state_attrs: Callable[[BrotherSensors], dict[str, Any]] @dataclass @@ -58,7 +56,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( name="Status", entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.status, - extra_state_attrs=lambda _: {}, ), BrotherSensorEntityDescription( key="page_counter", @@ -68,7 +65,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.page_counter, - extra_state_attrs=lambda _: {}, ), BrotherSensorEntityDescription( key="bw_counter", @@ -78,7 +74,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.bw_counter, - extra_state_attrs=lambda _: {}, ), BrotherSensorEntityDescription( key="color_counter", @@ -88,7 +83,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.color_counter, - extra_state_attrs=lambda _: {}, ), BrotherSensorEntityDescription( key="duplex_unit_pages_counter", @@ -98,7 +92,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.duplex_unit_pages_counter, - extra_state_attrs=lambda _: {}, ), BrotherSensorEntityDescription( key="drum_remaining_life", @@ -108,10 +101,24 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.drum_remaining_life, - extra_state_attrs=lambda data: { - ATTR_REMAINING_PAGES: data.drum_remaining_pages, - ATTR_COUNTER: data.drum_counter, - }, + ), + BrotherSensorEntityDescription( + key="drum_remaining_pages", + icon="mdi:chart-donut", + name="Drum remaining pages", + native_unit_of_measurement=UNIT_PAGES, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.drum_remaining_pages, + ), + BrotherSensorEntityDescription( + key="drum_counter", + icon="mdi:chart-donut", + name="Drum counter", + native_unit_of_measurement=UNIT_PAGES, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.drum_counter, ), BrotherSensorEntityDescription( key="black_drum_remaining_life", @@ -121,10 +128,24 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.black_drum_remaining_life, - extra_state_attrs=lambda data: { - ATTR_REMAINING_PAGES: data.black_drum_remaining_pages, - ATTR_COUNTER: data.black_drum_counter, - }, + ), + BrotherSensorEntityDescription( + key="black_drum_remaining_pages", + icon="mdi:chart-donut", + name="Black drum remaining pages", + native_unit_of_measurement=UNIT_PAGES, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.black_drum_remaining_pages, + ), + BrotherSensorEntityDescription( + key="black_drum_counter", + icon="mdi:chart-donut", + name="Black drum counter", + native_unit_of_measurement=UNIT_PAGES, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.black_drum_counter, ), BrotherSensorEntityDescription( key="cyan_drum_remaining_life", @@ -134,10 +155,24 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.cyan_drum_remaining_life, - extra_state_attrs=lambda data: { - ATTR_REMAINING_PAGES: data.cyan_drum_remaining_pages, - ATTR_COUNTER: data.cyan_drum_counter, - }, + ), + BrotherSensorEntityDescription( + key="cyan_drum_remaining_pages", + icon="mdi:chart-donut", + name="Cyan drum remaining pages", + native_unit_of_measurement=UNIT_PAGES, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.cyan_drum_remaining_pages, + ), + BrotherSensorEntityDescription( + key="cyan_drum_counter", + icon="mdi:chart-donut", + name="Cyan drum counter", + native_unit_of_measurement=UNIT_PAGES, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.cyan_drum_counter, ), BrotherSensorEntityDescription( key="magenta_drum_remaining_life", @@ -147,10 +182,24 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.magenta_drum_remaining_life, - extra_state_attrs=lambda data: { - ATTR_REMAINING_PAGES: data.magenta_drum_remaining_pages, - ATTR_COUNTER: data.magenta_drum_counter, - }, + ), + BrotherSensorEntityDescription( + key="magenta_drum_remaining_pages", + icon="mdi:chart-donut", + name="Magenta drum remaining pages", + native_unit_of_measurement=UNIT_PAGES, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.magenta_drum_remaining_pages, + ), + BrotherSensorEntityDescription( + key="magenta_drum_counter", + icon="mdi:chart-donut", + name="Magenta drum counter", + native_unit_of_measurement=UNIT_PAGES, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.magenta_drum_counter, ), BrotherSensorEntityDescription( key="yellow_drum_remaining_life", @@ -160,10 +209,24 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.yellow_drum_remaining_life, - extra_state_attrs=lambda data: { - ATTR_REMAINING_PAGES: data.yellow_drum_remaining_pages, - ATTR_COUNTER: data.yellow_drum_counter, - }, + ), + BrotherSensorEntityDescription( + key="yellow_drum_remaining_pages", + icon="mdi:chart-donut", + name="Yellow drum remaining pages", + native_unit_of_measurement=UNIT_PAGES, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.yellow_drum_remaining_pages, + ), + BrotherSensorEntityDescription( + key="yellow_drum_counter", + icon="mdi:chart-donut", + name="Yellow drum counter", + native_unit_of_measurement=UNIT_PAGES, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda data: data.yellow_drum_counter, ), BrotherSensorEntityDescription( key="belt_unit_remaining_life", @@ -173,7 +236,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.belt_unit_remaining_life, - extra_state_attrs=lambda _: {}, ), BrotherSensorEntityDescription( key="fuser_remaining_life", @@ -183,7 +245,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.fuser_remaining_life, - extra_state_attrs=lambda _: {}, ), BrotherSensorEntityDescription( key="laser_remaining_life", @@ -193,7 +254,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.laser_remaining_life, - extra_state_attrs=lambda _: {}, ), BrotherSensorEntityDescription( key="pf_kit_1_remaining_life", @@ -203,7 +263,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.pf_kit_1_remaining_life, - extra_state_attrs=lambda _: {}, ), BrotherSensorEntityDescription( key="pf_kit_mp_remaining_life", @@ -213,7 +272,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.pf_kit_mp_remaining_life, - extra_state_attrs=lambda _: {}, ), BrotherSensorEntityDescription( key="black_toner_remaining", @@ -223,7 +281,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.black_toner_remaining, - extra_state_attrs=lambda _: {}, ), BrotherSensorEntityDescription( key="cyan_toner_remaining", @@ -233,7 +290,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.cyan_toner_remaining, - extra_state_attrs=lambda _: {}, ), BrotherSensorEntityDescription( key="magenta_toner_remaining", @@ -243,7 +299,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.magenta_toner_remaining, - extra_state_attrs=lambda _: {}, ), BrotherSensorEntityDescription( key="yellow_toner_remaining", @@ -253,7 +308,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.yellow_toner_remaining, - extra_state_attrs=lambda _: {}, ), BrotherSensorEntityDescription( key="black_ink_remaining", @@ -263,7 +317,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.black_ink_remaining, - extra_state_attrs=lambda _: {}, ), BrotherSensorEntityDescription( key="cyan_ink_remaining", @@ -273,7 +326,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.cyan_ink_remaining, - extra_state_attrs=lambda _: {}, ), BrotherSensorEntityDescription( key="magenta_ink_remaining", @@ -283,7 +335,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.magenta_ink_remaining, - extra_state_attrs=lambda _: {}, ), BrotherSensorEntityDescription( key="yellow_ink_remaining", @@ -293,7 +344,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.yellow_ink_remaining, - extra_state_attrs=lambda _: {}, ), BrotherSensorEntityDescription( key="uptime", @@ -302,7 +352,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, value=lambda data: data.uptime, - extra_state_attrs=lambda _: {}, ), ) @@ -361,9 +410,6 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): """Initialize.""" super().__init__(coordinator) self._attr_device_info = device_info - self._attr_extra_state_attributes = description.extra_state_attrs( - coordinator.data - ) self._attr_native_value = description.value(coordinator.data) self._attr_unique_id = f"{coordinator.data.serial.lower()}_{description.key}" self.entity_description = description @@ -372,7 +418,4 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" self._attr_native_value = self.entity_description.value(self.coordinator.data) - self._attr_extra_state_attributes = self.entity_description.extra_state_attrs( - self.coordinator.data - ) self.async_write_ha_state() diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index 9212e12e5b3..58ccecaf29f 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -113,8 +113,6 @@ async def test_sensors(hass: HomeAssistant) -> None: state = hass.states.get("sensor.hl_l2340dw_drum_remaining_life") assert state assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" - assert state.attributes.get(ATTR_REMAINING_PAGES) == 11014 - assert state.attributes.get(ATTR_COUNTER) == 986 assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -123,11 +121,31 @@ async def test_sensors(hass: HomeAssistant) -> None: assert entry assert entry.unique_id == "0123456789_drum_remaining_life" + state = hass.states.get("sensor.hl_l2340dw_drum_remaining_pages") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES + assert state.state == "11014" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = registry.async_get("sensor.hl_l2340dw_drum_remaining_pages") + assert entry + assert entry.unique_id == "0123456789_drum_remaining_pages" + + state = hass.states.get("sensor.hl_l2340dw_drum_counter") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES + assert state.state == "986" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = registry.async_get("sensor.hl_l2340dw_drum_counter") + assert entry + assert entry.unique_id == "0123456789_drum_counter" + state = hass.states.get("sensor.hl_l2340dw_black_drum_remaining_life") assert state assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" - assert state.attributes.get(ATTR_REMAINING_PAGES) == 16389 - assert state.attributes.get(ATTR_COUNTER) == 1611 assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -136,11 +154,31 @@ async def test_sensors(hass: HomeAssistant) -> None: assert entry assert entry.unique_id == "0123456789_black_drum_remaining_life" + state = hass.states.get("sensor.hl_l2340dw_black_drum_remaining_pages") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES + assert state.state == "16389" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = registry.async_get("sensor.hl_l2340dw_black_drum_remaining_pages") + assert entry + assert entry.unique_id == "0123456789_black_drum_remaining_pages" + + state = hass.states.get("sensor.hl_l2340dw_black_drum_counter") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES + assert state.state == "1611" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = registry.async_get("sensor.hl_l2340dw_black_drum_counter") + assert entry + assert entry.unique_id == "0123456789_black_drum_counter" + state = hass.states.get("sensor.hl_l2340dw_cyan_drum_remaining_life") assert state assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" - assert state.attributes.get(ATTR_REMAINING_PAGES) == 16389 - assert state.attributes.get(ATTR_COUNTER) == 1611 assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -149,11 +187,31 @@ async def test_sensors(hass: HomeAssistant) -> None: assert entry assert entry.unique_id == "0123456789_cyan_drum_remaining_life" + state = hass.states.get("sensor.hl_l2340dw_cyan_drum_remaining_pages") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES + assert state.state == "16389" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = registry.async_get("sensor.hl_l2340dw_cyan_drum_remaining_pages") + assert entry + assert entry.unique_id == "0123456789_cyan_drum_remaining_pages" + + state = hass.states.get("sensor.hl_l2340dw_cyan_drum_counter") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES + assert state.state == "1611" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = registry.async_get("sensor.hl_l2340dw_cyan_drum_counter") + assert entry + assert entry.unique_id == "0123456789_cyan_drum_counter" + state = hass.states.get("sensor.hl_l2340dw_magenta_drum_remaining_life") assert state assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" - assert state.attributes.get(ATTR_REMAINING_PAGES) == 16389 - assert state.attributes.get(ATTR_COUNTER) == 1611 assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -162,11 +220,31 @@ async def test_sensors(hass: HomeAssistant) -> None: assert entry assert entry.unique_id == "0123456789_magenta_drum_remaining_life" + state = hass.states.get("sensor.hl_l2340dw_magenta_drum_remaining_pages") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES + assert state.state == "16389" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = registry.async_get("sensor.hl_l2340dw_magenta_drum_remaining_pages") + assert entry + assert entry.unique_id == "0123456789_magenta_drum_remaining_pages" + + state = hass.states.get("sensor.hl_l2340dw_magenta_drum_counter") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES + assert state.state == "1611" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = registry.async_get("sensor.hl_l2340dw_magenta_drum_counter") + assert entry + assert entry.unique_id == "0123456789_magenta_drum_counter" + state = hass.states.get("sensor.hl_l2340dw_yellow_drum_remaining_life") assert state assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" - assert state.attributes.get(ATTR_REMAINING_PAGES) == 16389 - assert state.attributes.get(ATTR_COUNTER) == 1611 assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -175,6 +253,28 @@ async def test_sensors(hass: HomeAssistant) -> None: assert entry assert entry.unique_id == "0123456789_yellow_drum_remaining_life" + state = hass.states.get("sensor.hl_l2340dw_yellow_drum_remaining_pages") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES + assert state.state == "16389" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = registry.async_get("sensor.hl_l2340dw_yellow_drum_remaining_pages") + assert entry + assert entry.unique_id == "0123456789_yellow_drum_remaining_pages" + + state = hass.states.get("sensor.hl_l2340dw_yellow_drum_counter") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES + assert state.state == "1611" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = registry.async_get("sensor.hl_l2340dw_yellow_drum_counter") + assert entry + assert entry.unique_id == "0123456789_yellow_drum_counter" + state = hass.states.get("sensor.hl_l2340dw_fuser_remaining_life") assert state assert state.attributes.get(ATTR_ICON) == "mdi:water-outline" From 031370358cb3a137a946116cdeebb8e0bf807627 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 9 Oct 2022 14:50:21 +0200 Subject: [PATCH 281/985] Remove yaml import openexchangerates (#79856) * Depr openexchangerates yaml * Remove setup_platform and issue * Remove schema * Remove not longer used constant --- .../openexchangerates/config_flow.py | 4 -- .../components/openexchangerates/sensor.py | 56 ++----------------- .../components/openexchangerates/strings.json | 4 +- .../openexchangerates/translations/en.json | 4 +- .../openexchangerates/test_config_flow.py | 29 ---------- 5 files changed, 8 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/openexchangerates/config_flow.py b/homeassistant/components/openexchangerates/config_flow.py index 3c22f3e0fe0..13060e19718 100644 --- a/homeassistant/components/openexchangerates/config_flow.py +++ b/homeassistant/components/openexchangerates/config_flow.py @@ -126,7 +126,3 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except asyncio.TimeoutError as err: raise AbortFlow("timeout_connect") from err return self.currencies - - async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: - """Handle import from yaml/configuration.""" - return await self.async_step_user(import_config) diff --git a/homeassistant/components/openexchangerates/sensor.py b/homeassistant/components/openexchangerates/sensor.py index 76573b351b3..f73f78cb4e8 100644 --- a/homeassistant/components/openexchangerates/sensor.py +++ b/homeassistant/components/openexchangerates/sensor.py @@ -1,68 +1,20 @@ """Support for openexchangerates.org exchange rates service.""" from __future__ import annotations -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_BASE, CONF_NAME, CONF_QUOTE +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME, CONF_QUOTE from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DEFAULT_BASE, DOMAIN, LOGGER +from .const import DOMAIN from .coordinator import OpenexchangeratesCoordinator ATTRIBUTION = "Data provided by openexchangerates.org" -DEFAULT_NAME = "Exchange Rate Sensor" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_QUOTE): cv.string, - vol.Optional(CONF_BASE, default=DEFAULT_BASE): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Open Exchange Rates sensor.""" - async_create_issue( - hass, - DOMAIN, - "deprecated_yaml", - breaks_in_ha_version="2022.11.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - - LOGGER.warning( - "Configuration of Open Exchange Rates integration in YAML is deprecated and " - "will be removed in Home Assistant 2022.11.; Your existing configuration " - "has been imported into the UI automatically and can be safely removed from" - " your configuration.yaml file" - ) - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/openexchangerates/strings.json b/homeassistant/components/openexchangerates/strings.json index 57180e367aa..5c4a833e0d6 100644 --- a/homeassistant/components/openexchangerates/strings.json +++ b/homeassistant/components/openexchangerates/strings.json @@ -26,8 +26,8 @@ }, "issues": { "deprecated_yaml": { - "title": "The Open Exchange Rates YAML configuration is being removed", - "description": "Configuring Open Exchange Rates using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Open Exchange Rates YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + "title": "The Open Exchange Rates YAML configuration has been removed", + "description": "Configuring Open Exchange Rates using YAML has been removed.\n\nRemove the Open Exchange Rates YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." } } } diff --git a/homeassistant/components/openexchangerates/translations/en.json b/homeassistant/components/openexchangerates/translations/en.json index 011953904ff..f4827c4df4d 100644 --- a/homeassistant/components/openexchangerates/translations/en.json +++ b/homeassistant/components/openexchangerates/translations/en.json @@ -26,8 +26,8 @@ }, "issues": { "deprecated_yaml": { - "description": "Configuring Open Exchange Rates using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Open Exchange Rates YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", - "title": "The Open Exchange Rates YAML configuration is being removed" + "description": "Configuring Open Exchange Rates using YAML has been removed.\n\nRemove the Open Exchange Rates YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Open Exchange Rates YAML configuration has been removed" } } } \ No newline at end of file diff --git a/tests/components/openexchangerates/test_config_flow.py b/tests/components/openexchangerates/test_config_flow.py index ee4ba57de2c..213badcab08 100644 --- a/tests/components/openexchangerates/test_config_flow.py +++ b/tests/components/openexchangerates/test_config_flow.py @@ -237,32 +237,3 @@ async def test_reauth( assert result["type"] == "abort" assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_create_entry( - hass: HomeAssistant, - mock_latest_rates_config_flow: AsyncMock, - mock_setup_entry: AsyncMock, -) -> None: - """Test we can import data from configuration.yaml.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - "api_key": "test-api-key", - "base": "USD", - "quote": "EUR", - "name": "test", - }, - ) - await hass.async_block_till_done() - - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "USD" - assert result["data"] == { - "api_key": "test-api-key", - "base": "USD", - "quote": "EUR", - "name": "test", - } - assert len(mock_setup_entry.mock_calls) == 1 From 3126762707f49775c78b9beb484dff17bf6941cb Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Sun, 9 Oct 2022 15:11:42 +0200 Subject: [PATCH 282/985] Add friendly name to ZHA config entities (#79926) * Add friendly name to ZHA config entities * Follow HA capitalization conventions * Change from "Start-up level" to "Start-up current level" * Remove siren select friendly names (temporarily) * Change tests to expect new entity ids * Re-add siren select friendly names * Change siren tests to expect new entity ids --- homeassistant/components/zha/button.py | 2 + homeassistant/components/zha/number.py | 9 ++++ homeassistant/components/zha/select.py | 9 ++++ homeassistant/components/zha/switch.py | 3 ++ tests/components/zha/test_device_action.py | 8 ++-- tests/components/zha/test_number.py | 2 +- tests/components/zha/test_select.py | 10 ++--- tests/components/zha/zha_devices_list.py | 48 +++++++++++----------- 8 files changed, 57 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py index fcc040cbde2..cb0463a855f 100644 --- a/homeassistant/components/zha/button.py +++ b/homeassistant/components/zha/button.py @@ -159,6 +159,7 @@ class FrostLockResetButton(ZHAAttributeButton, id_suffix="reset_frost_lock"): """Defines a ZHA frost lock reset button.""" _attribute_name = "frost_lock_reset" + _attr_name = "Frost lock reset" _attribute_value = 0 _attr_device_class = ButtonDeviceClass.RESTART _attr_entity_category = EntityCategory.CONFIG @@ -171,6 +172,7 @@ class NoPresenceStatusResetButton( """Defines a ZHA no presence status reset button.""" _attribute_name = "reset_no_presence_status" + _attr_name = "Presence status reset" _attribute_value = 1 _attr_device_class = ButtonDeviceClass.RESTART _attr_entity_category = EntityCategory.CONFIG diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 3bace412744..9203986057d 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -455,6 +455,7 @@ class AqaraMotionDetectionInterval( _attr_native_min_value: float = 2 _attr_native_max_value: float = 65535 _zcl_attribute: str = "detection_interval" + _attr_name = "Detection interval" @CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL) @@ -466,6 +467,7 @@ class OnOffTransitionTimeConfigurationEntity( _attr_native_min_value: float = 0x0000 _attr_native_max_value: float = 0xFFFF _zcl_attribute: str = "on_off_transition_time" + _attr_name = "On/Off transition time" @CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL) @@ -475,6 +477,7 @@ class OnLevelConfigurationEntity(ZHANumberConfigurationEntity, id_suffix="on_lev _attr_native_min_value: float = 0x00 _attr_native_max_value: float = 0xFF _zcl_attribute: str = "on_level" + _attr_name = "On level" @CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL) @@ -486,6 +489,7 @@ class OnTransitionTimeConfigurationEntity( _attr_native_min_value: float = 0x0000 _attr_native_max_value: float = 0xFFFE _zcl_attribute: str = "on_transition_time" + _attr_name = "On transition time" @CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL) @@ -497,6 +501,7 @@ class OffTransitionTimeConfigurationEntity( _attr_native_min_value: float = 0x0000 _attr_native_max_value: float = 0xFFFE _zcl_attribute: str = "off_transition_time" + _attr_name = "Off transition time" @CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL) @@ -508,6 +513,7 @@ class DefaultMoveRateConfigurationEntity( _attr_native_min_value: float = 0x00 _attr_native_max_value: float = 0xFE _zcl_attribute: str = "default_move_rate" + _attr_name = "Default move rate" @CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL) @@ -519,6 +525,7 @@ class StartUpCurrentLevelConfigurationEntity( _attr_native_min_value: float = 0x00 _attr_native_max_value: float = 0xFF _zcl_attribute: str = "start_up_current_level" + _attr_name = "Start-up current level" @CONFIG_DIAGNOSTIC_MATCH( @@ -536,6 +543,7 @@ class TimerDurationMinutes(ZHANumberConfigurationEntity, id_suffix="timer_durati _attr_native_max_value: float = 0x257 _attr_native_unit_of_measurement: str | None = UNITS[72] _zcl_attribute: str = "timer_duration" + _attr_name = "Timer duration" @CONFIG_DIAGNOSTIC_MATCH(channel_names="ikea_airpurifier") @@ -548,6 +556,7 @@ class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time") _attr_native_max_value: float = 0xFFFFFFFF _attr_native_unit_of_measurement: str | None = UNITS[72] _zcl_attribute: str = "filter_life_time" + _attr_name = "Filter life time" @CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI) diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 8b2623b4de1..c2f315cd217 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -123,6 +123,7 @@ class ZHADefaultToneSelectEntity( """Representation of a ZHA default siren tone select entity.""" _enum = IasWd.Warning.WarningMode + _attr_name = "Default siren tone" @CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_IAS_WD) @@ -132,6 +133,7 @@ class ZHADefaultSirenLevelSelectEntity( """Representation of a ZHA default siren level select entity.""" _enum = IasWd.Warning.SirenLevel + _attr_name = "Default siren level" @CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_IAS_WD) @@ -141,6 +143,7 @@ class ZHADefaultStrobeLevelSelectEntity( """Representation of a ZHA default siren strobe level select entity.""" _enum = IasWd.StrobeLevel + _attr_name = "Default strobe level" @CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_IAS_WD) @@ -148,6 +151,7 @@ class ZHADefaultStrobeSelectEntity(ZHANonZCLSelectEntity, id_suffix=Strobe.__nam """Representation of a ZHA default siren strobe select entity.""" _enum = Strobe + _attr_name = "Default strobe" class ZCLEnumSelectEntity(ZhaEntity, SelectEntity): @@ -220,6 +224,7 @@ class ZHAStartupOnOffSelectEntity( _select_attr = "start_up_on_off" _enum = OnOff.StartUpOnOff + _attr_name = "Start-up behavior" class AqaraMotionSensitivities(types.enum8): @@ -238,6 +243,7 @@ class AqaraMotionSensitivity(ZCLEnumSelectEntity, id_suffix="motion_sensitivity" _select_attr = "motion_sensitivity" _enum = AqaraMotionSensitivities + _attr_name = "Motion sensitivity" class AqaraMonitoringModess(types.enum8): @@ -253,6 +259,7 @@ class AqaraMonitoringMode(ZCLEnumSelectEntity, id_suffix="monitoring_mode"): _select_attr = "monitoring_mode" _enum = AqaraMonitoringModess + _attr_name = "Monitoring mode" class AqaraApproachDistances(types.enum8): @@ -269,6 +276,7 @@ class AqaraApproachDistance(ZCLEnumSelectEntity, id_suffix="approach_distance"): _select_attr = "approach_distance" _enum = AqaraApproachDistances + _attr_name = "Approach distance" class AqaraE1ReverseDirection(types.enum8): @@ -286,6 +294,7 @@ class AqaraCurtainMode(ZCLEnumSelectEntity, id_suffix="window_covering_mode"): _select_attr = "window_covering_mode" _enum = AqaraE1ReverseDirection + _attr_name = "Curtain mode" class InovelliOutputMode(types.enum1): diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 47568648f2b..3db142694fb 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -290,6 +290,7 @@ class P1MotionTriggerIndicatorSwitch( """Representation of a ZHA motion triggering configuration entity.""" _zcl_attribute: str = "trigger_indicator" + _attr_name = "LED trigger indicator" @CONFIG_DIAGNOSTIC_MATCH( @@ -300,6 +301,7 @@ class ChildLock(ZHASwitchConfigurationEntity, id_suffix="child_lock"): """ZHA BinarySensor.""" _zcl_attribute: str = "child_lock" + _attr_name = "Child lock" @CONFIG_DIAGNOSTIC_MATCH( @@ -310,6 +312,7 @@ class DisableLed(ZHASwitchConfigurationEntity, id_suffix="disable_led"): """ZHA BinarySensor.""" _zcl_attribute: str = "disable_led" + _attr_name = "Disable LED" @CONFIG_DIAGNOSTIC_MATCH( diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index e745856c342..a5e23d420d9 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -125,28 +125,28 @@ async def test_get_actions(hass, device_ias): "domain": Platform.SELECT, "type": "select_option", "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_defaulttoneselect", + "entity_id": "select.fakemanufacturer_fakemodel_default_siren_tone", "metadata": {"secondary": True}, }, { "domain": Platform.SELECT, "type": "select_option", "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_defaultsirenlevelselect", + "entity_id": "select.fakemanufacturer_fakemodel_default_siren_level", "metadata": {"secondary": True}, }, { "domain": Platform.SELECT, "type": "select_option", "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_defaultstrobelevelselect", + "entity_id": "select.fakemanufacturer_fakemodel_default_strobe_level", "metadata": {"secondary": True}, }, { "domain": Platform.SELECT, "type": "select_option", "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_defaultstrobeselect", + "entity_id": "select.fakemanufacturer_fakemodel_default_strobe", "metadata": {"secondary": True}, }, ] diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index 0bb620e98f4..6af98b35e09 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -211,7 +211,7 @@ async def test_level_control_number( Platform.NUMBER, zha_device, hass, - qualifier=attr.replace("_", ""), + qualifier=attr, ) assert entity_id is not None diff --git a/tests/components/zha/test_select.py b/tests/components/zha/test_select.py index b9c72975823..e9a7f476efb 100644 --- a/tests/components/zha/test_select.py +++ b/tests/components/zha/test_select.py @@ -163,7 +163,7 @@ async def test_select_restore_state( ): """Test zha select entity restore state.""" - entity_id = "select.fakemanufacturer_fakemodel_defaulttoneselect" + entity_id = "select.fakemanufacturer_fakemodel_default_siren_tone" core_rs(entity_id, state="Burglar") zigpy_device = zigpy_device_mock( @@ -202,12 +202,12 @@ async def test_on_off_select_new_join(hass, light, zha_device_joined): "start_up_on_off": general.OnOff.StartUpOnOff.On } zha_device = await zha_device_joined(light) - select_name = general.OnOff.StartUpOnOff.__name__ + select_name = "start_up_behavior" entity_id = await find_entity_id( Platform.SELECT, zha_device, hass, - qualifier=select_name.lower(), + qualifier=select_name, ) assert entity_id is not None @@ -285,12 +285,12 @@ async def test_on_off_select_restored(hass, light, zha_device_restored): in on_off_cluster.read_attributes.call_args_list ) - select_name = general.OnOff.StartUpOnOff.__name__ + select_name = "start_up_behavior" entity_id = await find_entity_id( Platform.SELECT, zha_device, hass, - qualifier=select_name.lower(), + qualifier=select_name, ) assert entity_id is not None diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 2d15f9335db..f79ba06f721 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -649,10 +649,10 @@ DEVICES = [ "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_iaszone", "sensor.climaxtechnology_sd8sc_00_00_03_12tc_rssi", "sensor.climaxtechnology_sd8sc_00_00_03_12tc_lqi", - "select.climaxtechnology_sd8sc_00_00_03_12tc_defaulttoneselect", - "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultsirenlevelselect", - "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultstrobelevelselect", - "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultstrobeselect", + "select.climaxtechnology_sd8sc_00_00_03_12tc_default_siren_tone", + "select.climaxtechnology_sd8sc_00_00_03_12tc_default_siren_level", + "select.climaxtechnology_sd8sc_00_00_03_12tc_default_strobe_level", + "select.climaxtechnology_sd8sc_00_00_03_12tc_default_strobe", "siren.climaxtechnology_sd8sc_00_00_03_12tc_siren", ], DEV_SIG_ENT_MAP: { @@ -679,22 +679,22 @@ DEVICES = [ ("select", "00:11:22:33:44:55:66:77-1-1282-WarningMode"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultToneSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_defaulttoneselect", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_default_siren_tone", }, ("select", "00:11:22:33:44:55:66:77-1-1282-SirenLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultSirenLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultsirenlevelselect", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_default_siren_level", }, ("select", "00:11:22:33:44:55:66:77-1-1282-StrobeLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultstrobelevelselect", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_default_strobe_level", }, ("select", "00:11:22:33:44:55:66:77-1-1282-Strobe"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultstrobeselect", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_default_strobe", }, ("siren", "00:11:22:33:44:55:66:77-1-1282"): { DEV_SIG_CHANNELS: ["ias_wd"], @@ -819,10 +819,10 @@ DEVICES = [ "binary_sensor.heiman_smokesensor_em_iaszone", "sensor.heiman_smokesensor_em_rssi", "sensor.heiman_smokesensor_em_lqi", - "select.heiman_smokesensor_em_defaulttoneselect", - "select.heiman_smokesensor_em_defaultsirenlevelselect", - "select.heiman_smokesensor_em_defaultstrobelevelselect", - "select.heiman_smokesensor_em_defaultstrobeselect", + "select.heiman_smokesensor_em_default_siren_tone", + "select.heiman_smokesensor_em_default_siren_level", + "select.heiman_smokesensor_em_default_strobe_level", + "select.heiman_smokesensor_em_default_strobe", "siren.heiman_smokesensor_em_siren", ], DEV_SIG_ENT_MAP: { @@ -854,22 +854,22 @@ DEVICES = [ ("select", "00:11:22:33:44:55:66:77-1-1282-WarningMode"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultToneSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_defaulttoneselect", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_default_siren_tone", }, ("select", "00:11:22:33:44:55:66:77-1-1282-SirenLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultSirenLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_defaultsirenlevelselect", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_default_siren_level", }, ("select", "00:11:22:33:44:55:66:77-1-1282-StrobeLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_defaultstrobelevelselect", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_default_strobe_level", }, ("select", "00:11:22:33:44:55:66:77-1-1282-Strobe"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_defaultstrobeselect", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_default_strobe", }, ("siren", "00:11:22:33:44:55:66:77-1-1282"): { DEV_SIG_CHANNELS: ["ias_wd"], @@ -942,32 +942,32 @@ DEVICES = [ "binary_sensor.heiman_warningdevice_iaszone", "sensor.heiman_warningdevice_rssi", "sensor.heiman_warningdevice_lqi", - "select.heiman_warningdevice_defaulttoneselect", - "select.heiman_warningdevice_defaultsirenlevelselect", - "select.heiman_warningdevice_defaultstrobelevelselect", - "select.heiman_warningdevice_defaultstrobeselect", + "select.heiman_warningdevice_default_siren_tone", + "select.heiman_warningdevice_default_siren_level", + "select.heiman_warningdevice_default_strobe_level", + "select.heiman_warningdevice_default_strobe", "siren.heiman_warningdevice_siren", ], DEV_SIG_ENT_MAP: { ("select", "00:11:22:33:44:55:66:77-1-1282-WarningMode"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultToneSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_defaulttoneselect", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_default_siren_tone", }, ("select", "00:11:22:33:44:55:66:77-1-1282-SirenLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultSirenLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_defaultsirenlevelselect", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_default_siren_level", }, ("select", "00:11:22:33:44:55:66:77-1-1282-StrobeLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_defaultstrobelevelselect", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_default_strobe_level", }, ("select", "00:11:22:33:44:55:66:77-1-1282-Strobe"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_defaultstrobeselect", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_default_strobe", }, ("siren", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["ias_wd"], From a2ed7f7679ab849bf891ea423a056f30b122542e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 9 Oct 2022 16:18:10 +0300 Subject: [PATCH 283/985] Remove incorrect UpCloud logger from manifest (#79855) --- homeassistant/components/upcloud/manifest.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/upcloud/manifest.json b/homeassistant/components/upcloud/manifest.json index 26e1f92ef9a..a9e0f74462e 100644 --- a/homeassistant/components/upcloud/manifest.json +++ b/homeassistant/components/upcloud/manifest.json @@ -5,6 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/upcloud", "requirements": ["upcloud-api==2.0.0"], "codeowners": ["@scop"], - "iot_class": "cloud_polling", - "loggers": ["upcloud_api"] + "iot_class": "cloud_polling" } From e5dafbc166879e829cb9fd3ff83c134d34127ad4 Mon Sep 17 00:00:00 2001 From: HarvsG <11440490+HarvsG@users.noreply.github.com> Date: Sun, 9 Oct 2022 14:23:08 +0100 Subject: [PATCH 284/985] Make _TrackTemplateResultInfo not private (#79812) --- homeassistant/helpers/event.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 107567c98ce..613b6fb3227 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -806,7 +806,7 @@ def async_track_template( track_template = threaded_listener_factory(async_track_template) -class _TrackTemplateResultInfo: +class TrackTemplateResultInfo: """Handle removal / refresh of tracker.""" def __init__( @@ -1145,7 +1145,7 @@ def async_track_template_result( raise_on_template_error: bool = False, strict: bool = False, has_super_template: bool = False, -) -> _TrackTemplateResultInfo: +) -> TrackTemplateResultInfo: """Add a listener that fires when the result of a template changes. The action will fire with the initial result from the template, and @@ -1184,9 +1184,7 @@ def async_track_template_result( Info object used to unregister the listener, and refresh the template. """ - tracker = _TrackTemplateResultInfo( - hass, track_templates, action, has_super_template - ) + tracker = TrackTemplateResultInfo(hass, track_templates, action, has_super_template) tracker.async_setup(raise_on_template_error, strict=strict) return tracker From 77864ad80f22221ca0381a324da17bc3a3e46a83 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 9 Oct 2022 16:55:49 +0200 Subject: [PATCH 285/985] Remove not used string from openexchangerates (#79937) --- homeassistant/components/openexchangerates/strings.json | 6 ------ .../components/openexchangerates/translations/en.json | 6 ------ 2 files changed, 12 deletions(-) diff --git a/homeassistant/components/openexchangerates/strings.json b/homeassistant/components/openexchangerates/strings.json index 5c4a833e0d6..d8837f468a4 100644 --- a/homeassistant/components/openexchangerates/strings.json +++ b/homeassistant/components/openexchangerates/strings.json @@ -23,11 +23,5 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" } - }, - "issues": { - "deprecated_yaml": { - "title": "The Open Exchange Rates YAML configuration has been removed", - "description": "Configuring Open Exchange Rates using YAML has been removed.\n\nRemove the Open Exchange Rates YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } } } diff --git a/homeassistant/components/openexchangerates/translations/en.json b/homeassistant/components/openexchangerates/translations/en.json index f4827c4df4d..eb41ae0ca14 100644 --- a/homeassistant/components/openexchangerates/translations/en.json +++ b/homeassistant/components/openexchangerates/translations/en.json @@ -23,11 +23,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Configuring Open Exchange Rates using YAML has been removed.\n\nRemove the Open Exchange Rates YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", - "title": "The Open Exchange Rates YAML configuration has been removed" - } } } \ No newline at end of file From b86927a4535be0a5936411a769e036a202d6dddc Mon Sep 17 00:00:00 2001 From: HarvsG <11440490+HarvsG@users.noreply.github.com> Date: Sun, 9 Oct 2022 19:30:38 +0100 Subject: [PATCH 286/985] Enable strict typing on Bayesian (#79870) * make bayesian static * no longer private --- .strict-typing | 1 + homeassistant/components/bayesian/binary_sensor.py | 3 ++- mypy.ini | 10 ++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.strict-typing b/.strict-typing index 61e9c53609a..e47bb51f173 100644 --- a/.strict-typing +++ b/.strict-typing @@ -64,6 +64,7 @@ homeassistant.components.automation.* homeassistant.components.awair.* homeassistant.components.backup.* homeassistant.components.baf.* +homeassistant.components.bayesian.* homeassistant.components.binary_sensor.* homeassistant.components.bluetooth.* homeassistant.components.bluetooth_tracker.* diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 1d2674255f9..f3a7d08ffb9 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -34,6 +34,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( TrackTemplate, TrackTemplateResult, + TrackTemplateResultInfo, async_track_state_change_event, async_track_template_result, ) @@ -189,7 +190,7 @@ class BayesianBinarySensor(BinarySensorEntity): self._probability_threshold = probability_threshold self._attr_device_class = device_class self._attr_is_on = False - self._callbacks: list = [] + self._callbacks: list[TrackTemplateResultInfo] = [] self.prior = prior self.probability = prior diff --git a/mypy.ini b/mypy.ini index 309068bf2c1..b96efc4b8c3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -392,6 +392,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.bayesian.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.binary_sensor.*] check_untyped_defs = true disallow_incomplete_defs = true From 618f259fd8c07c32087662ef30c5aff7a3be4cf0 Mon Sep 17 00:00:00 2001 From: Jeef Date: Sun, 9 Oct 2022 12:32:03 -0600 Subject: [PATCH 287/985] Add configuration URL to IPP (Printer) (#79313) switch to 0.12.0 ipp lib --- homeassistant/components/ipp/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/ipp/entity.py b/homeassistant/components/ipp/entity.py index b2f3a4a1469..50f81f74bdb 100644 --- a/homeassistant/components/ipp/entity.py +++ b/homeassistant/components/ipp/entity.py @@ -41,4 +41,5 @@ class IPPEntity(CoordinatorEntity[IPPDataUpdateCoordinator]): model=self.coordinator.data.info.model, name=self.coordinator.data.info.name, sw_version=self.coordinator.data.info.version, + configuration_url=self.coordinator.data.info.more_info, ) From b7e84543c1d5265b51dbebe4a9cc87f1e5322a6b Mon Sep 17 00:00:00 2001 From: Kevin Addeman Date: Sun, 9 Oct 2022 14:39:12 -0400 Subject: [PATCH 288/985] Add support for parent_device field so entities are nested within Keypad Devices (#79513) Co-authored-by: J. Nick Koston --- .../components/lutron_caseta/__init__.py | 72 +++++++++++++------ .../components/lutron_caseta/binary_sensor.py | 7 +- .../components/lutron_caseta/cover.py | 4 +- homeassistant/components/lutron_caseta/fan.py | 5 +- .../components/lutron_caseta/light.py | 4 +- .../components/lutron_caseta/models.py | 1 + .../components/lutron_caseta/scene.py | 13 ++-- .../components/lutron_caseta/switch.py | 4 +- .../lutron_caseta/test_device_trigger.py | 21 +++--- 9 files changed, 78 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 2041f4d65d6..321c25b6944 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -177,15 +177,15 @@ async def async_setup_entry( buttons = bridge.buttons _async_register_bridge_device(hass, entry_id, bridge_device) - button_devices = _async_register_button_devices( - hass, entry_id, bridge_device, buttons + button_devices, device_info_by_device_id = _async_register_button_devices( + hass, entry_id, bridge, bridge_device, buttons ) _async_subscribe_pico_remote_events(hass, bridge, buttons) # Store this bridge (keyed by entry_id) so it can be retrieved by the # platforms we're setting up. hass.data[DOMAIN][entry_id] = LutronCasetaData( - bridge, bridge_device, button_devices + bridge, bridge_device, button_devices, device_info_by_device_id ) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -213,34 +213,46 @@ def _async_register_bridge_device( def _async_register_button_devices( hass: HomeAssistant, config_entry_id: str, + bridge, bridge_device, button_devices_by_id: dict[int, dict], -) -> dict[str, dict]: +) -> tuple[dict[str, dict], dict[int, dict[str, Any]]]: """Register button devices (Pico Remotes) in the device registry.""" device_registry = dr.async_get(hass) button_devices_by_dr_id: dict[str, dict] = {} - seen = set() + device_info_by_device_id: dict[int, dict[str, Any]] = {} + seen: set[str] = set() + bridge_devices = bridge.get_devices() for device in button_devices_by_id.values(): - if "serial" not in device or device["serial"] in seen: + + ha_device = device + if "parent_device" in device and device["parent_device"] is not None: + # Device is a child of parent_device + # use the parent_device for HA device info + ha_device = bridge_devices[device["parent_device"]] + + if "serial" not in ha_device or ha_device["serial"] in seen: continue - seen.add(device["serial"]) - area, name = _area_and_name_from_name(device["name"]) + seen.add(ha_device["serial"]) + + area, name = _area_and_name_from_name(ha_device["name"]) device_args: dict[str, Any] = { "name": f"{area} {name}", "manufacturer": MANUFACTURER, "config_entry_id": config_entry_id, - "identifiers": {(DOMAIN, device["serial"])}, - "model": f"{device['model']} ({device['type']})", + "identifiers": {(DOMAIN, ha_device["serial"])}, + "model": f"{ha_device['model']} ({ha_device['type']})", "via_device": (DOMAIN, bridge_device["serial"]), } if area != UNASSIGNED_AREA: device_args["suggested_area"] = area dr_device = device_registry.async_get_or_create(**device_args) - button_devices_by_dr_id[dr_device.id] = device + button_devices_by_dr_id[dr_device.id] = ha_device + device_info_by_device_id.setdefault(ha_device["device_id"], device_args) - return button_devices_by_dr_id + return button_devices_by_dr_id, device_info_by_device_id def _area_and_name_from_name(device_name: str) -> tuple[str, str]: @@ -282,16 +294,23 @@ def _async_subscribe_pico_remote_events( else: action = ACTION_RELEASE - type_ = _lutron_model_to_device_type(device["model"], device["type"]) - area, name = _area_and_name_from_name(device["name"]) + bridge_devices = bridge_device.get_devices() + ha_device = device + if "parent_device" in device and device["parent_device"] is not None: + # Device is a child of parent_device + # use the parent_device for HA device info + ha_device = bridge_devices[device["parent_device"]] + + type_ = _lutron_model_to_device_type(ha_device["model"], ha_device["type"]) + area, name = _area_and_name_from_name(ha_device["name"]) leap_button_number = device["button_number"] lip_button_number = async_get_lip_button(type_, leap_button_number) - hass_device = dev_reg.async_get_device({(DOMAIN, device["serial"])}) + hass_device = dev_reg.async_get_device({(DOMAIN, ha_device["serial"])}) hass.bus.async_fire( LUTRON_CASETA_BUTTON_EVENT, { - ATTR_SERIAL: device["serial"], + ATTR_SERIAL: ha_device["serial"], ATTR_TYPE: type_, ATTR_BUTTON_NUMBER: lip_button_number, ATTR_LEAP_BUTTON_NUMBER: leap_button_number, @@ -327,7 +346,7 @@ class LutronCasetaDevice(Entity): _attr_should_poll = False - def __init__(self, device, bridge, bridge_device): + def __init__(self, device, data): """Set up the base class. [:param]device the device metadata @@ -335,11 +354,24 @@ class LutronCasetaDevice(Entity): [:param]bridge_device a dict with the details of the bridge """ self._device = device - self._smartbridge = bridge - self._bridge_device = bridge_device - self._bridge_unique_id = serial_to_unique_id(bridge_device["serial"]) + self._smartbridge = data.bridge + self._bridge_device = data.bridge_device + self._bridge_unique_id = serial_to_unique_id(data.bridge_device["serial"]) if "serial" not in self._device: return + + if "parent_device" in device and ( + parent_device_info := data.device_info_by_device_id.get( + device["parent_device"] + ) + ): + # Append the child device name to the end of the parent keypad name to create the entity name + self._attr_name = f'{parent_device_info["name"]} {device["device_name"]}' + # Set the device_info to the same as the Parent Keypad + # The entities will be nested inside the keypad device + self._attr_device_info = parent_device_info + return + area, name = _area_and_name_from_name(device["name"]) self._attr_name = full_name = f"{area} {name}" info = DeviceInfo( diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index 20fc221cdef..6df1125f7e9 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -28,10 +28,9 @@ async def async_setup_entry( """ data: LutronCasetaData = hass.data[CASETA_DOMAIN][config_entry.entry_id] bridge = data.bridge - bridge_device = data.bridge_device occupancy_groups = bridge.occupancy_groups async_add_entities( - LutronOccupancySensor(occupancy_group, bridge, bridge_device) + LutronOccupancySensor(occupancy_group, data) for occupancy_group in occupancy_groups.values() ) @@ -41,9 +40,9 @@ class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.OCCUPANCY - def __init__(self, device, bridge, bridge_device): + def __init__(self, device, data): """Init an occupancy sensor.""" - super().__init__(device, bridge, bridge_device) + super().__init__(device, data) _, name = _area_and_name_from_name(device["name"]) self._attr_name = name self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py index d63c1191d57..cca04e0a298 100644 --- a/homeassistant/components/lutron_caseta/cover.py +++ b/homeassistant/components/lutron_caseta/cover.py @@ -30,11 +30,9 @@ async def async_setup_entry( """ data: LutronCasetaData = hass.data[CASETA_DOMAIN][config_entry.entry_id] bridge = data.bridge - bridge_device = data.bridge_device cover_devices = bridge.get_devices_by_domain(DOMAIN) async_add_entities( - LutronCasetaCover(cover_device, bridge, bridge_device) - for cover_device in cover_devices + LutronCasetaCover(cover_device, data) for cover_device in cover_devices ) diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py index bf2328565d4..ba69f17d880 100644 --- a/homeassistant/components/lutron_caseta/fan.py +++ b/homeassistant/components/lutron_caseta/fan.py @@ -34,11 +34,8 @@ async def async_setup_entry( """ data: LutronCasetaData = hass.data[CASETA_DOMAIN][config_entry.entry_id] bridge = data.bridge - bridge_device = data.bridge_device fan_devices = bridge.get_devices_by_domain(DOMAIN) - async_add_entities( - LutronCasetaFan(fan_device, bridge, bridge_device) for fan_device in fan_devices - ) + async_add_entities(LutronCasetaFan(fan_device, data) for fan_device in fan_devices) class LutronCasetaFan(LutronCasetaDeviceUpdatableEntity, FanEntity): diff --git a/homeassistant/components/lutron_caseta/light.py b/homeassistant/components/lutron_caseta/light.py index cfad8115a20..ffab0689636 100644 --- a/homeassistant/components/lutron_caseta/light.py +++ b/homeassistant/components/lutron_caseta/light.py @@ -41,11 +41,9 @@ async def async_setup_entry( """ data: LutronCasetaData = hass.data[CASETA_DOMAIN][config_entry.entry_id] bridge = data.bridge - bridge_device = data.bridge_device light_devices = bridge.get_devices_by_domain(DOMAIN) async_add_entities( - LutronCasetaLight(light_device, bridge, bridge_device) - for light_device in light_devices + LutronCasetaLight(light_device, data) for light_device in light_devices ) diff --git a/homeassistant/components/lutron_caseta/models.py b/homeassistant/components/lutron_caseta/models.py index 362760b0caf..d0e59c25438 100644 --- a/homeassistant/components/lutron_caseta/models.py +++ b/homeassistant/components/lutron_caseta/models.py @@ -14,3 +14,4 @@ class LutronCasetaData: bridge: Smartbridge bridge_device: dict[str, Any] button_devices: dict[str, dict] + device_info_by_device_id: dict[int, dict[str, Any]] diff --git a/homeassistant/components/lutron_caseta/scene.py b/homeassistant/components/lutron_caseta/scene.py index 2870d6ee96a..cc3be8a6479 100644 --- a/homeassistant/components/lutron_caseta/scene.py +++ b/homeassistant/components/lutron_caseta/scene.py @@ -27,23 +27,20 @@ async def async_setup_entry( """ data: LutronCasetaData = hass.data[CASETA_DOMAIN][config_entry.entry_id] bridge = data.bridge - bridge_device = data.bridge_device scenes = bridge.get_scenes() - async_add_entities( - LutronCasetaScene(scenes[scene], bridge, bridge_device) for scene in scenes - ) + async_add_entities(LutronCasetaScene(scenes[scene], data) for scene in scenes) class LutronCasetaScene(Scene): """Representation of a Lutron Caseta scene.""" - def __init__(self, scene, bridge, bridge_device): + def __init__(self, scene, data): """Initialize the Lutron Caseta scene.""" self._scene_id = scene["scene_id"] - self._bridge: Smartbridge = bridge - bridge_unique_id = serial_to_unique_id(bridge_device["serial"]) + self._bridge: Smartbridge = data.bridge + bridge_unique_id = serial_to_unique_id(data.bridge_device["serial"]) self._attr_device_info = DeviceInfo( - identifiers={(CASETA_DOMAIN, bridge_device["serial"])}, + identifiers={(CASETA_DOMAIN, data.bridge_device["serial"])}, ) self._attr_name = _area_and_name_from_name(scene["name"])[1] self._attr_unique_id = f"scene_{bridge_unique_id}_{self._scene_id}" diff --git a/homeassistant/components/lutron_caseta/switch.py b/homeassistant/components/lutron_caseta/switch.py index 92ec6b35f98..d87fd4c3bfa 100644 --- a/homeassistant/components/lutron_caseta/switch.py +++ b/homeassistant/components/lutron_caseta/switch.py @@ -24,11 +24,9 @@ async def async_setup_entry( """ data: LutronCasetaData = hass.data[CASETA_DOMAIN][config_entry.entry_id] bridge = data.bridge - bridge_device = data.bridge_device switch_devices = bridge.get_devices_by_domain(DOMAIN) async_add_entities( - LutronCasetaLight(switch_device, bridge, bridge_device) - for switch_device in switch_devices + LutronCasetaLight(switch_device, data) for switch_device in switch_devices ) diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index 161f5cf357f..46a26f129c7 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -34,6 +34,7 @@ from tests.common import ( MOCK_BUTTON_DEVICES = [ { + "device_id": "710", "Name": "Back Hall Pico", "ID": 2, "Area": {"Name": "Back Hall"}, @@ -50,6 +51,7 @@ MOCK_BUTTON_DEVICES = [ "serial": 43845548, }, { + "device_id": "742", "Name": "Front Steps Sunnata Keypad", "ID": 3, "Area": {"Name": "Front Steps"}, @@ -87,19 +89,22 @@ async def _async_setup_lutron_with_picos(hass, device_reg): config_entry = MockConfigEntry(domain=DOMAIN, data={}) config_entry.add_to_hass(hass) dr_button_devices = {} + device_info_by_device_id = {} for device in MOCK_BUTTON_DEVICES: - dr_device = device_reg.async_get_or_create( - name=device["leap_name"], - manufacturer=MANUFACTURER, - config_entry_id=config_entry.entry_id, - identifiers={(DOMAIN, device["serial"])}, - model=f"{device['model']} ({device[CONF_TYPE]})", - ) + device_args = { + "name": device["leap_name"], + "manufacturer": MANUFACTURER, + "config_entry_id": config_entry.entry_id, + "identifiers": {(DOMAIN, device["serial"])}, + "model": f"{device['model']} ({device[CONF_TYPE]})", + } + dr_device = device_reg.async_get_or_create(**device_args) dr_button_devices[dr_device.id] = device + device_info_by_device_id.setdefault(device["device_id"], device_args) hass.data[DOMAIN][config_entry.entry_id] = LutronCasetaData( - MagicMock(), MagicMock(), dr_button_devices + MagicMock(), MagicMock(), dr_button_devices, device_info_by_device_id ) return config_entry.entry_id From 5a0609ae8b39cf0d5a516b139aaf0565f98ac743 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 9 Oct 2022 21:28:35 +0200 Subject: [PATCH 289/985] Add sensor platform to LaMetric (#79935) --- homeassistant/components/lametric/const.py | 2 +- homeassistant/components/lametric/sensor.py | 87 +++++++++++++++++++++ tests/components/lametric/test_sensor.py | 54 +++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/lametric/sensor.py create mode 100644 tests/components/lametric/test_sensor.py diff --git a/homeassistant/components/lametric/const.py b/homeassistant/components/lametric/const.py index 1ba48e0d992..6a3df3b54f1 100644 --- a/homeassistant/components/lametric/const.py +++ b/homeassistant/components/lametric/const.py @@ -7,7 +7,7 @@ from typing import Final from homeassistant.const import Platform DOMAIN: Final = "lametric" -PLATFORMS = [Platform.BUTTON, Platform.NUMBER, Platform.SWITCH] +PLATFORMS = [Platform.BUTTON, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH] LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/lametric/sensor.py b/homeassistant/components/lametric/sensor.py new file mode 100644 index 00000000000..b9ff430ab2b --- /dev/null +++ b/homeassistant/components/lametric/sensor.py @@ -0,0 +1,87 @@ +"""Support for LaMetric sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from demetriek import Device + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import LaMetricDataUpdateCoordinator +from .entity import LaMetricEntity + + +@dataclass +class LaMetricEntityDescriptionMixin: + """Mixin values for LaMetric entities.""" + + value_fn: Callable[[Device], int | None] + + +@dataclass +class LaMetricSensorEntityDescription( + SensorEntityDescription, LaMetricEntityDescriptionMixin +): + """Class describing LaMetric sensor entities.""" + + +SENSORS = [ + LaMetricSensorEntityDescription( + key="rssi", + name="Wi-Fi signal", + icon="mdi:wifi", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda device: device.wifi.rssi, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up LaMetric sensor based on a config entry.""" + coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + LaMetricSensorEntity( + coordinator=coordinator, + description=description, + ) + for description in SENSORS + ) + + +class LaMetricSensorEntity(LaMetricEntity, SensorEntity): + """Representation of a LaMetric sensor.""" + + entity_description: LaMetricSensorEntityDescription + + def __init__( + self, + coordinator: LaMetricDataUpdateCoordinator, + description: LaMetricSensorEntityDescription, + ) -> None: + """Initiate LaMetric sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.data.serial_number}-{description.key}" + + @property + def native_value(self) -> int | None: + """Return the sensor value.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/tests/components/lametric/test_sensor.py b/tests/components/lametric/test_sensor.py new file mode 100644 index 00000000000..76f584b1cde --- /dev/null +++ b/tests/components/lametric/test_sensor.py @@ -0,0 +1,54 @@ +"""Tests for the LaMetric sensor platform.""" +from unittest.mock import AsyncMock, MagicMock + +from homeassistant.components.lametric.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory + +from tests.common import MockConfigEntry + + +async def test_wifi_signal( + hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, + init_integration: MockConfigEntry, + mock_lametric: MagicMock, +) -> None: + """Test the LaMetric Wi-Fi sensor.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + state = hass.states.get("sensor.frenck_s_lametric_wi_fi_signal") + assert state + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's LaMetric Wi-Fi signal" + assert state.attributes.get(ATTR_ICON) == "mdi:wifi" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + assert state.state == "21" + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry.device_id + assert entry.entity_category is EntityCategory.DIAGNOSTIC + assert entry.unique_id == "SA110405124500W00BS9-rssi" + + device = device_registry.async_get(entry.device_id) + assert device + assert device.configuration_url is None + assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")} + assert device.entry_type is None + assert device.hw_version is None + assert device.identifiers == {(DOMAIN, "SA110405124500W00BS9")} + assert device.manufacturer == "LaMetric Inc." + assert device.name == "Frenck's LaMetric" + assert device.sw_version == "2.2.2" From 45a30546ecf7ad5a9d8e762896679348e3b00588 Mon Sep 17 00:00:00 2001 From: Kevin Addeman Date: Sun, 9 Oct 2022 18:17:06 -0400 Subject: [PATCH 290/985] Add support for Homeowner and Phantom Keypads (#79958) --- .../components/lutron_caseta/__init__.py | 22 ++++++++++--- .../lutron_caseta/device_trigger.py | 33 +++++++++++++++++-- .../lutron_caseta/test_device_trigger.py | 31 +++++++++++++++++ 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 321c25b6944..9638f769919 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -232,16 +232,20 @@ def _async_register_button_devices( # use the parent_device for HA device info ha_device = bridge_devices[device["parent_device"]] - if "serial" not in ha_device or ha_device["serial"] in seen: + ha_device_serial = _handle_none_keypad_serial( + ha_device, bridge_device["serial"] + ) + + if "serial" not in ha_device or ha_device_serial in seen: continue - seen.add(ha_device["serial"]) + seen.add(ha_device_serial) area, name = _area_and_name_from_name(ha_device["name"]) device_args: dict[str, Any] = { "name": f"{area} {name}", "manufacturer": MANUFACTURER, "config_entry_id": config_entry_id, - "identifiers": {(DOMAIN, ha_device["serial"])}, + "identifiers": {(DOMAIN, ha_device_serial)}, "model": f"{ha_device['model']} ({ha_device['type']})", "via_device": (DOMAIN, bridge_device["serial"]), } @@ -255,6 +259,10 @@ def _async_register_button_devices( return button_devices_by_dr_id, device_info_by_device_id +def _handle_none_keypad_serial(keypad_device: dict, bridge_serial: int) -> str: + return keypad_device["serial"] or f"{bridge_serial}_{keypad_device['device_id']}" + + def _area_and_name_from_name(device_name: str) -> tuple[str, str]: """Return the area and name from the devices internal name.""" if "_" in device_name: @@ -301,16 +309,20 @@ def _async_subscribe_pico_remote_events( # use the parent_device for HA device info ha_device = bridge_devices[device["parent_device"]] + ha_device_serial = _handle_none_keypad_serial( + ha_device, bridge_devices[BRIDGE_DEVICE_ID]["serial"] + ) + type_ = _lutron_model_to_device_type(ha_device["model"], ha_device["type"]) area, name = _area_and_name_from_name(ha_device["name"]) leap_button_number = device["button_number"] lip_button_number = async_get_lip_button(type_, leap_button_number) - hass_device = dev_reg.async_get_device({(DOMAIN, ha_device["serial"])}) + hass_device = dev_reg.async_get_device({(DOMAIN, ha_device_serial)}) hass.bus.async_fire( LUTRON_CASETA_BUTTON_EVENT, { - ATTR_SERIAL: ha_device["serial"], + ATTR_SERIAL: ha_device_serial, ATTR_TYPE: type_, ATTR_BUTTON_NUMBER: lip_button_number, ATTR_LEAP_BUTTON_NUMBER: leap_button_number, diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index b9fe89edf7f..30e4e772c99 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -315,6 +315,27 @@ SUNNATA_KEYPAD_4_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( } ) +HOMEOWNER_KEYPAD_BUTTON_TYPES_TO_LEAP = { + "button_1": 1, + "button_2": 2, + "button_3": 3, + "button_4": 4, + "button_5": 5, + "button_6": 6, + "button_7": 7, +} +HOMEOWNER_KEYPAD_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In(HOMEOWNER_KEYPAD_BUTTON_TYPES_TO_LEAP), + } +) + +PHANTOM_KEYPAD_BUTTON_TYPES_TO_LEAP: dict[str, int] = {} +PHANTOM_KEYPAD_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In(PHANTOM_KEYPAD_BUTTON_TYPES_TO_LEAP), + } +) DEVICE_TYPE_SCHEMA_MAP = { "Pico2Button": PICO_2_BUTTON_TRIGGER_SCHEMA, @@ -329,6 +350,8 @@ DEVICE_TYPE_SCHEMA_MAP = { "SunnataKeypad_2Button": SUNNATA_KEYPAD_2_BUTTON_TRIGGER_SCHEMA, "SunnataKeypad_3ButtonRaiseLower": SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA, "SunnataKeypad_4Button": SUNNATA_KEYPAD_4_BUTTON_TRIGGER_SCHEMA, + "HomeownerKeypad": HOMEOWNER_KEYPAD_BUTTON_TRIGGER_SCHEMA, + "PhantomKeypad": PHANTOM_KEYPAD_BUTTON_TRIGGER_SCHEMA, } DEVICE_TYPE_SUBTYPE_MAP_TO_LIP = { @@ -356,6 +379,8 @@ DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP = { "SunnataKeypad_2Button": SUNNATA_KEYPAD_2_BUTTON_BUTTON_TYPES_TO_LEAP, "SunnataKeypad_3ButtonRaiseLower": SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP, "SunnataKeypad_4Button": SUNNATA_KEYPAD_4_BUTTON_BUTTON_TYPES_TO_LEAP, + "HomeownerKeypad": HOMEOWNER_KEYPAD_BUTTON_TYPES_TO_LEAP, + "PhantomKeypad": PHANTOM_KEYPAD_BUTTON_TYPES_TO_LEAP, } LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP = { @@ -373,6 +398,8 @@ TRIGGER_SCHEMA = vol.Any( SUNNATA_KEYPAD_2_BUTTON_TRIGGER_SCHEMA, SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA, SUNNATA_KEYPAD_4_BUTTON_TRIGGER_SCHEMA, + HOMEOWNER_KEYPAD_BUTTON_TRIGGER_SCHEMA, + PHANTOM_KEYPAD_BUTTON_TRIGGER_SCHEMA, ) @@ -429,9 +456,9 @@ async def async_get_triggers( def _device_model_to_type(device_registry_model: str) -> str: """Convert a lutron_caseta device registry entry model to type.""" - model, p_device_type = device_registry_model.split(" ") - device_type = p_device_type.replace("(", "").replace(")", "") - return _lutron_model_to_device_type(model, device_type) + model_list = device_registry_model.split(" ") + device_type = model_list.pop().replace("(", "").replace(")", "") + return _lutron_model_to_device_type(" ".join(model_list), device_type) def _lutron_model_to_device_type(model: str, device_type: str) -> str: diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index 46a26f129c7..991fa191f69 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -67,6 +67,25 @@ MOCK_BUTTON_DEVICES = [ "model": "RRST-W4B-XX", "serial": 43845547, }, + { + "device_id": "786", + "Name": "Example Homeowner Keypad", + "ID": 3, + "Area": {"Name": "Front Steps"}, + "Buttons": [ + {"Number": 12}, + {"Number": 13}, + {"Number": 14}, + {"Number": 15}, + {"Number": 16}, + {"Number": 17}, + {"Number": 18}, + ], + "leap_name": "Front Steps_Example Homeowner Keypad", + "type": "HomeownerKeypad", + "model": "Homeowner Keypad", + "serial": None, + }, ] @@ -178,6 +197,18 @@ async def test_get_triggers_for_non_button_device(hass, device_reg): assert triggers == [] +async def test_none_serial_keypad(hass, device_reg): + """Test serial assignment for keypads without serials.""" + config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg) + + keypad_device = device_reg.async_get_or_create( + config_entry_id=config_entry_id, + identifiers={(DOMAIN, "1234_786")}, + ) + + assert keypad_device is not None + + async def test_if_fires_on_button_event(hass, calls, device_reg): """Test for press trigger firing.""" await _async_setup_lutron_with_picos(hass, device_reg) From 41595b0cbad2d2cde7b5999a02502ebf97d56400 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Mon, 10 Oct 2022 09:19:50 +1100 Subject: [PATCH 291/985] Migrate the LIFX integration to use kelvin for color temp (#79775) --- homeassistant/components/lifx/light.py | 19 ++++++------------- homeassistant/components/lifx/manager.py | 10 ++++------ homeassistant/components/lifx/util.py | 11 +++-------- tests/components/lifx/test_light.py | 13 +++++++------ 4 files changed, 20 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 50e4593077a..b8128df100e 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta -import math from typing import Any import aiolifx_effects as aiolifx_effects_module @@ -26,7 +25,6 @@ from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time -import homeassistant.util.color as color_util from .const import ( _LOGGER, @@ -130,16 +128,13 @@ class LIFXLight(LIFXEntity, LightEntity): self.entry = entry self._attr_unique_id = self.coordinator.serial_number self._attr_name = self.bulb.label - self._attr_min_mireds = math.floor( - color_util.color_temperature_kelvin_to_mired(bulb_features["max_kelvin"]) - ) - self._attr_max_mireds = math.ceil( - color_util.color_temperature_kelvin_to_mired(bulb_features["min_kelvin"]) - ) + self._attr_min_color_temp_kelvin = bulb_features["min_kelvin"] + self._attr_max_color_temp_kelvin = bulb_features["max_kelvin"] if bulb_features["min_kelvin"] != bulb_features["max_kelvin"]: color_mode = ColorMode.COLOR_TEMP else: color_mode = ColorMode.BRIGHTNESS + self._attr_color_mode = color_mode self._attr_supported_color_modes = {color_mode} self._attr_effect = None @@ -151,11 +146,9 @@ class LIFXLight(LIFXEntity, LightEntity): return convert_16_to_8(int(fade * self.bulb.color[HSBK_BRIGHTNESS])) @property - def color_temp(self) -> int | None: - """Return the color temperature.""" - return color_util.color_temperature_kelvin_to_mired( - self.bulb.color[HSBK_KELVIN] - ) + def color_temp_kelvin(self) -> int | None: + """Return the color temperature of this light in kelvin.""" + return int(self.bulb.color[HSBK_KELVIN]) @property def is_on(self) -> bool: diff --git a/homeassistant/components/lifx/manager.py b/homeassistant/components/lifx/manager.py index c199ee8a9a1..2b4536656d8 100644 --- a/homeassistant/components/lifx/manager.py +++ b/homeassistant/components/lifx/manager.py @@ -14,15 +14,14 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, ATTR_COLOR_TEMP, + ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, - ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_XY_COLOR, COLOR_GROUP, VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT, - preprocess_turn_on_alternatives, ) from homeassistant.const import ATTR_MODE from homeassistant.core import HomeAssistant, ServiceCall, callback @@ -98,10 +97,10 @@ LIFX_EFFECT_PULSE_SCHEMA = cv.make_entity_service_schema( ) ), ), - vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All( - vol.Coerce(int), vol.Range(min=1) + vol.Exclusive(ATTR_COLOR_TEMP_KELVIN, COLOR_GROUP): vol.All( + vol.Coerce(int), vol.Range(min=1500, max=9000) ), - vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): cv.positive_int, + vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): cv.positive_int, ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)), ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)), ATTR_MODE: vol.In(PULSE_MODES), @@ -250,7 +249,6 @@ class LIFXManager: await self.effects_conductor.start(effect, bulbs) elif service == SERVICE_EFFECT_COLORLOOP: - preprocess_turn_on_alternatives(self.hass, kwargs) brightness = None if ATTR_BRIGHTNESS in kwargs: diff --git a/homeassistant/components/lifx/util.py b/homeassistant/components/lifx/util.py index 2136ab5f63b..4e811e6c366 100644 --- a/homeassistant/components/lifx/util.py +++ b/homeassistant/components/lifx/util.py @@ -14,11 +14,10 @@ from awesomeversion import AwesomeVersion from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, + ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, ATTR_RGB_COLOR, ATTR_XY_COLOR, - preprocess_turn_on_alternatives, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -81,8 +80,6 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] | """ hue, saturation, brightness, kelvin = [None] * 4 - preprocess_turn_on_alternatives(hass, kwargs) - if ATTR_HS_COLOR in kwargs: hue, saturation = kwargs[ATTR_HS_COLOR] elif ATTR_RGB_COLOR in kwargs: @@ -96,10 +93,8 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] | saturation = int(saturation / 100 * 65535) kelvin = 3500 - if ATTR_COLOR_TEMP in kwargs: - kelvin = int( - color_util.color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) - ) + if ATTR_COLOR_TEMP_KELVIN in kwargs: + kelvin = kwargs.pop(ATTR_COLOR_TEMP_KELVIN) saturation = 0 if ATTR_BRIGHTNESS in kwargs: diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index c2f846b0a76..1c424f354e3 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -20,6 +20,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, ATTR_COLOR_TEMP, + ATTR_COLOR_TEMP_KELVIN, ATTR_EFFECT, ATTR_HS_COLOR, ATTR_RGB_COLOR, @@ -784,9 +785,9 @@ async def test_color_light_with_temp( ColorMode.COLOR_TEMP, ColorMode.HS, ] - assert attributes[ATTR_HS_COLOR] == (31.007, 6.862) - assert attributes[ATTR_RGB_COLOR] == (255, 246, 237) - assert attributes[ATTR_XY_COLOR] == (0.339, 0.338) + assert attributes[ATTR_HS_COLOR] == (30.754, 7.122) + assert attributes[ATTR_RGB_COLOR] == (255, 246, 236) + assert attributes[ATTR_XY_COLOR] == (0.34, 0.339) bulb.color = [65535, 65535, 65535, 65535] await hass.services.async_call( @@ -911,7 +912,7 @@ async def test_white_bulb(hass: HomeAssistant) -> None: assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ ColorMode.COLOR_TEMP, ] - assert attributes[ATTR_COLOR_TEMP] == 166 + assert attributes[ATTR_COLOR_TEMP_KELVIN] == 6000 await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) @@ -1012,10 +1013,10 @@ async def test_white_light_fails(hass): await hass.services.async_call( LIGHT_DOMAIN, "turn_on", - {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 153}, + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 6000}, blocking=True, ) - assert bulb.set_color.calls[0][0][0] == [1, 0, 3, 6535] + assert bulb.set_color.calls[0][0][0] == [1, 0, 3, 6000] bulb.set_color.reset_mock() From 7a1939c6082da04362ddebbe0535f50d027a91c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Oct 2022 14:07:22 -1000 Subject: [PATCH 292/985] Bump dbus-fast to 1.38.0 (#79962) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index acdfce5acfd..e7cd7803878 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.33.0" + "dbus-fast==1.38.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a1133f64c0d..0d8498399c6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.33.0 +dbus-fast==1.38.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index cb52c6c52cd..8e1edf95f75 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -543,7 +543,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.33.0 +dbus-fast==1.38.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a219b7bbd5..aa8d8082dbd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.33.0 +dbus-fast==1.38.0 # homeassistant.components.debugpy debugpy==1.6.3 From d53499c0bf8b8529804ac8817521f9a44bf01e78 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 9 Oct 2022 19:10:12 -0500 Subject: [PATCH 293/985] Bump jellyfin-apiclient-python to 1.9.2 (#79945) --- homeassistant/components/jellyfin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/jellyfin/manifest.json b/homeassistant/components/jellyfin/manifest.json index e2189bed2cb..674aab64e0b 100644 --- a/homeassistant/components/jellyfin/manifest.json +++ b/homeassistant/components/jellyfin/manifest.json @@ -3,7 +3,7 @@ "name": "Jellyfin", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/jellyfin", - "requirements": ["jellyfin-apiclient-python==1.8.1"], + "requirements": ["jellyfin-apiclient-python==1.9.2"], "iot_class": "local_polling", "codeowners": ["@j-stienstra", "@ctalkington"], "loggers": ["jellyfin_apiclient_python"] diff --git a/requirements_all.txt b/requirements_all.txt index 8e1edf95f75..8f8160c199a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -949,7 +949,7 @@ iperf3==0.1.11 ismartgate==4.0.4 # homeassistant.components.jellyfin -jellyfin-apiclient-python==1.8.1 +jellyfin-apiclient-python==1.9.2 # homeassistant.components.rest jsonpath==0.82 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa8d8082dbd..233245acbbd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -702,7 +702,7 @@ iotawattpy==0.1.0 ismartgate==4.0.4 # homeassistant.components.jellyfin -jellyfin-apiclient-python==1.8.1 +jellyfin-apiclient-python==1.9.2 # homeassistant.components.rest jsonpath==0.82 From aca340de1c7a7367cdf5f435338e6fad93e8154e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 10 Oct 2022 00:34:37 +0000 Subject: [PATCH 294/985] [ci skip] Translation update --- .../airthings_ble/translations/zh-Hans.json | 22 +++++++++++++++++ .../apcupsd/translations/zh-Hans.json | 18 ++++++++++++++ .../braviatv/translations/zh-Hans.json | 11 +++++++++ .../components/climate/translations/da.json | 8 +++---- .../components/generic/translations/ca.json | 7 ++++++ .../generic/translations/zh-Hans.json | 11 +++++++++ .../generic/translations/zh-Hant.json | 7 ++++++ .../huawei_lte/translations/ca.json | 11 ++++++++- .../huawei_lte/translations/zh-Hans.json | 11 ++++++++- .../huawei_lte/translations/zh-Hant.json | 11 ++++++++- .../mikrotik/translations/zh-Hans.json | 10 +++++++- .../nibe_heatpump/translations/zh-Hans.json | 14 +++++++++++ .../octoprint/translations/zh-Hans.json | 14 +++++++++++ .../openexchangerates/translations/de.json | 4 ++-- .../openexchangerates/translations/en.json | 6 +++++ .../openexchangerates/translations/es.json | 4 ++-- .../openexchangerates/translations/hu.json | 2 +- .../openexchangerates/translations/id.json | 4 ++-- .../openexchangerates/translations/ru.json | 4 ++-- .../components/overkiz/translations/ca.json | 3 ++- .../components/overkiz/translations/de.json | 3 ++- .../components/overkiz/translations/es.json | 3 ++- .../components/overkiz/translations/et.json | 3 ++- .../components/overkiz/translations/hu.json | 3 ++- .../components/overkiz/translations/id.json | 3 ++- .../components/overkiz/translations/pl.json | 3 ++- .../components/overkiz/translations/ru.json | 3 ++- .../overkiz/translations/zh-Hans.json | 7 ++++++ .../overkiz/translations/zh-Hant.json | 3 ++- .../plugwise/translations/select.ca.json | 11 +++++++++ .../plugwise/translations/select.zh-Hans.json | 11 +++++++++ .../plugwise/translations/select.zh-Hant.json | 11 +++++++++ .../radarr/translations/zh-Hans.json | 24 +++++++++++++++++++ .../uptimerobot/translations/zh-Hans.json | 3 ++- .../components/zha/translations/ca.json | 2 ++ .../components/zha/translations/zh-Hans.json | 15 ++++++++++++ .../components/zha/translations/zh-Hant.json | 16 +++++++++++++ 37 files changed, 279 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/airthings_ble/translations/zh-Hans.json create mode 100644 homeassistant/components/apcupsd/translations/zh-Hans.json create mode 100644 homeassistant/components/nibe_heatpump/translations/zh-Hans.json create mode 100644 homeassistant/components/octoprint/translations/zh-Hans.json create mode 100644 homeassistant/components/overkiz/translations/zh-Hans.json create mode 100644 homeassistant/components/plugwise/translations/select.ca.json create mode 100644 homeassistant/components/plugwise/translations/select.zh-Hans.json create mode 100644 homeassistant/components/plugwise/translations/select.zh-Hant.json create mode 100644 homeassistant/components/radarr/translations/zh-Hans.json diff --git a/homeassistant/components/airthings_ble/translations/zh-Hans.json b/homeassistant/components/airthings_ble/translations/zh-Hans.json new file mode 100644 index 00000000000..165d98f5bbd --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/zh-Hans.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e", + "already_in_progress": "\u914d\u7f6e\u6d41\u7a0b\u5df2\u5728\u8fdb\u884c\u4e2d", + "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5", + "no_devices_found": "\u5728\u6b64\u7f51\u7edc\u4e0a\u672a\u627e\u5230\u8bbe\u5907", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "step": { + "bluetooth_confirm": { + "description": "\u60a8\u60f3\u8bbe\u7f6e\u7684\u8bbe\u5907\u662f\u5426\u662f\uff1a {name}?" + }, + "user": { + "data": { + "address": "\u8bbe\u5907" + }, + "description": "\u9009\u62e9\u4e00\u4e2a\u8bbe\u5907\u4ee5\u914d\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/zh-Hans.json b/homeassistant/components/apcupsd/translations/zh-Hans.json new file mode 100644 index 00000000000..60a57c849ef --- /dev/null +++ b/homeassistant/components/apcupsd/translations/zh-Hans.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e" + }, + "error": { + "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u673a", + "port": "\u7aef\u53e3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/zh-Hans.json b/homeassistant/components/braviatv/translations/zh-Hans.json index 447c136dcf4..6f115e243ac 100644 --- a/homeassistant/components/braviatv/translations/zh-Hans.json +++ b/homeassistant/components/braviatv/translations/zh-Hans.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "reauth_successful": "\u91cd\u65b0\u8ba4\u8bc1\u6210\u529f", + "reauth_unsuccessful": "\u91cd\u65b0\u9a8c\u8bc1\u5931\u8d25\uff0c\u8bf7\u79fb\u9664\u96c6\u6210\u5e76\u91cd\u65b0\u8bbe\u7f6e\u3002" + }, "step": { "authorize": { "data": { @@ -8,6 +12,13 @@ "description": "\u8f93\u5165\u5728 Sony Bravia \u7535\u89c6\u4e0a\u663e\u793a\u7684 PIN \u7801\u3002 \n\n\u5982\u679c\u672a\u663e\u793a PIN \u7801\uff0c\u60a8\u9700\u8981\u5728\u7535\u89c6\u4e0a\u53d6\u6d88\u6ce8\u518c Home Assistant\uff0c\u8bf7\u8f6c\u5230\uff1a\u8bbe\u7f6e - >\u7f51\u7edc - >\u8fdc\u7a0b\u8bbe\u5907\u8bbe\u7f6e - >\u53d6\u6d88\u6ce8\u518c\u8fdc\u7a0b\u8bbe\u5907\u3002", "title": "\u6388\u6743 Sony Bravia \u7535\u89c6" }, + "reauth_confirm": { + "data": { + "pin": "PIN\u7801", + "use_psk": "\u4f7f\u7528 PSK \u8ba4\u8bc1" + }, + "description": "\u8f93\u5165 Sony Bravia \u7535\u89c6\u4e0a\u663e\u793a\u7684 PIN \u7801\u3002 \n\n\u5982\u679c PIN \u7801\u672a\u663e\u793a\uff0c\u60a8\u5fc5\u987b\u5728\u7535\u89c6\u4e0a\u53d6\u6d88\u6ce8\u518c Home Assistant\uff0c\u524d\u5f80\uff1a\u8bbe\u7f6e - >\u7f51\u7edc - >\u8fdc\u7a0b\u8bbe\u5907\u8bbe\u7f6e - >\u53d6\u6d88\u6ce8\u518c\u8fdc\u7a0b\u8bbe\u5907\u3002 \n\n\u60a8\u53ef\u4ee5\u4f7f\u7528 PSK\uff08\u9884\u5171\u4eab\u5bc6\u94a5\uff09\u4ee3\u66ff PIN\u3002 PSK \u662f\u7528\u4e8e\u8bbf\u95ee\u63a7\u5236\u7684\u7528\u6237\u5b9a\u4e49\u7684\u5bc6\u94a5\u3002\u63a8\u8350\u4f7f\u7528\u8fd9\u79cd\u8eab\u4efd\u9a8c\u8bc1\u65b9\u6cd5\uff0c\u56e0\u4e3a\u5b83\u66f4\u7a33\u5b9a\u3002\u8981\u5728\u7535\u89c6\u4e0a\u542f\u7528 PSK\uff0c\u8bf7\u8f6c\u5230\uff1a\u8bbe\u7f6e - >\u7f51\u7edc - >\u5bb6\u5ead\u7f51\u7edc\u8bbe\u7f6e - > IP \u63a7\u5236\u3002\u7136\u540e\u9009\u4e2d\u00ab\u4f7f\u7528 PSK \u8eab\u4efd\u9a8c\u8bc1\u00bb\u6846\u5e76\u8f93\u5165\u60a8\u7684 PSK \u800c\u4e0d\u662f PIN\u3002" + }, "user": { "description": "\u8bbe\u7f6e Sony Bravia \u7535\u89c6\u96c6\u6210\u3002\u5982\u679c\u60a8\u5728\u914d\u7f6e\u65b9\u9762\u9047\u5230\u95ee\u9898\uff0c\u8bf7\u8bbf\u95ee\uff1ahttps://www.home-assistant.io/integrations/braviatv\n\u786e\u4fdd\u7535\u89c6\u5df2\u6253\u5f00\u3002" } diff --git a/homeassistant/components/climate/translations/da.json b/homeassistant/components/climate/translations/da.json index 18b2bf16d49..e637e873e42 100644 --- a/homeassistant/components/climate/translations/da.json +++ b/homeassistant/components/climate/translations/da.json @@ -17,12 +17,12 @@ "state": { "_": { "auto": "Auto", - "cool": "K\u00f8l", - "dry": "T\u00f8r", + "cool": "Afk\u00f8ling", + "dry": "Affugtning", "fan_only": "Kun bl\u00e6ser", - "heat": "Varme", + "heat": "Opvarmning", "heat_cool": "Opvarm/k\u00f8l", - "off": "Fra" + "off": "Slukket" } }, "title": "Klima" diff --git a/homeassistant/components/generic/translations/ca.json b/homeassistant/components/generic/translations/ca.json index 90e12b8ea69..17e2ec60638 100644 --- a/homeassistant/components/generic/translations/ca.json +++ b/homeassistant/components/generic/translations/ca.json @@ -45,6 +45,13 @@ "verify_ssl": "Verifica el certificat SSL" }, "description": "Introdueix la configuraci\u00f3 de connexi\u00f3 amb la c\u00e0mera." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "La imatge es veu b\u00e9." + }, + "description": "![Vista pr\u00e8via de la imatge de la c\u00e0mera]({preview_url})", + "title": "Vista pr\u00e8via" } } }, diff --git a/homeassistant/components/generic/translations/zh-Hans.json b/homeassistant/components/generic/translations/zh-Hans.json index f13ba39c5b8..639aebabfd0 100644 --- a/homeassistant/components/generic/translations/zh-Hans.json +++ b/homeassistant/components/generic/translations/zh-Hans.json @@ -1,4 +1,15 @@ { + "config": { + "step": { + "user_confirm_still": { + "data": { + "confirmed_ok": "\u8fd9\u5f20\u56fe\u7247\u770b\u8d77\u6765\u4e0d\u9519\u3002" + }, + "description": "![\u6444\u50cf\u673a\u9759\u6b62\u753b\u9762\u9884\u89c8]({preview_url})", + "title": "\u9884\u89c8" + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/generic/translations/zh-Hant.json b/homeassistant/components/generic/translations/zh-Hant.json index ded2ea569c4..e58b3d34ef6 100644 --- a/homeassistant/components/generic/translations/zh-Hant.json +++ b/homeassistant/components/generic/translations/zh-Hant.json @@ -45,6 +45,13 @@ "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" }, "description": "\u8f38\u5165\u651d\u5f71\u6a5f\u9023\u7dda\u8a2d\u5b9a\u3002" + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "\u5f71\u50cf\u633a\u6e05\u6670\u3002" + }, + "description": "![\u651d\u5f71\u6a5f\u975c\u614b\u9810\u89bd]({preview_url})", + "title": "\u9810\u89bd" } } }, diff --git a/homeassistant/components/huawei_lte/translations/ca.json b/homeassistant/components/huawei_lte/translations/ca.json index 903ba233407..7c872862488 100644 --- a/homeassistant/components/huawei_lte/translations/ca.json +++ b/homeassistant/components/huawei_lte/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "No \u00e9s un dispositiu Huawei LTE" + "not_huawei_lte": "No \u00e9s un dispositiu Huawei LTE", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "connection_timeout": "S'ha acabat el temps d'espera de la connexi\u00f3", @@ -15,6 +16,14 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Introdueix les credencials d'acc\u00e9s del dispositiu.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/huawei_lte/translations/zh-Hans.json b/homeassistant/components/huawei_lte/translations/zh-Hans.json index c04409b2783..e229f8f28a4 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hans.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hans.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "\u8be5\u8bbe\u5907\u4e0d\u662f\u534e\u4e3a LTE \u8bbe\u5907" + "not_huawei_lte": "\u8be5\u8bbe\u5907\u4e0d\u662f\u534e\u4e3a LTE \u8bbe\u5907", + "reauth_successful": "\u91cd\u65b0\u8ba4\u8bc1\u6210\u529f" }, "error": { "connection_timeout": "\u8fde\u63a5\u8d85\u65f6", @@ -14,6 +15,14 @@ "unknown": "\u672a\u77e5\u9519\u8bef" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u7801", + "username": "\u7528\u6237\u540d" + }, + "description": "\u8bf7\u8f93\u5165\u8bbe\u5907\u8ba4\u8bc1\u51ed\u636e\u3002", + "title": "\u8bf7\u91cd\u65b0\u8ba4\u8bc1\u6b64\u96c6\u6210" + }, "user": { "data": { "url": "\u4e3b\u673a\u5730\u5740", diff --git a/homeassistant/components/huawei_lte/translations/zh-Hant.json b/homeassistant/components/huawei_lte/translations/zh-Hant.json index 85a2d84f6de..df014095c90 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hant.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "\u4e26\u975e\u83ef\u70ba LTE \u88dd\u7f6e" + "not_huawei_lte": "\u4e26\u975e\u83ef\u70ba LTE \u88dd\u7f6e", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "connection_timeout": "\u9023\u7dda\u903e\u6642", @@ -15,6 +16,14 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8f38\u5165\u88dd\u7f6e\u5b58\u53d6\u6191\u8b49\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "password": "\u5bc6\u78bc", diff --git a/homeassistant/components/mikrotik/translations/zh-Hans.json b/homeassistant/components/mikrotik/translations/zh-Hans.json index 14916be1264..fee7fc43e78 100644 --- a/homeassistant/components/mikrotik/translations/zh-Hans.json +++ b/homeassistant/components/mikrotik/translations/zh-Hans.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e" + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "reauth_successful": "\u91cd\u65b0\u8ba4\u8bc1\u6210\u529f" }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", @@ -9,6 +10,13 @@ "name_exists": "\u540d\u79f0\u5df2\u5b58\u5728" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u7801" + }, + "description": "{username} \u7684\u5bc6\u7801\u5df2\u5931\u6548\u3002", + "title": "\u91cd\u65b0\u8ba4\u8bc1\u96c6\u6210" + }, "user": { "data": { "host": "\u4e3b\u673a", diff --git a/homeassistant/components/nibe_heatpump/translations/zh-Hans.json b/homeassistant/components/nibe_heatpump/translations/zh-Hans.json new file mode 100644 index 00000000000..527e3717c4a --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/zh-Hans.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ip_address": "\u8fdc\u7a0bIP\u5730\u5740", + "listening_port": "\u672c\u5730\u76d1\u542c\u7aef\u53e3", + "remote_read_port": "\u8fdc\u7a0b\u8bfb\u53d6\u7aef\u53e3", + "remote_write_port": "\u8fdc\u7a0b\u5199\u5165\u7aef\u53e3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/zh-Hans.json b/homeassistant/components/octoprint/translations/zh-Hans.json new file mode 100644 index 00000000000..4849b3ce475 --- /dev/null +++ b/homeassistant/components/octoprint/translations/zh-Hans.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u91cd\u65b0\u8ba4\u8bc1\u6210\u529f" + }, + "step": { + "reauth_confirm": { + "data": { + "username": "\u7528\u6237\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/de.json b/homeassistant/components/openexchangerates/translations/de.json index 51a0294c816..a0f974d3374 100644 --- a/homeassistant/components/openexchangerates/translations/de.json +++ b/homeassistant/components/openexchangerates/translations/de.json @@ -26,8 +26,8 @@ }, "issues": { "deprecated_yaml": { - "description": "Die Konfiguration von Open Exchange Rates mittels YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die YAML-Konfiguration f\u00fcr Open Exchange Rates aus deiner configuration.yaml und starte den Home Assistant neu, um dieses Problem zu beheben.", - "title": "Die Open Exchange Rates YAML-Konfiguration wird entfernt" + "description": "Das Konfigurieren von Open Exchange Rates mit YAML wurde entfernt. \n\nEntferne die YAML-Konfiguration f\u00fcr Open Exchange Rates aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Open Exchange Rates YAML-Konfiguration wurde entfernt" } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/en.json b/homeassistant/components/openexchangerates/translations/en.json index eb41ae0ca14..f4827c4df4d 100644 --- a/homeassistant/components/openexchangerates/translations/en.json +++ b/homeassistant/components/openexchangerates/translations/en.json @@ -23,5 +23,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Open Exchange Rates using YAML has been removed.\n\nRemove the Open Exchange Rates YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Open Exchange Rates YAML configuration has been removed" + } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/es.json b/homeassistant/components/openexchangerates/translations/es.json index c5cf1b266e2..b71ef652770 100644 --- a/homeassistant/components/openexchangerates/translations/es.json +++ b/homeassistant/components/openexchangerates/translations/es.json @@ -26,8 +26,8 @@ }, "issues": { "deprecated_yaml": { - "description": "Se va a eliminar la configuraci\u00f3n de Open Exchange Rates mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Open Exchange Rates de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", - "title": "Se va a eliminar la configuraci\u00f3n YAML de Open Exchange Rates" + "description": "Se ha eliminado la configuraci\u00f3n de Open Exchange Rates mediante YAML. \n\nElimina la configuraci\u00f3n YAML de Open Exchange Rates de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se ha eliminado la configuraci\u00f3n YAML de Open Exchange Rates" } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/hu.json b/homeassistant/components/openexchangerates/translations/hu.json index 83f3ebae2b3..51843cd899a 100644 --- a/homeassistant/components/openexchangerates/translations/hu.json +++ b/homeassistant/components/openexchangerates/translations/hu.json @@ -26,7 +26,7 @@ }, "issues": { "deprecated_yaml": { - "description": "Az Open Exchange Rates konfigur\u00e1l\u00e1sa YAML haszn\u00e1lat\u00e1val elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "description": "Az Open Exchange Rates konfigur\u00e1l\u00e1sa YAML haszn\u00e1lat\u00e1val elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", "title": "Az Open Exchange Rates YAML-konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } } diff --git a/homeassistant/components/openexchangerates/translations/id.json b/homeassistant/components/openexchangerates/translations/id.json index 29f4a6eaabf..27f3503abc8 100644 --- a/homeassistant/components/openexchangerates/translations/id.json +++ b/homeassistant/components/openexchangerates/translations/id.json @@ -26,8 +26,8 @@ }, "issues": { "deprecated_yaml": { - "description": "Proses konfigurasi Open Exchange Rates lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Open Exchange Rates dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", - "title": "Konfigurasi YAML Open Exchange Rates dalam proses penghapusan" + "description": "Proses konfigurasi Open Exchange Rates lewat YAML telah dihapus.\n\nHapus konfigurasi YAML Open Exchange Rates dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Open Exchange Rates telah dihapus" } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/ru.json b/homeassistant/components/openexchangerates/translations/ru.json index 1707c8e8646..cfc8edb0e8d 100644 --- a/homeassistant/components/openexchangerates/translations/ru.json +++ b/homeassistant/components/openexchangerates/translations/ru.json @@ -26,8 +26,8 @@ }, "issues": { "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Open Exchange Rates \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", - "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Open Exchange Rates \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"Open Exchange Rates\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Open Exchange Rates \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/ca.json b/homeassistant/components/overkiz/translations/ca.json index ca55a8468b3..d1707e0cbf2 100644 --- a/homeassistant/components/overkiz/translations/ca.json +++ b/homeassistant/components/overkiz/translations/ca.json @@ -12,7 +12,8 @@ "too_many_attempts": "Massa intents amb un 'token' inv\u00e0lid, bloquejat temporalment", "too_many_requests": "Massa sol\u00b7licituds, torna-ho a provar m\u00e9s tard", "unknown": "Error inesperat", - "unknown_user": "Usuari desconegut. Els comptes de Somfy Protect no s\u00f3n compatibles amb aquesta integraci\u00f3." + "unknown_user": "Usuari desconegut. Els comptes de Somfy Protect no s\u00f3n compatibles amb aquesta integraci\u00f3.", + "unsupported_hardware": "{unsupported_device} no \u00e9s compatible amb aquesta integraci\u00f3." }, "flow_title": "Passarel\u00b7la: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/de.json b/homeassistant/components/overkiz/translations/de.json index 1e4cd0cb254..ff7536e0363 100644 --- a/homeassistant/components/overkiz/translations/de.json +++ b/homeassistant/components/overkiz/translations/de.json @@ -12,7 +12,8 @@ "too_many_attempts": "Zu viele Versuche mit einem ung\u00fcltigen Token, vor\u00fcbergehend gesperrt", "too_many_requests": "Zu viele Anfragen, versuche es sp\u00e4ter erneut.", "unknown": "Unerwarteter Fehler", - "unknown_user": "Unbekannter Benutzer. Somfy Protect-Konten werden von dieser Integration nicht unterst\u00fctzt." + "unknown_user": "Unbekannter Benutzer. Somfy Protect-Konten werden von dieser Integration nicht unterst\u00fctzt.", + "unsupported_hardware": "Deine {unsupported_device} Hardware wird von dieser Integration nicht unterst\u00fctzt." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/es.json b/homeassistant/components/overkiz/translations/es.json index a5a3ad16e0d..1cec438abbb 100644 --- a/homeassistant/components/overkiz/translations/es.json +++ b/homeassistant/components/overkiz/translations/es.json @@ -12,7 +12,8 @@ "too_many_attempts": "Demasiados intentos con un token no v\u00e1lido, prohibido temporalmente", "too_many_requests": "Demasiadas solicitudes, vuelve a intentarlo m\u00e1s tarde", "unknown": "Error inesperado", - "unknown_user": "Usuario desconocido. Las cuentas de Somfy Protect no son compatibles con esta integraci\u00f3n." + "unknown_user": "Usuario desconocido. Las cuentas de Somfy Protect no son compatibles con esta integraci\u00f3n.", + "unsupported_hardware": "Tu hardware {unsupported_device} no es compatible con esta integraci\u00f3n." }, "flow_title": "Puerta de enlace: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/et.json b/homeassistant/components/overkiz/translations/et.json index 34639ea1739..2170fff5c45 100644 --- a/homeassistant/components/overkiz/translations/et.json +++ b/homeassistant/components/overkiz/translations/et.json @@ -12,7 +12,8 @@ "too_many_attempts": "Liiga palju katseid kehtetu v\u00f5tmega, ajutiselt keelatud", "too_many_requests": "Liiga palju p\u00e4ringuid, proovi hiljem uuesti", "unknown": "Ootamatu t\u00f5rge", - "unknown_user": "Tundmatu kasutaja. See sidumine ei toeta Somfy Protecti kontosid." + "unknown_user": "Tundmatu kasutaja. See sidumine ei toeta Somfy Protecti kontosid.", + "unsupported_hardware": "See sidumine ei toeta {unsupported_device} riistvara." }, "flow_title": "L\u00fc\u00fcs: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/hu.json b/homeassistant/components/overkiz/translations/hu.json index 4fa4c0a9ddc..95e3090add0 100644 --- a/homeassistant/components/overkiz/translations/hu.json +++ b/homeassistant/components/overkiz/translations/hu.json @@ -12,7 +12,8 @@ "too_many_attempts": "T\u00fal sok pr\u00f3b\u00e1lkoz\u00e1s \u00e9rv\u00e9nytelen tokennel, ideiglenesen kitiltva", "too_many_requests": "T\u00fal sok a k\u00e9r\u00e9s, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", - "unknown_user": "Ismeretlen felhaszn\u00e1l\u00f3. Ez az integr\u00e1ci\u00f3 nem t\u00e1mogatja a Somfy Protect fi\u00f3kokat." + "unknown_user": "Ismeretlen felhaszn\u00e1l\u00f3. Ez az integr\u00e1ci\u00f3 nem t\u00e1mogatja a Somfy Protect fi\u00f3kokat.", + "unsupported_hardware": "{unsupported_device} hardver\u00e9t ez az integr\u00e1ci\u00f3 nem t\u00e1mogatja." }, "flow_title": "\u00c1tj\u00e1r\u00f3: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/id.json b/homeassistant/components/overkiz/translations/id.json index 709cef9819c..8f4b1912366 100644 --- a/homeassistant/components/overkiz/translations/id.json +++ b/homeassistant/components/overkiz/translations/id.json @@ -12,7 +12,8 @@ "too_many_attempts": "Terlalu banyak percobaan dengan token yang tidak valid, untuk sementara diblokir", "too_many_requests": "Terlalu banyak permintaan, coba lagi nanti.", "unknown": "Kesalahan yang tidak diharapkan", - "unknown_user": "Pengguna tidak dikenal. Akun Somfy Protect tidak didukung oleh integrasi ini." + "unknown_user": "Pengguna tidak dikenal. Akun Somfy Protect tidak didukung oleh integrasi ini.", + "unsupported_hardware": "Perangkat keras {unsupported_device} Anda tidak didukung oleh integrasi ini." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/pl.json b/homeassistant/components/overkiz/translations/pl.json index 23065776db4..517ea42ac47 100644 --- a/homeassistant/components/overkiz/translations/pl.json +++ b/homeassistant/components/overkiz/translations/pl.json @@ -12,7 +12,8 @@ "too_many_attempts": "Zbyt wiele pr\u00f3b z nieprawid\u0142owym tokenem, konto tymczasowo zablokowane", "too_many_requests": "Zbyt wiele \u017c\u0105da\u0144, spr\u00f3buj ponownie p\u00f3\u017aniej.", "unknown": "Nieoczekiwany b\u0142\u0105d", - "unknown_user": "Nieznany u\u017cytkownik. Konta Somfy Protect nie s\u0105 obs\u0142ugiwane przez t\u0119 integracj\u0119." + "unknown_user": "Nieznany u\u017cytkownik. Konta Somfy Protect nie s\u0105 obs\u0142ugiwane przez t\u0119 integracj\u0119.", + "unsupported_hardware": "Twoje urz\u0105dzenie {unsupported_device} nie jest wspierane przez t\u0119 integracj\u0119." }, "flow_title": "Bramka: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/ru.json b/homeassistant/components/overkiz/translations/ru.json index 17ef0ecd27e..128792152dc 100644 --- a/homeassistant/components/overkiz/translations/ru.json +++ b/homeassistant/components/overkiz/translations/ru.json @@ -12,7 +12,8 @@ "too_many_attempts": "\u0421\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u043f\u043e\u043f\u044b\u0442\u043e\u043a \u0441 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0442\u043e\u043a\u0435\u043d\u043e\u043c, \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e.", "too_many_requests": "\u0421\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \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.", - "unknown_user": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c. \u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 Somfy Protect." + "unknown_user": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c. \u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 Somfy Protect.", + "unsupported_hardware": "{unsupported_device} \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439." }, "flow_title": "\u0428\u043b\u044e\u0437: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/zh-Hans.json b/homeassistant/components/overkiz/translations/zh-Hans.json new file mode 100644 index 00000000000..c2cd756d03f --- /dev/null +++ b/homeassistant/components/overkiz/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unsupported_hardware": "\u60a8\u7684\u8bbe\u5907\u786c\u4ef6:\u201c {unsupported_device}\u201d \uff0c\u4e0d\u88ab\u6b64\u96c6\u6210\u6240\u652f\u6301\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/zh-Hant.json b/homeassistant/components/overkiz/translations/zh-Hant.json index c9e20812ecc..ea8ebcd29dc 100644 --- a/homeassistant/components/overkiz/translations/zh-Hant.json +++ b/homeassistant/components/overkiz/translations/zh-Hant.json @@ -12,7 +12,8 @@ "too_many_attempts": "\u4f7f\u7528\u7121\u6548\u6b0a\u6756\u5617\u8a66\u6b21\u6578\u904e\u591a\uff0c\u66ab\u6642\u906d\u5230\u5c01\u9396", "too_many_requests": "\u8acb\u6c42\u6b21\u6578\u904e\u591a\uff0c\u8acb\u7a0d\u5f8c\u91cd\u8a66\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4", - "unknown_user": "\u672a\u77e5\u4f7f\u7528\u8005\u3001\u6b64\u6574\u5408\u4e0d\u652f\u63f4 Somfy Protect \u5e33\u865f\u3002" + "unknown_user": "\u672a\u77e5\u4f7f\u7528\u8005\u3001\u6b64\u6574\u5408\u4e0d\u652f\u63f4 Somfy Protect \u5e33\u865f\u3002", + "unsupported_hardware": "\u6b64\u6574\u5408\u4e0d\u652f\u63f4\u60a8\u7684 {unsupported_device} \u786c\u9ad4\u3002" }, "flow_title": "\u9598\u9053\u5668\uff1a{gateway_id}", "step": { diff --git a/homeassistant/components/plugwise/translations/select.ca.json b/homeassistant/components/plugwise/translations/select.ca.json new file mode 100644 index 00000000000..e7caf47404c --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.ca.json @@ -0,0 +1,11 @@ +{ + "state": { + "plugwise__regulation_mode": { + "bleeding_cold": "Molt fred", + "bleeding_hot": "Molt calent", + "cooling": "Refredant", + "heating": "Escalfant", + "off": "OFF" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/select.zh-Hans.json b/homeassistant/components/plugwise/translations/select.zh-Hans.json new file mode 100644 index 00000000000..d454e38b89b --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.zh-Hans.json @@ -0,0 +1,11 @@ +{ + "state": { + "plugwise__regulation_mode": { + "bleeding_cold": "\u8fc7\u51b7", + "bleeding_hot": "\u8fc7\u70ed", + "cooling": "\u5236\u51b7", + "heating": "\u5236\u70ed", + "off": "\u5173" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/select.zh-Hant.json b/homeassistant/components/plugwise/translations/select.zh-Hant.json new file mode 100644 index 00000000000..6c3837959cc --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.zh-Hant.json @@ -0,0 +1,11 @@ +{ + "state": { + "plugwise__regulation_mode": { + "bleeding_cold": "\u5f37\u51b7", + "bleeding_hot": "\u5f37\u6696", + "cooling": "\u51b7\u6c23", + "heating": "\u6696\u6c23", + "off": "\u95dc\u9589" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/zh-Hans.json b/homeassistant/components/radarr/translations/zh-Hans.json new file mode 100644 index 00000000000..fe3b28ff5b6 --- /dev/null +++ b/homeassistant/components/radarr/translations/zh-Hans.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u91cd\u65b0\u8ba4\u8bc1\u6210\u529f" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u9a8c\u8bc1\u5931\u8d25", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "step": { + "reauth_confirm": { + "title": "\u91cd\u65b0\u9a8c\u8bc1\u96c6\u6210" + }, + "user": { + "data": { + "api_key": "API\u79d8\u94a5", + "url": "URL", + "verify_ssl": "\u9a8c\u8bc1SSL\u8bc1\u4e66" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/zh-Hans.json b/homeassistant/components/uptimerobot/translations/zh-Hans.json index d680c09e967..f67afb7a2cb 100644 --- a/homeassistant/components/uptimerobot/translations/zh-Hans.json +++ b/homeassistant/components/uptimerobot/translations/zh-Hans.json @@ -17,7 +17,8 @@ "data": { "api_key": "API \u5bc6\u94a5" }, - "description": "\u60a8\u9700\u8981\u4ece Uptime Robot \u4e2d\u63d0\u4f9b\u4e00\u4e2a\"\u53ea\u8bfb API \u5bc6\u94a5\"" + "description": "\u60a8\u9700\u8981\u4ece Uptime Robot \u4e2d\u63d0\u4f9b\u4e00\u4e2a\"\u53ea\u8bfb API \u5bc6\u94a5\"", + "title": "\u91cd\u65b0\u8ba4\u8bc1\u96c6\u6210" }, "user": { "data": { diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index c66de6758ed..db5d77047f5 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -213,9 +213,11 @@ "title": "Reconfiguraci\u00f3 de ZHA" }, "instruct_unplug": { + "description": "La r\u00e0dio antiga s'ha reiniciat. Si el maquinari ja no \u00e9s necessari, ara pots desconnectar-lo.", "title": "Desconnecta la r\u00e0dio antiga" }, "intent_migrate": { + "description": "La r\u00e0dio antiga es restablir\u00e0 de f\u00e0brica. Si utilitzes un adaptador de Z-Wave i Zigbee combinat com el HUSBZB-1, nom\u00e9s restablir\u00e0 la part del Zigbee. \n\nVols continuar?", "title": "Migra a una nova r\u00e0dio" }, "manual_pick_radio_type": { diff --git a/homeassistant/components/zha/translations/zh-Hans.json b/homeassistant/components/zha/translations/zh-Hans.json index ab4b69efbb0..c2b8d9f7cc2 100644 --- a/homeassistant/components/zha/translations/zh-Hans.json +++ b/homeassistant/components/zha/translations/zh-Hans.json @@ -87,5 +87,20 @@ "remote_button_short_release": "\"{subtype}\" \u677e\u5f00", "remote_button_triple_press": "\"{subtype}\" \u4e09\u8fde\u51fb" } + }, + "options": { + "step": { + "intent_migrate": { + "title": "\u8fc1\u79fb\u5230\u65b0\u7684\u65e0\u7ebf\u7535\u8bbe\u7f6e" + }, + "prompt_migrate_or_reconfigure": { + "description": "\u60a8\u662f\u5426\u6b63\u5728\u8fc1\u79fb\u5230\u65b0\u65e0\u7ebf\u7535\u6216\u91cd\u65b0\u914d\u7f6e\u5f53\u524d\u65e0\u7ebf\u7535\uff1f", + "menu_options": { + "intent_migrate": "\u8fc1\u79fb\u5230\u65b0\u7684\u65e0\u7ebf\u7535\u8bbe\u7f6e", + "intent_reconfigure": "\u91cd\u65b0\u914d\u7f6e\u5f53\u524d\u65e0\u7ebf\u7535" + }, + "title": "\u8fc1\u79fb\u6216\u91cd\u65b0\u914d\u7f6e" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index 9f36a3f03a9..23d64c8cc42 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -212,6 +212,14 @@ "description": "ZHA \u5c07\u505c\u6b62\u3001\u662f\u5426\u8981\u7e7c\u7e8c\uff1f", "title": "\u91cd\u65b0\u8a2d\u5b9a ZHA" }, + "instruct_unplug": { + "description": "\u820a\u7121\u7dda\u96fb\u5df2\u7d93\u91cd\u7f6e\uff0c\u5047\u5982\u786c\u9ad4\u4e0d\u518d\u4f7f\u7528\u3001\u53ef\u4ee5\u9032\u884c\u79fb\u9664\u3002", + "title": "\u79fb\u9664\u820a\u7121\u7dda\u96fb" + }, + "intent_migrate": { + "description": "\u820a\u7121\u7dda\u96fb\u5c07\u6703\u9032\u884c\u91cd\u7f6e\u3002\u5047\u5982\u4f7f\u7528\u7684\u9069\u914d\u5668\u70ba\u985e\u4f3c\u65bc HUSBZB-1 \u7684 Z-Wave \u8207 Zigbee \u8907\u5408\u88dd\u7f6e\uff0c\u5c07\u50c5\u6703\u91cd\u7f6e Zigbee \u90e8\u5206\u3002\n\n\u662f\u5426\u8981\u7e7c\u7e8c\uff1f", + "title": "\u9077\u79fb\u81f3\u65b0\u7121\u7dda\u96fb" + }, "manual_pick_radio_type": { "data": { "radio_type": "\u7121\u7dda\u96fb\u985e\u5225" @@ -235,6 +243,14 @@ "description": "\u5099\u4efd\u4e2d\u7684 IEEE \u4f4d\u5740\u8207\u73fe\u6709\u7121\u7dda\u96fb\u4e0d\u540c\u3002\u70ba\u4e86\u78ba\u8a8d\u7db2\u8def\u6b63\u5e38\u5de5\u4f5c\uff0c\u7121\u7dda\u96fb\u7684 IEEE \u4f4d\u5740\u5fc5\u9808\u9032\u884c\u8b8a\u66f4\u3002\n\n\u6b64\u70ba\u6c38\u4e45\u6027\u64cd\u4f5c\u3002.", "title": "\u8986\u5beb\u7121\u7dda\u96fb IEEE \u4f4d\u5740" }, + "prompt_migrate_or_reconfigure": { + "description": "\u8981\u9077\u79fb\u81f3\u65b0\u7121\u7dda\u96fb\u6216\u91cd\u65b0\u8a2d\u5b9a\u76ee\u524d\u7121\u7dda\u96fb\uff1f", + "menu_options": { + "intent_migrate": "\u9077\u79fb\u81f3\u65b0\u7121\u7dda\u96fb", + "intent_reconfigure": "\u91cd\u65b0\u8a2d\u5b9a\u76ee\u524d\u7121\u7dda\u96fb" + }, + "title": "\u9077\u79fb\u6216\u91cd\u65b0\u8a2d\u5b9a" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "\u4e0a\u50b3\u6a94\u6848" From 58d531841bc2197616e7285d0561a9935d9bda73 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Sun, 9 Oct 2022 23:06:28 -0400 Subject: [PATCH 295/985] Fix typo SIGNAL_BOOTSTRAP_INTEGRATONS -> SIGNAL_BOOTSTRAP_INTEGRATIONS (#79970) --- homeassistant/bootstrap.py | 6 +++--- homeassistant/components/websocket_api/commands.py | 4 ++-- homeassistant/const.py | 2 +- tests/components/websocket_api/test_commands.py | 4 ++-- tests/test_bootstrap.py | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index f2339e6bd1a..31834c7b7a3 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -21,7 +21,7 @@ from .components import http, persistent_notification from .const import ( REQUIRED_NEXT_PYTHON_HA_RELEASE, REQUIRED_NEXT_PYTHON_VER, - SIGNAL_BOOTSTRAP_INTEGRATONS, + SIGNAL_BOOTSTRAP_INTEGRATIONS, ) from .exceptions import HomeAssistantError from .helpers import ( @@ -431,7 +431,7 @@ async def _async_watch_pending_setups(hass: core.HomeAssistant) -> None: _LOGGER.debug("Integration remaining: %s", remaining_with_setup_started) if remaining_with_setup_started or not previous_was_empty: async_dispatcher_send( - hass, SIGNAL_BOOTSTRAP_INTEGRATONS, remaining_with_setup_started + hass, SIGNAL_BOOTSTRAP_INTEGRATIONS, remaining_with_setup_started ) previous_was_empty = not remaining_with_setup_started await asyncio.sleep(SLOW_STARTUP_CHECK_INTERVAL) @@ -622,7 +622,7 @@ async def _async_set_up_integrations( _LOGGER.warning("Setup timed out for bootstrap - moving forward") watch_task.cancel() - async_dispatcher_send(hass, SIGNAL_BOOTSTRAP_INTEGRATONS, {}) + async_dispatcher_send(hass, SIGNAL_BOOTSTRAP_INTEGRATIONS, {}) _LOGGER.debug( "Integration setup times: %s", diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index a78099e6065..68dbffa7440 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -12,7 +12,7 @@ from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_READ from homeassistant.const import ( EVENT_STATE_CHANGED, MATCH_ALL, - SIGNAL_BOOTSTRAP_INTEGRATONS, + SIGNAL_BOOTSTRAP_INTEGRATIONS, ) from homeassistant.core import Context, Event, HomeAssistant, State, callback from homeassistant.exceptions import ( @@ -151,7 +151,7 @@ def handle_subscribe_bootstrap_integrations( connection.send_message(messages.event_message(msg["id"], message)) connection.subscriptions[msg["id"]] = async_dispatcher_connect( - hass, SIGNAL_BOOTSTRAP_INTEGRATONS, forward_bootstrap_integrations + hass, SIGNAL_BOOTSTRAP_INTEGRATIONS, forward_bootstrap_integrations ) connection.send_result(msg["id"]) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4f4da4925d0..c89eca63623 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -786,4 +786,4 @@ CAST_APP_ID_HOMEASSISTANT_LOVELACE: Final = "A078F6B0" # User used by Supervisor HASSIO_USER_NAME = "Supervisor" -SIGNAL_BOOTSTRAP_INTEGRATONS = "bootstrap_integrations" +SIGNAL_BOOTSTRAP_INTEGRATIONS = "bootstrap_integrations" diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 27ae21db6e2..135882d9aed 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -14,7 +14,7 @@ from homeassistant.components.websocket_api.auth import ( TYPE_AUTH_REQUIRED, ) from homeassistant.components.websocket_api.const import FEATURE_COALESCE_MESSAGES, URL -from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATONS +from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATIONS from homeassistant.core import Context, HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity @@ -1712,7 +1712,7 @@ async def test_subscribe_unsubscribe_bootstrap_integrations( message = {"august": 12.5, "isy994": 12.8} - async_dispatcher_send(hass, SIGNAL_BOOTSTRAP_INTEGRATONS, message) + async_dispatcher_send(hass, SIGNAL_BOOTSTRAP_INTEGRATIONS, message) msg = await websocket_client.receive_json() assert msg["id"] == 7 assert msg["type"] == "event" diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 56c15f49337..e51f4d315ee 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -9,7 +9,7 @@ import pytest from homeassistant import bootstrap, core, runner import homeassistant.config as config_util -from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATONS +from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATIONS from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -748,7 +748,7 @@ async def test_empty_integrations_list_is_only_sent_at_the_end_of_bootstrap(hass integrations.append(data) async_dispatcher_connect( - hass, SIGNAL_BOOTSTRAP_INTEGRATONS, _bootstrap_integrations + hass, SIGNAL_BOOTSTRAP_INTEGRATIONS, _bootstrap_integrations ) with patch.object(bootstrap, "SLOW_STARTUP_CHECK_INTERVAL", 0.05): await bootstrap._async_set_up_integrations( From ef719cf7ef645da46006814bad055841a0925383 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Oct 2022 17:56:11 -1000 Subject: [PATCH 296/985] Bump bluetooth-auto-recovery to 0.3.4 (#79971) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index e7cd7803878..9b240f0c612 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -9,7 +9,7 @@ "bleak==0.18.1", "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.6.0", - "bluetooth-auto-recovery==0.3.3", + "bluetooth-auto-recovery==0.3.4", "dbus-fast==1.38.0" ], "codeowners": ["@bdraco"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0d8498399c6..20d4d940b60 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ bcrypt==3.1.7 bleak-retry-connector==2.1.3 bleak==0.18.1 bluetooth-adapters==0.6.0 -bluetooth-auto-recovery==0.3.3 +bluetooth-auto-recovery==0.3.4 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 8f8160c199a..ed5017827d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -441,7 +441,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.6.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.3 +bluetooth-auto-recovery==0.3.4 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 233245acbbd..b60588132ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -355,7 +355,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.6.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.3 +bluetooth-auto-recovery==0.3.4 # homeassistant.components.bond bond-async==0.1.22 From 84acb416b80c84610b71e517954ff80c4b50b6bb Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 9 Oct 2022 23:50:05 -0500 Subject: [PATCH 297/985] Use server name as entry title in Jellyfin (#79965) --- .../components/jellyfin/client_wrapper.py | 29 +- .../components/jellyfin/config_flow.py | 15 +- tests/components/jellyfin/__init__.py | 16 + tests/components/jellyfin/conftest.py | 14 +- tests/components/jellyfin/const.py | 10 - .../auth-connect-address-failure.json | 3 + .../fixtures/auth-connect-address.json | 4 + .../jellyfin/fixtures/auth-login-failure.json | 1 + .../jellyfin/fixtures/auth-login.json | 1844 +++++++++++++++++ .../jellyfin/fixtures/get-user-settings.json | 19 + tests/components/jellyfin/test_config_flow.py | 28 +- 11 files changed, 1939 insertions(+), 44 deletions(-) create mode 100644 tests/components/jellyfin/fixtures/auth-connect-address-failure.json create mode 100644 tests/components/jellyfin/fixtures/auth-connect-address.json create mode 100644 tests/components/jellyfin/fixtures/auth-login-failure.json create mode 100644 tests/components/jellyfin/fixtures/auth-login.json create mode 100644 tests/components/jellyfin/fixtures/get-user-settings.json diff --git a/homeassistant/components/jellyfin/client_wrapper.py b/homeassistant/components/jellyfin/client_wrapper.py index 65de5d4232e..c6ae67b4c80 100644 --- a/homeassistant/components/jellyfin/client_wrapper.py +++ b/homeassistant/components/jellyfin/client_wrapper.py @@ -20,17 +20,17 @@ from .const import CLIENT_VERSION, USER_AGENT, USER_APP_NAME async def validate_input( hass: HomeAssistant, user_input: dict[str, Any], client: JellyfinClient -) -> str: +) -> tuple[str, dict[str, Any]]: """Validate that the provided url and credentials can be used to connect.""" url = user_input[CONF_URL] username = user_input[CONF_USERNAME] password = user_input[CONF_PASSWORD] - userid = await hass.async_add_executor_job( + user_id, connect_result = await hass.async_add_executor_job( _connect, client, url, username, password ) - return userid + return (user_id, connect_result) def create_client(device_id: str, device_name: str | None = None) -> JellyfinClient: @@ -47,21 +47,30 @@ def create_client(device_id: str, device_name: str | None = None) -> JellyfinCli return client -def _connect(client: JellyfinClient, url: str, username: str, password: str) -> str: +def _connect( + client: JellyfinClient, url: str, username: str, password: str +) -> tuple[str, dict[str, Any]]: """Connect to the Jellyfin server and assert that the user can login.""" client.config.data["auth.ssl"] = url.startswith("https") - _connect_to_address(client.auth, url) + connect_result = _connect_to_address(client.auth, url) + _login(client.auth, url, username, password) - return _get_id(client.jellyfin) + + return (_get_user_id(client.jellyfin), connect_result) -def _connect_to_address(connection_manager: ConnectionManager, url: str) -> None: +def _connect_to_address( + connection_manager: ConnectionManager, url: str +) -> dict[str, Any]: """Connect to the Jellyfin server.""" - state = connection_manager.connect_to_address(url) - if state["State"] != CONNECTION_STATE["ServerSignIn"]: + result: dict[str, Any] = connection_manager.connect_to_address(url) + + if result["State"] != CONNECTION_STATE["ServerSignIn"]: raise CannotConnect + return result + def _login( connection_manager: ConnectionManager, @@ -76,7 +85,7 @@ def _login( raise InvalidAuth -def _get_id(api: API) -> str: +def _get_user_id(api: API) -> str: """Set the unique userid from a Jellyfin server.""" settings: dict[str, Any] = api.get_user_settings() userid: str = settings["Id"] diff --git a/homeassistant/components/jellyfin/config_flow.py b/homeassistant/components/jellyfin/config_flow.py index 51553f1a6f2..84b78d51926 100644 --- a/homeassistant/components/jellyfin/config_flow.py +++ b/homeassistant/components/jellyfin/config_flow.py @@ -54,7 +54,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): client = create_client(device_id=self.client_device_id) try: - userid = await validate_input(self.hass, user_input, client) + user_id, connect_result = await validate_input( + self.hass, user_input, client + ) except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: @@ -63,11 +65,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" _LOGGER.exception(ex) else: - await self.async_set_unique_id(userid) + entry_title = user_input[CONF_URL] + + server_info: dict[str, Any] = connect_result["Servers"][0] + + if server_name := server_info.get("Name"): + entry_title = server_name + + await self.async_set_unique_id(user_id) self._abort_if_unique_id_configured() return self.async_create_entry( - title=user_input[CONF_URL], + title=entry_title, data={CONF_CLIENT_DEVICE_ID: self.client_device_id, **user_input}, ) diff --git a/tests/components/jellyfin/__init__.py b/tests/components/jellyfin/__init__.py index e5ff9ab3207..c1f7bbb2f35 100644 --- a/tests/components/jellyfin/__init__.py +++ b/tests/components/jellyfin/__init__.py @@ -1 +1,17 @@ """Tests for the jellyfin integration.""" +import json +from typing import Any + +from homeassistant.core import HomeAssistant + +from tests.common import load_fixture + + +def load_json_fixture(filename: str) -> Any: + """Load JSON fixture on-demand.""" + return json.loads(load_fixture(f"jellyfin/{filename}")) + + +async def async_load_json_fixture(hass: HomeAssistant, filename: str) -> Any: + """Load JSON fixture on-demand asynchronously.""" + return await hass.async_add_executor_job(load_json_fixture, filename) diff --git a/tests/components/jellyfin/conftest.py b/tests/components/jellyfin/conftest.py index c1d9634aede..bd86ae925c2 100644 --- a/tests/components/jellyfin/conftest.py +++ b/tests/components/jellyfin/conftest.py @@ -10,11 +10,7 @@ from jellyfin_apiclient_python.configuration import Config from jellyfin_apiclient_python.connection_manager import ConnectionManager import pytest -from .const import ( - MOCK_SUCCESFUL_CONNECTION_STATE, - MOCK_SUCCESFUL_LOGIN_RESPONSE, - MOCK_USER_SETTINGS, -) +from . import load_json_fixture @pytest.fixture @@ -40,8 +36,10 @@ def mock_client_device_id() -> Generator[None, MagicMock, None]: def mock_auth() -> MagicMock: """Return a mocked ConnectionManager.""" jf_auth = create_autospec(ConnectionManager) - jf_auth.connect_to_address.return_value = MOCK_SUCCESFUL_CONNECTION_STATE - jf_auth.login.return_value = MOCK_SUCCESFUL_LOGIN_RESPONSE + jf_auth.connect_to_address.return_value = load_json_fixture( + "auth-connect-address.json" + ) + jf_auth.login.return_value = load_json_fixture("auth-login.json") return jf_auth @@ -50,7 +48,7 @@ def mock_auth() -> MagicMock: def mock_api() -> MagicMock: """Return a mocked API.""" jf_api = create_autospec(API) - jf_api.get_user_settings.return_value = MOCK_USER_SETTINGS + jf_api.get_user_settings.return_value = load_json_fixture("get-user-settings.json") return jf_api diff --git a/tests/components/jellyfin/const.py b/tests/components/jellyfin/const.py index b33f00818b7..4953824a1c5 100644 --- a/tests/components/jellyfin/const.py +++ b/tests/components/jellyfin/const.py @@ -2,16 +2,6 @@ from typing import Final -from jellyfin_apiclient_python.connection_manager import CONNECTION_STATE - TEST_URL: Final = "https://example.com" TEST_USERNAME: Final = "test-username" TEST_PASSWORD: Final = "test-password" - -MOCK_SUCCESFUL_CONNECTION_STATE: Final = {"State": CONNECTION_STATE["ServerSignIn"]} -MOCK_SUCCESFUL_LOGIN_RESPONSE: Final = {"AccessToken": "Test"} - -MOCK_UNSUCCESFUL_CONNECTION_STATE: Final = {"State": CONNECTION_STATE["Unavailable"]} -MOCK_UNSUCCESFUL_LOGIN_RESPONSE: Final = {""} - -MOCK_USER_SETTINGS: Final = {"Id": "123"} diff --git a/tests/components/jellyfin/fixtures/auth-connect-address-failure.json b/tests/components/jellyfin/fixtures/auth-connect-address-failure.json new file mode 100644 index 00000000000..9055c2c7105 --- /dev/null +++ b/tests/components/jellyfin/fixtures/auth-connect-address-failure.json @@ -0,0 +1,3 @@ +{ + "State": 0 +} diff --git a/tests/components/jellyfin/fixtures/auth-connect-address.json b/tests/components/jellyfin/fixtures/auth-connect-address.json new file mode 100644 index 00000000000..2adfded3070 --- /dev/null +++ b/tests/components/jellyfin/fixtures/auth-connect-address.json @@ -0,0 +1,4 @@ +{ + "State": 2, + "Servers": [{ "Id": "SERVER-UUID", "Name": "JELLYFIN-SERVER" }] +} diff --git a/tests/components/jellyfin/fixtures/auth-login-failure.json b/tests/components/jellyfin/fixtures/auth-login-failure.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/tests/components/jellyfin/fixtures/auth-login-failure.json @@ -0,0 +1 @@ +{} diff --git a/tests/components/jellyfin/fixtures/auth-login.json b/tests/components/jellyfin/fixtures/auth-login.json new file mode 100644 index 00000000000..5df9dd599a8 --- /dev/null +++ b/tests/components/jellyfin/fixtures/auth-login.json @@ -0,0 +1,1844 @@ +{ + "User": { + "Name": "string", + "ServerId": "string", + "ServerName": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "PrimaryImageTag": "string", + "HasPassword": true, + "HasConfiguredPassword": true, + "HasConfiguredEasyPassword": true, + "EnableAutoLogin": true, + "LastLoginDate": "2019-08-24T14:15:22Z", + "LastActivityDate": "2019-08-24T14:15:22Z", + "Configuration": { + "AudioLanguagePreference": "string", + "PlayDefaultAudioTrack": true, + "SubtitleLanguagePreference": "string", + "DisplayMissingEpisodes": true, + "GroupedFolders": ["string"], + "SubtitleMode": "Default", + "DisplayCollectionsView": true, + "EnableLocalPassword": true, + "OrderedViews": ["string"], + "LatestItemsExcludes": ["string"], + "MyMediaExcludes": ["string"], + "HidePlayedInLatest": true, + "RememberAudioSelections": true, + "RememberSubtitleSelections": true, + "EnableNextEpisodeAutoPlay": true + }, + "Policy": { + "IsAdministrator": true, + "IsHidden": true, + "IsDisabled": true, + "MaxParentalRating": 0, + "BlockedTags": ["string"], + "EnableUserPreferenceAccess": true, + "AccessSchedules": [ + { + "Id": 0, + "UserId": "08ba1929-681e-4b24-929b-9245852f65c0", + "DayOfWeek": "Sunday", + "StartHour": 0, + "EndHour": 0 + } + ], + "BlockUnratedItems": ["Movie"], + "EnableRemoteControlOfOtherUsers": true, + "EnableSharedDeviceControl": true, + "EnableRemoteAccess": true, + "EnableLiveTvManagement": true, + "EnableLiveTvAccess": true, + "EnableMediaPlayback": true, + "EnableAudioPlaybackTranscoding": true, + "EnableVideoPlaybackTranscoding": true, + "EnablePlaybackRemuxing": true, + "ForceRemoteSourceTranscoding": true, + "EnableContentDeletion": true, + "EnableContentDeletionFromFolders": ["string"], + "EnableContentDownloading": true, + "EnableSyncTranscoding": true, + "EnableMediaConversion": true, + "EnabledDevices": ["string"], + "EnableAllDevices": true, + "EnabledChannels": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "EnableAllChannels": true, + "EnabledFolders": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "EnableAllFolders": true, + "InvalidLoginAttemptCount": 0, + "LoginAttemptsBeforeLockout": 0, + "MaxActiveSessions": 0, + "EnablePublicSharing": true, + "BlockedMediaFolders": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "BlockedChannels": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "RemoteClientBitrateLimit": 0, + "AuthenticationProviderId": "string", + "PasswordResetProviderId": "string", + "SyncPlayAccess": "CreateAndJoinGroups" + }, + "PrimaryImageAspectRatio": 0 + }, + "SessionInfo": { + "PlayState": { + "PositionTicks": 0, + "CanSeek": true, + "IsPaused": true, + "IsMuted": true, + "VolumeLevel": 0, + "AudioStreamIndex": 0, + "SubtitleStreamIndex": 0, + "MediaSourceId": "string", + "PlayMethod": "Transcode", + "RepeatMode": "RepeatNone", + "LiveStreamId": "string" + }, + "AdditionalUsers": [ + { + "UserId": "08ba1929-681e-4b24-929b-9245852f65c0", + "UserName": "string" + } + ], + "Capabilities": { + "PlayableMediaTypes": ["string"], + "SupportedCommands": ["MoveUp"], + "SupportsMediaControl": true, + "SupportsContentUploading": true, + "MessageCallbackUrl": "string", + "SupportsPersistentIdentifier": true, + "SupportsSync": true, + "DeviceProfile": { + "Name": "string", + "Id": "string", + "Identification": { + "FriendlyName": "string", + "ModelNumber": "string", + "SerialNumber": "string", + "ModelName": "string", + "ModelDescription": "string", + "ModelUrl": "string", + "Manufacturer": "string", + "ManufacturerUrl": "string", + "Headers": [ + { + "Name": "string", + "Value": "string", + "Match": "Equals" + } + ] + }, + "FriendlyName": "string", + "Manufacturer": "string", + "ManufacturerUrl": "string", + "ModelName": "string", + "ModelDescription": "string", + "ModelNumber": "string", + "ModelUrl": "string", + "SerialNumber": "string", + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "string", + "UserId": "string", + "AlbumArtPn": "string", + "MaxAlbumArtWidth": 0, + "MaxAlbumArtHeight": 0, + "MaxIconWidth": 0, + "MaxIconHeight": 0, + "MaxStreamingBitrate": 0, + "MaxStaticBitrate": 0, + "MusicStreamingTranscodingBitrate": 0, + "MaxStaticMusicBitrate": 0, + "SonyAggregationFlags": "string", + "ProtocolInfo": "string", + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "XmlRootAttributes": [ + { + "Name": "string", + "Value": "string" + } + ], + "DirectPlayProfiles": [ + { + "Container": "string", + "AudioCodec": "string", + "VideoCodec": "string", + "Type": "Audio" + } + ], + "TranscodingProfiles": [ + { + "Container": "string", + "Type": "Audio", + "VideoCodec": "string", + "AudioCodec": "string", + "Protocol": "string", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "string", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "Conditions": [ + { + "Condition": "Equals", + "Property": "AudioChannels", + "Value": "string", + "IsRequired": true + } + ] + } + ], + "ContainerProfiles": [ + { + "Type": "Audio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "AudioChannels", + "Value": "string", + "IsRequired": true + } + ], + "Container": "string" + } + ], + "CodecProfiles": [ + { + "Type": "Video", + "Conditions": [ + { + "Condition": "Equals", + "Property": "AudioChannels", + "Value": "string", + "IsRequired": true + } + ], + "ApplyConditions": [ + { + "Condition": "Equals", + "Property": "AudioChannels", + "Value": "string", + "IsRequired": true + } + ], + "Codec": "string", + "Container": "string" + } + ], + "ResponseProfiles": [ + { + "Container": "string", + "AudioCodec": "string", + "VideoCodec": "string", + "Type": "Audio", + "OrgPn": "string", + "MimeType": "string", + "Conditions": [ + { + "Condition": "Equals", + "Property": "AudioChannels", + "Value": "string", + "IsRequired": true + } + ] + } + ], + "SubtitleProfiles": [ + { + "Format": "string", + "Method": "Encode", + "DidlMode": "string", + "Language": "string", + "Container": "string" + } + ] + }, + "AppStoreUrl": "string", + "IconUrl": "string" + }, + "RemoteEndPoint": "string", + "PlayableMediaTypes": ["string"], + "Id": "string", + "UserId": "08ba1929-681e-4b24-929b-9245852f65c0", + "UserName": "string", + "Client": "string", + "LastActivityDate": "2019-08-24T14:15:22Z", + "LastPlaybackCheckIn": "2019-08-24T14:15:22Z", + "DeviceName": "string", + "DeviceType": "string", + "NowPlayingItem": { + "Name": "string", + "OriginalTitle": "string", + "ServerId": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "Etag": "string", + "SourceType": "string", + "PlaylistItemId": "string", + "DateCreated": "2019-08-24T14:15:22Z", + "DateLastMediaAdded": "2019-08-24T14:15:22Z", + "ExtraType": "string", + "AirsBeforeSeasonNumber": 0, + "AirsAfterSeasonNumber": 0, + "AirsBeforeEpisodeNumber": 0, + "CanDelete": true, + "CanDownload": true, + "HasSubtitles": true, + "PreferredMetadataLanguage": "string", + "PreferredMetadataCountryCode": "string", + "SupportsSync": true, + "Container": "string", + "SortName": "string", + "ForcedSortName": "string", + "Video3DFormat": "HalfSideBySide", + "PremiereDate": "2019-08-24T14:15:22Z", + "ExternalUrls": [ + { + "Name": "string", + "Url": "string" + } + ], + "MediaSources": [ + { + "Protocol": "File", + "Id": "string", + "Path": "string", + "EncoderPath": "string", + "EncoderProtocol": "File", + "Type": "Default", + "Container": "string", + "Size": 0, + "Name": "string", + "IsRemote": true, + "ETag": "string", + "RunTimeTicks": 0, + "ReadAtNativeFramerate": true, + "IgnoreDts": true, + "IgnoreIndex": true, + "GenPtsInput": true, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "IsInfiniteStream": true, + "RequiresOpening": true, + "OpenToken": "string", + "RequiresClosing": true, + "LiveStreamId": "string", + "BufferMs": 0, + "RequiresLooping": true, + "SupportsProbing": true, + "VideoType": "VideoFile", + "IsoType": "Dvd", + "Video3DFormat": "HalfSideBySide", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "MediaAttachments": [ + { + "Codec": "string", + "CodecTag": "string", + "Comment": "string", + "Index": 0, + "FileName": "string", + "MimeType": "string", + "DeliveryUrl": "string" + } + ], + "Formats": ["string"], + "Bitrate": 0, + "Timestamp": "None", + "RequiredHttpHeaders": { + "property1": "string", + "property2": "string" + }, + "TranscodingUrl": "string", + "TranscodingSubProtocol": "string", + "TranscodingContainer": "string", + "AnalyzeDurationMs": 0, + "DefaultAudioStreamIndex": 0, + "DefaultSubtitleStreamIndex": 0 + } + ], + "CriticRating": 0, + "ProductionLocations": ["string"], + "Path": "string", + "EnableMediaSourceDisplay": true, + "OfficialRating": "string", + "CustomRating": "string", + "ChannelId": "04b0b2a5-93cb-474d-8ea9-3df0f84eb0ff", + "ChannelName": "string", + "Overview": "string", + "Taglines": ["string"], + "Genres": ["string"], + "CommunityRating": 0, + "CumulativeRunTimeTicks": 0, + "RunTimeTicks": 0, + "PlayAccess": "Full", + "AspectRatio": "string", + "ProductionYear": 0, + "IsPlaceHolder": true, + "Number": "string", + "ChannelNumber": "string", + "IndexNumber": 0, + "IndexNumberEnd": 0, + "ParentIndexNumber": 0, + "RemoteTrailers": [ + { + "Url": "string", + "Name": "string" + } + ], + "ProviderIds": { + "property1": "string", + "property2": "string" + }, + "IsHD": true, + "IsFolder": true, + "ParentId": "c54e2d15-b5eb-48b7-9b04-53f376904b1e", + "Type": "AggregateFolder", + "People": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "Role": "string", + "Type": "string", + "PrimaryImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + } + } + ], + "Studios": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "GenreItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "ParentLogoItemId": "c78d400f-de5c-421e-8714-4fb05d387233", + "ParentBackdropItemId": "c22fd826-17fc-44f4-9b04-1eb3e8fb9173", + "ParentBackdropImageTags": ["string"], + "LocalTrailerCount": 0, + "UserData": { + "Rating": 0, + "PlayedPercentage": 0, + "UnplayedItemCount": 0, + "PlaybackPositionTicks": 0, + "PlayCount": 0, + "IsFavorite": true, + "Likes": true, + "LastPlayedDate": "2019-08-24T14:15:22Z", + "Played": true, + "Key": "string", + "ItemId": "string" + }, + "RecursiveItemCount": 0, + "ChildCount": 0, + "SeriesName": "string", + "SeriesId": "c7b70af4-4902-4a7e-95ab-28349b6c7afc", + "SeasonId": "badb6463-e5b7-45c5-8141-71204420ec8f", + "SpecialFeatureCount": 0, + "DisplayPreferencesId": "string", + "Status": "string", + "AirTime": "string", + "AirDays": ["Sunday"], + "Tags": ["string"], + "PrimaryImageAspectRatio": 0, + "Artists": ["string"], + "ArtistItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "Album": "string", + "CollectionType": "string", + "DisplayOrder": "string", + "AlbumId": "21af9851-8e39-43a9-9c47-513d3b9e99fc", + "AlbumPrimaryImageTag": "string", + "SeriesPrimaryImageTag": "string", + "AlbumArtist": "string", + "AlbumArtists": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "SeasonName": "string", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "VideoType": "VideoFile", + "PartCount": 0, + "MediaSourceCount": 0, + "ImageTags": { + "property1": "string", + "property2": "string" + }, + "BackdropImageTags": ["string"], + "ScreenshotImageTags": ["string"], + "ParentLogoImageTag": "string", + "ParentArtItemId": "10c1875b-b82c-48e8-bae9-939a5e68dc2f", + "ParentArtImageTag": "string", + "SeriesThumbImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + }, + "SeriesStudio": "string", + "ParentThumbItemId": "ae6ff707-333d-4994-be6d-b83ca1b35f46", + "ParentThumbImageTag": "string", + "ParentPrimaryImageItemId": "string", + "ParentPrimaryImageTag": "string", + "Chapters": [ + { + "StartPositionTicks": 0, + "Name": "string", + "ImagePath": "string", + "ImageDateModified": "2019-08-24T14:15:22Z", + "ImageTag": "string" + } + ], + "LocationType": "FileSystem", + "IsoType": "Dvd", + "MediaType": "string", + "EndDate": "2019-08-24T14:15:22Z", + "LockedFields": ["Cast"], + "TrailerCount": 0, + "MovieCount": 0, + "SeriesCount": 0, + "ProgramCount": 0, + "EpisodeCount": 0, + "SongCount": 0, + "AlbumCount": 0, + "ArtistCount": 0, + "MusicVideoCount": 0, + "LockData": true, + "Width": 0, + "Height": 0, + "CameraMake": "string", + "CameraModel": "string", + "Software": "string", + "ExposureTime": 0, + "FocalLength": 0, + "ImageOrientation": "TopLeft", + "Aperture": 0, + "ShutterSpeed": 0, + "Latitude": 0, + "Longitude": 0, + "Altitude": 0, + "IsoSpeedRating": 0, + "SeriesTimerId": "string", + "ProgramId": "string", + "ChannelPrimaryImageTag": "string", + "StartDate": "2019-08-24T14:15:22Z", + "CompletionPercentage": 0, + "IsRepeat": true, + "EpisodeTitle": "string", + "ChannelType": "TV", + "Audio": "Mono", + "IsMovie": true, + "IsSports": true, + "IsSeries": true, + "IsLive": true, + "IsNews": true, + "IsKids": true, + "IsPremiere": true, + "TimerId": "string", + "CurrentProgram": {} + }, + "FullNowPlayingItem": { + "Size": 0, + "Container": "string", + "IsHD": true, + "IsShortcut": true, + "ShortcutPath": "string", + "Width": 0, + "Height": 0, + "ExtraIds": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "DateLastSaved": "2019-08-24T14:15:22Z", + "RemoteTrailers": [ + { + "Url": "string", + "Name": "string" + } + ], + "SupportsExternalTransfer": true + }, + "NowViewingItem": { + "Name": "string", + "OriginalTitle": "string", + "ServerId": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "Etag": "string", + "SourceType": "string", + "PlaylistItemId": "string", + "DateCreated": "2019-08-24T14:15:22Z", + "DateLastMediaAdded": "2019-08-24T14:15:22Z", + "ExtraType": "string", + "AirsBeforeSeasonNumber": 0, + "AirsAfterSeasonNumber": 0, + "AirsBeforeEpisodeNumber": 0, + "CanDelete": true, + "CanDownload": true, + "HasSubtitles": true, + "PreferredMetadataLanguage": "string", + "PreferredMetadataCountryCode": "string", + "SupportsSync": true, + "Container": "string", + "SortName": "string", + "ForcedSortName": "string", + "Video3DFormat": "HalfSideBySide", + "PremiereDate": "2019-08-24T14:15:22Z", + "ExternalUrls": [ + { + "Name": "string", + "Url": "string" + } + ], + "MediaSources": [ + { + "Protocol": "File", + "Id": "string", + "Path": "string", + "EncoderPath": "string", + "EncoderProtocol": "File", + "Type": "Default", + "Container": "string", + "Size": 0, + "Name": "string", + "IsRemote": true, + "ETag": "string", + "RunTimeTicks": 0, + "ReadAtNativeFramerate": true, + "IgnoreDts": true, + "IgnoreIndex": true, + "GenPtsInput": true, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "IsInfiniteStream": true, + "RequiresOpening": true, + "OpenToken": "string", + "RequiresClosing": true, + "LiveStreamId": "string", + "BufferMs": 0, + "RequiresLooping": true, + "SupportsProbing": true, + "VideoType": "VideoFile", + "IsoType": "Dvd", + "Video3DFormat": "HalfSideBySide", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "MediaAttachments": [ + { + "Codec": "string", + "CodecTag": "string", + "Comment": "string", + "Index": 0, + "FileName": "string", + "MimeType": "string", + "DeliveryUrl": "string" + } + ], + "Formats": ["string"], + "Bitrate": 0, + "Timestamp": "None", + "RequiredHttpHeaders": { + "property1": "string", + "property2": "string" + }, + "TranscodingUrl": "string", + "TranscodingSubProtocol": "string", + "TranscodingContainer": "string", + "AnalyzeDurationMs": 0, + "DefaultAudioStreamIndex": 0, + "DefaultSubtitleStreamIndex": 0 + } + ], + "CriticRating": 0, + "ProductionLocations": ["string"], + "Path": "string", + "EnableMediaSourceDisplay": true, + "OfficialRating": "string", + "CustomRating": "string", + "ChannelId": "04b0b2a5-93cb-474d-8ea9-3df0f84eb0ff", + "ChannelName": "string", + "Overview": "string", + "Taglines": ["string"], + "Genres": ["string"], + "CommunityRating": 0, + "CumulativeRunTimeTicks": 0, + "RunTimeTicks": 0, + "PlayAccess": "Full", + "AspectRatio": "string", + "ProductionYear": 0, + "IsPlaceHolder": true, + "Number": "string", + "ChannelNumber": "string", + "IndexNumber": 0, + "IndexNumberEnd": 0, + "ParentIndexNumber": 0, + "RemoteTrailers": [ + { + "Url": "string", + "Name": "string" + } + ], + "ProviderIds": { + "property1": "string", + "property2": "string" + }, + "IsHD": true, + "IsFolder": true, + "ParentId": "c54e2d15-b5eb-48b7-9b04-53f376904b1e", + "Type": "AggregateFolder", + "People": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "Role": "string", + "Type": "string", + "PrimaryImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + } + } + ], + "Studios": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "GenreItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "ParentLogoItemId": "c78d400f-de5c-421e-8714-4fb05d387233", + "ParentBackdropItemId": "c22fd826-17fc-44f4-9b04-1eb3e8fb9173", + "ParentBackdropImageTags": ["string"], + "LocalTrailerCount": 0, + "UserData": { + "Rating": 0, + "PlayedPercentage": 0, + "UnplayedItemCount": 0, + "PlaybackPositionTicks": 0, + "PlayCount": 0, + "IsFavorite": true, + "Likes": true, + "LastPlayedDate": "2019-08-24T14:15:22Z", + "Played": true, + "Key": "string", + "ItemId": "string" + }, + "RecursiveItemCount": 0, + "ChildCount": 0, + "SeriesName": "string", + "SeriesId": "c7b70af4-4902-4a7e-95ab-28349b6c7afc", + "SeasonId": "badb6463-e5b7-45c5-8141-71204420ec8f", + "SpecialFeatureCount": 0, + "DisplayPreferencesId": "string", + "Status": "string", + "AirTime": "string", + "AirDays": ["Sunday"], + "Tags": ["string"], + "PrimaryImageAspectRatio": 0, + "Artists": ["string"], + "ArtistItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "Album": "string", + "CollectionType": "string", + "DisplayOrder": "string", + "AlbumId": "21af9851-8e39-43a9-9c47-513d3b9e99fc", + "AlbumPrimaryImageTag": "string", + "SeriesPrimaryImageTag": "string", + "AlbumArtist": "string", + "AlbumArtists": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "SeasonName": "string", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "VideoType": "VideoFile", + "PartCount": 0, + "MediaSourceCount": 0, + "ImageTags": { + "property1": "string", + "property2": "string" + }, + "BackdropImageTags": ["string"], + "ScreenshotImageTags": ["string"], + "ParentLogoImageTag": "string", + "ParentArtItemId": "10c1875b-b82c-48e8-bae9-939a5e68dc2f", + "ParentArtImageTag": "string", + "SeriesThumbImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + }, + "SeriesStudio": "string", + "ParentThumbItemId": "ae6ff707-333d-4994-be6d-b83ca1b35f46", + "ParentThumbImageTag": "string", + "ParentPrimaryImageItemId": "string", + "ParentPrimaryImageTag": "string", + "Chapters": [ + { + "StartPositionTicks": 0, + "Name": "string", + "ImagePath": "string", + "ImageDateModified": "2019-08-24T14:15:22Z", + "ImageTag": "string" + } + ], + "LocationType": "FileSystem", + "IsoType": "Dvd", + "MediaType": "string", + "EndDate": "2019-08-24T14:15:22Z", + "LockedFields": ["Cast"], + "TrailerCount": 0, + "MovieCount": 0, + "SeriesCount": 0, + "ProgramCount": 0, + "EpisodeCount": 0, + "SongCount": 0, + "AlbumCount": 0, + "ArtistCount": 0, + "MusicVideoCount": 0, + "LockData": true, + "Width": 0, + "Height": 0, + "CameraMake": "string", + "CameraModel": "string", + "Software": "string", + "ExposureTime": 0, + "FocalLength": 0, + "ImageOrientation": "TopLeft", + "Aperture": 0, + "ShutterSpeed": 0, + "Latitude": 0, + "Longitude": 0, + "Altitude": 0, + "IsoSpeedRating": 0, + "SeriesTimerId": "string", + "ProgramId": "string", + "ChannelPrimaryImageTag": "string", + "StartDate": "2019-08-24T14:15:22Z", + "CompletionPercentage": 0, + "IsRepeat": true, + "EpisodeTitle": "string", + "ChannelType": "TV", + "Audio": "Mono", + "IsMovie": true, + "IsSports": true, + "IsSeries": true, + "IsLive": true, + "IsNews": true, + "IsKids": true, + "IsPremiere": true, + "TimerId": "string", + "CurrentProgram": {} + }, + "DeviceId": "string", + "ApplicationVersion": "string", + "TranscodingInfo": { + "AudioCodec": "string", + "VideoCodec": "string", + "Container": "string", + "IsVideoDirect": true, + "IsAudioDirect": true, + "Bitrate": 0, + "Framerate": 0, + "CompletionPercentage": 0, + "Width": 0, + "Height": 0, + "AudioChannels": 0, + "HardwareAccelerationType": "AMF", + "TranscodeReasons": "ContainerNotSupported" + }, + "IsActive": true, + "SupportsMediaControl": true, + "SupportsRemoteControl": true, + "NowPlayingQueue": [ + { + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "PlaylistItemId": "string" + } + ], + "NowPlayingQueueFullItems": [ + { + "Name": "string", + "OriginalTitle": "string", + "ServerId": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "Etag": "string", + "SourceType": "string", + "PlaylistItemId": "string", + "DateCreated": "2019-08-24T14:15:22Z", + "DateLastMediaAdded": "2019-08-24T14:15:22Z", + "ExtraType": "string", + "AirsBeforeSeasonNumber": 0, + "AirsAfterSeasonNumber": 0, + "AirsBeforeEpisodeNumber": 0, + "CanDelete": true, + "CanDownload": true, + "HasSubtitles": true, + "PreferredMetadataLanguage": "string", + "PreferredMetadataCountryCode": "string", + "SupportsSync": true, + "Container": "string", + "SortName": "string", + "ForcedSortName": "string", + "Video3DFormat": "HalfSideBySide", + "PremiereDate": "2019-08-24T14:15:22Z", + "ExternalUrls": [ + { + "Name": "string", + "Url": "string" + } + ], + "MediaSources": [ + { + "Protocol": "File", + "Id": "string", + "Path": "string", + "EncoderPath": "string", + "EncoderProtocol": "File", + "Type": "Default", + "Container": "string", + "Size": 0, + "Name": "string", + "IsRemote": true, + "ETag": "string", + "RunTimeTicks": 0, + "ReadAtNativeFramerate": true, + "IgnoreDts": true, + "IgnoreIndex": true, + "GenPtsInput": true, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "IsInfiniteStream": true, + "RequiresOpening": true, + "OpenToken": "string", + "RequiresClosing": true, + "LiveStreamId": "string", + "BufferMs": 0, + "RequiresLooping": true, + "SupportsProbing": true, + "VideoType": "VideoFile", + "IsoType": "Dvd", + "Video3DFormat": "HalfSideBySide", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "MediaAttachments": [ + { + "Codec": "string", + "CodecTag": "string", + "Comment": "string", + "Index": 0, + "FileName": "string", + "MimeType": "string", + "DeliveryUrl": "string" + } + ], + "Formats": ["string"], + "Bitrate": 0, + "Timestamp": "None", + "RequiredHttpHeaders": { + "property1": "string", + "property2": "string" + }, + "TranscodingUrl": "string", + "TranscodingSubProtocol": "string", + "TranscodingContainer": "string", + "AnalyzeDurationMs": 0, + "DefaultAudioStreamIndex": 0, + "DefaultSubtitleStreamIndex": 0 + } + ], + "CriticRating": 0, + "ProductionLocations": ["string"], + "Path": "string", + "EnableMediaSourceDisplay": true, + "OfficialRating": "string", + "CustomRating": "string", + "ChannelId": "04b0b2a5-93cb-474d-8ea9-3df0f84eb0ff", + "ChannelName": "string", + "Overview": "string", + "Taglines": ["string"], + "Genres": ["string"], + "CommunityRating": 0, + "CumulativeRunTimeTicks": 0, + "RunTimeTicks": 0, + "PlayAccess": "Full", + "AspectRatio": "string", + "ProductionYear": 0, + "IsPlaceHolder": true, + "Number": "string", + "ChannelNumber": "string", + "IndexNumber": 0, + "IndexNumberEnd": 0, + "ParentIndexNumber": 0, + "RemoteTrailers": [ + { + "Url": "string", + "Name": "string" + } + ], + "ProviderIds": { + "property1": "string", + "property2": "string" + }, + "IsHD": true, + "IsFolder": true, + "ParentId": "c54e2d15-b5eb-48b7-9b04-53f376904b1e", + "Type": "AggregateFolder", + "People": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "Role": "string", + "Type": "string", + "PrimaryImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + } + } + ], + "Studios": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "GenreItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "ParentLogoItemId": "c78d400f-de5c-421e-8714-4fb05d387233", + "ParentBackdropItemId": "c22fd826-17fc-44f4-9b04-1eb3e8fb9173", + "ParentBackdropImageTags": ["string"], + "LocalTrailerCount": 0, + "UserData": { + "Rating": 0, + "PlayedPercentage": 0, + "UnplayedItemCount": 0, + "PlaybackPositionTicks": 0, + "PlayCount": 0, + "IsFavorite": true, + "Likes": true, + "LastPlayedDate": "2019-08-24T14:15:22Z", + "Played": true, + "Key": "string", + "ItemId": "string" + }, + "RecursiveItemCount": 0, + "ChildCount": 0, + "SeriesName": "string", + "SeriesId": "c7b70af4-4902-4a7e-95ab-28349b6c7afc", + "SeasonId": "badb6463-e5b7-45c5-8141-71204420ec8f", + "SpecialFeatureCount": 0, + "DisplayPreferencesId": "string", + "Status": "string", + "AirTime": "string", + "AirDays": ["Sunday"], + "Tags": ["string"], + "PrimaryImageAspectRatio": 0, + "Artists": ["string"], + "ArtistItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "Album": "string", + "CollectionType": "string", + "DisplayOrder": "string", + "AlbumId": "21af9851-8e39-43a9-9c47-513d3b9e99fc", + "AlbumPrimaryImageTag": "string", + "SeriesPrimaryImageTag": "string", + "AlbumArtist": "string", + "AlbumArtists": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "SeasonName": "string", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "VideoType": "VideoFile", + "PartCount": 0, + "MediaSourceCount": 0, + "ImageTags": { + "property1": "string", + "property2": "string" + }, + "BackdropImageTags": ["string"], + "ScreenshotImageTags": ["string"], + "ParentLogoImageTag": "string", + "ParentArtItemId": "10c1875b-b82c-48e8-bae9-939a5e68dc2f", + "ParentArtImageTag": "string", + "SeriesThumbImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + }, + "SeriesStudio": "string", + "ParentThumbItemId": "ae6ff707-333d-4994-be6d-b83ca1b35f46", + "ParentThumbImageTag": "string", + "ParentPrimaryImageItemId": "string", + "ParentPrimaryImageTag": "string", + "Chapters": [ + { + "StartPositionTicks": 0, + "Name": "string", + "ImagePath": "string", + "ImageDateModified": "2019-08-24T14:15:22Z", + "ImageTag": "string" + } + ], + "LocationType": "FileSystem", + "IsoType": "Dvd", + "MediaType": "string", + "EndDate": "2019-08-24T14:15:22Z", + "LockedFields": ["Cast"], + "TrailerCount": 0, + "MovieCount": 0, + "SeriesCount": 0, + "ProgramCount": 0, + "EpisodeCount": 0, + "SongCount": 0, + "AlbumCount": 0, + "ArtistCount": 0, + "MusicVideoCount": 0, + "LockData": true, + "Width": 0, + "Height": 0, + "CameraMake": "string", + "CameraModel": "string", + "Software": "string", + "ExposureTime": 0, + "FocalLength": 0, + "ImageOrientation": "TopLeft", + "Aperture": 0, + "ShutterSpeed": 0, + "Latitude": 0, + "Longitude": 0, + "Altitude": 0, + "IsoSpeedRating": 0, + "SeriesTimerId": "string", + "ProgramId": "string", + "ChannelPrimaryImageTag": "string", + "StartDate": "2019-08-24T14:15:22Z", + "CompletionPercentage": 0, + "IsRepeat": true, + "EpisodeTitle": "string", + "ChannelType": "TV", + "Audio": "Mono", + "IsMovie": true, + "IsSports": true, + "IsSeries": true, + "IsLive": true, + "IsNews": true, + "IsKids": true, + "IsPremiere": true, + "TimerId": "string", + "CurrentProgram": {} + } + ], + "HasCustomDeviceName": true, + "PlaylistItemId": "string", + "ServerId": "string", + "UserPrimaryImageTag": "string", + "SupportedCommands": ["MoveUp"] + }, + "AccessToken": "string", + "ServerId": "string" +} diff --git a/tests/components/jellyfin/fixtures/get-user-settings.json b/tests/components/jellyfin/fixtures/get-user-settings.json new file mode 100644 index 00000000000..5e28f87d8f2 --- /dev/null +++ b/tests/components/jellyfin/fixtures/get-user-settings.json @@ -0,0 +1,19 @@ +{ + "Id": "string", + "ViewType": "string", + "SortBy": "string", + "IndexBy": "string", + "RememberIndexing": true, + "PrimaryImageHeight": 0, + "PrimaryImageWidth": 0, + "CustomPrefs": { + "property1": "string", + "property2": "string" + }, + "ScrollDirection": "Horizontal", + "ShowBackdrop": true, + "RememberSorting": true, + "SortOrder": "Ascending", + "ShowSidebar": true, + "Client": "emby" +} diff --git a/tests/components/jellyfin/test_config_flow.py b/tests/components/jellyfin/test_config_flow.py index be90e521ac1..9dc0fc86b5e 100644 --- a/tests/components/jellyfin/test_config_flow.py +++ b/tests/components/jellyfin/test_config_flow.py @@ -6,14 +6,8 @@ from homeassistant.components.jellyfin.const import CONF_CLIENT_DEVICE_ID, DOMAI from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant -from .const import ( - MOCK_SUCCESFUL_LOGIN_RESPONSE, - MOCK_UNSUCCESFUL_CONNECTION_STATE, - MOCK_UNSUCCESFUL_LOGIN_RESPONSE, - TEST_PASSWORD, - TEST_URL, - TEST_USERNAME, -) +from . import async_load_json_fixture +from .const import TEST_PASSWORD, TEST_URL, TEST_USERNAME from tests.common import MockConfigEntry @@ -55,7 +49,7 @@ async def test_form( await hass.async_block_till_done() assert result2["type"] == "create_entry" - assert result2["title"] == TEST_URL + assert result2["title"] == "JELLYFIN-SERVER" assert result2["data"] == { CONF_CLIENT_DEVICE_ID: "TEST-UUID", CONF_URL: TEST_URL, @@ -82,7 +76,9 @@ async def test_form_cannot_connect( assert result["type"] == "form" assert result["errors"] == {} - mock_client.auth.connect_to_address.return_value = MOCK_UNSUCCESFUL_CONNECTION_STATE + mock_client.auth.connect_to_address.return_value = await async_load_json_fixture( + hass, "auth-connect-address-failure.json" + ) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -113,7 +109,9 @@ async def test_form_invalid_auth( assert result["type"] == "form" assert result["errors"] == {} - mock_client.auth.login.return_value = MOCK_UNSUCCESFUL_LOGIN_RESPONSE + mock_client.auth.login.return_value = await async_load_json_fixture( + hass, "auth-login-failure.json" + ) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -174,7 +172,9 @@ async def test_form_persists_device_id_on_error( assert result["errors"] == {} mock_client_device_id.return_value = "TEST-UUID-1" - mock_client.auth.login.return_value = MOCK_UNSUCCESFUL_LOGIN_RESPONSE + mock_client.auth.login.return_value = await async_load_json_fixture( + hass, "auth-login-failure.json" + ) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -190,7 +190,9 @@ async def test_form_persists_device_id_on_error( assert result2["errors"] == {"base": "invalid_auth"} mock_client_device_id.return_value = "TEST-UUID-2" - mock_client.auth.login.return_value = MOCK_SUCCESFUL_LOGIN_RESPONSE + mock_client.auth.login.return_value = await async_load_json_fixture( + hass, "auth-login.json" + ) result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], From 7fae85ee8566052d000d434bbf6954e23b5c664e Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Mon, 10 Oct 2022 00:29:37 -0500 Subject: [PATCH 298/985] Add tests for Jellyfin init (#79968) --- .coveragerc | 1 - tests/components/jellyfin/conftest.py | 21 +++++++++++ tests/components/jellyfin/test_init.py | 48 ++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 tests/components/jellyfin/test_init.py diff --git a/.coveragerc b/.coveragerc index 01c36d8a836..e4d9a242604 100644 --- a/.coveragerc +++ b/.coveragerc @@ -612,7 +612,6 @@ omit = homeassistant/components/izone/__init__.py homeassistant/components/izone/climate.py homeassistant/components/izone/discovery.py - homeassistant/components/jellyfin/__init__.py homeassistant/components/jellyfin/media_source.py homeassistant/components/joaoapps_join/* homeassistant/components/juicenet/__init__.py diff --git a/tests/components/jellyfin/conftest.py b/tests/components/jellyfin/conftest.py index bd86ae925c2..4d32e3a72ef 100644 --- a/tests/components/jellyfin/conftest.py +++ b/tests/components/jellyfin/conftest.py @@ -10,7 +10,28 @@ from jellyfin_apiclient_python.configuration import Config from jellyfin_apiclient_python.connection_manager import ConnectionManager import pytest +from homeassistant.components.jellyfin.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME + from . import load_json_fixture +from .const import TEST_PASSWORD, TEST_URL, TEST_USERNAME + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Jellyfin", + domain=DOMAIN, + data={ + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + unique_id="USER-UUID", + ) @pytest.fixture diff --git a/tests/components/jellyfin/test_init.py b/tests/components/jellyfin/test_init.py new file mode 100644 index 00000000000..542be0736c7 --- /dev/null +++ b/tests/components/jellyfin/test_init.py @@ -0,0 +1,48 @@ +"""Tests for the Jellyfin integration.""" +from unittest.mock import MagicMock + +from homeassistant.components.jellyfin.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import async_load_json_fixture + +from tests.common import MockConfigEntry + + +async def test_config_entry_not_ready( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_jellyfin: MagicMock, + mock_client: MagicMock, +) -> None: + """Test the Jellyfin configuration entry not ready.""" + mock_client.auth.connect_to_address.return_value = await async_load_json_fixture( + hass, + "auth-connect-address-failure.json", + ) + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_jellyfin: MagicMock, +) -> None: + """Test the Jellyfin configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.entry_id in hass.data[DOMAIN] + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.entry_id not in hass.data[DOMAIN] + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED From 575501d26ad867db5ebc1511504253176b7c8547 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 10 Oct 2022 09:28:36 +0200 Subject: [PATCH 299/985] Add select platform to LaMetric (#79803) --- homeassistant/components/lametric/const.py | 8 +- homeassistant/components/lametric/select.py | 91 +++++++++++++++++++ .../components/lametric/strings.select.json | 8 ++ .../lametric/translations/select.en.json | 8 ++ tests/components/lametric/test_select.py | 71 +++++++++++++++ 5 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/lametric/select.py create mode 100644 homeassistant/components/lametric/strings.select.json create mode 100644 homeassistant/components/lametric/translations/select.en.json create mode 100644 tests/components/lametric/test_select.py diff --git a/homeassistant/components/lametric/const.py b/homeassistant/components/lametric/const.py index 6a3df3b54f1..ecaed7b833c 100644 --- a/homeassistant/components/lametric/const.py +++ b/homeassistant/components/lametric/const.py @@ -7,7 +7,13 @@ from typing import Final from homeassistant.const import Platform DOMAIN: Final = "lametric" -PLATFORMS = [Platform.BUTTON, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [ + Platform.BUTTON, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, +] LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/lametric/select.py b/homeassistant/components/lametric/select.py new file mode 100644 index 00000000000..fccb6a3f771 --- /dev/null +++ b/homeassistant/components/lametric/select.py @@ -0,0 +1,91 @@ +"""Support for LaMetric selects.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any + +from demetriek import BrightnessMode, Device, LaMetricDevice + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import LaMetricDataUpdateCoordinator +from .entity import LaMetricEntity + + +@dataclass +class LaMetricEntityDescriptionMixin: + """Mixin values for LaMetric entities.""" + + options: list[str] + current_fn: Callable[[Device], str] + select_fn: Callable[[LaMetricDevice, str], Awaitable[Any]] + + +@dataclass +class LaMetricSelectEntityDescription( + SelectEntityDescription, LaMetricEntityDescriptionMixin +): + """Class describing LaMetric select entities.""" + + +SELECTS = [ + LaMetricSelectEntityDescription( + key="brightness_mode", + name="Brightness mode", + icon="mdi:brightness-auto", + entity_category=EntityCategory.CONFIG, + device_class="lametric__brightness_mode", + options=["auto", "manual"], + current_fn=lambda device: device.display.brightness_mode.value, + select_fn=lambda api, opt: api.display(brightness_mode=BrightnessMode(opt)), + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up LaMetric select based on a config entry.""" + coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + LaMetricSelectEntity( + coordinator=coordinator, + description=description, + ) + for description in SELECTS + ) + + +class LaMetricSelectEntity(LaMetricEntity, SelectEntity): + """Representation of a LaMetric select.""" + + entity_description: LaMetricSelectEntityDescription + + def __init__( + self, + coordinator: LaMetricDataUpdateCoordinator, + description: LaMetricSelectEntityDescription, + ) -> None: + """Initiate LaMetric Select.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_options = description.options + self._attr_unique_id = f"{coordinator.data.serial_number}-{description.key}" + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + return self.entity_description.current_fn(self.coordinator.data) + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + await self.entity_description.select_fn(self.coordinator.lametric, option) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/lametric/strings.select.json b/homeassistant/components/lametric/strings.select.json new file mode 100644 index 00000000000..1d2ce0a2ce7 --- /dev/null +++ b/homeassistant/components/lametric/strings.select.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "Automatic", + "manual": "Manual" + } + } +} diff --git a/homeassistant/components/lametric/translations/select.en.json b/homeassistant/components/lametric/translations/select.en.json new file mode 100644 index 00000000000..de1f7e5f642 --- /dev/null +++ b/homeassistant/components/lametric/translations/select.en.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "Automatic", + "manual": "Manual" + } + } +} \ No newline at end of file diff --git a/tests/components/lametric/test_select.py b/tests/components/lametric/test_select.py new file mode 100644 index 00000000000..8d9394b9068 --- /dev/null +++ b/tests/components/lametric/test_select.py @@ -0,0 +1,71 @@ +"""Tests for the LaMetric select platform.""" +from unittest.mock import MagicMock + +from demetriek import BrightnessMode + +from homeassistant.components.lametric.const import DOMAIN +from homeassistant.components.select import ( + ATTR_OPTIONS, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_OPTION, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory + +from tests.common import MockConfigEntry + + +async def test_brightness_mode( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_lametric: MagicMock, +) -> None: + """Test the LaMetric brightness mode controls.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + state = hass.states.get("select.frenck_s_lametric_brightness_mode") + assert state + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's LaMetric Brightness mode" + ) + assert state.attributes.get(ATTR_ICON) == "mdi:brightness-auto" + assert state.attributes.get(ATTR_OPTIONS) == ["auto", "manual"] + assert state.state == BrightnessMode.AUTO + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry.device_id + assert entry.entity_category is EntityCategory.CONFIG + assert entry.unique_id == "SA110405124500W00BS9-brightness_mode" + + device = device_registry.async_get(entry.device_id) + assert device + assert device.configuration_url is None + assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")} + assert device.entry_type is None + assert device.hw_version is None + assert device.identifiers == {(DOMAIN, "SA110405124500W00BS9")} + assert device.manufacturer == "LaMetric Inc." + assert device.name == "Frenck's LaMetric" + assert device.sw_version == "2.2.2" + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.frenck_s_lametric_brightness_mode", + ATTR_OPTION: "manual", + }, + blocking=True, + ) + + assert len(mock_lametric.display.mock_calls) == 1 + mock_lametric.display.assert_called_once_with(brightness_mode=BrightnessMode.MANUAL) From 881c2a4956151cfb59c276ad80f2be3123847003 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Oct 2022 10:02:19 +0200 Subject: [PATCH 300/985] Bump actions/stale from 6.0.0 to 6.0.1 (#79977) Bumps [actions/stale](https://github.com/actions/stale) from 6.0.0 to 6.0.1. - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/v6.0.0...v6.0.1) --- updated-dependencies: - dependency-name: actions/stale dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index f6f7d24fff1..914fa415051 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,7 +17,7 @@ jobs: # - No PRs marked as no-stale # - No issues marked as no-stale or help-wanted - name: 90 days stale issues & PRs policy - uses: actions/stale@v6.0.0 + uses: actions/stale@v6.0.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 90 @@ -54,7 +54,7 @@ jobs: # - No PRs marked as no-stale or new-integrations # - No issues (-1) - name: 30 days stale PRs policy - uses: actions/stale@v6.0.0 + uses: actions/stale@v6.0.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 30 @@ -79,7 +79,7 @@ jobs: # - No Issues marked as no-stale or help-wanted # - No PRs (-1) - name: Needs more information stale issues policy - uses: actions/stale@v6.0.0 + uses: actions/stale@v6.0.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "needs-more-information" From 907af7ffe46252a4f809a779aa96cd9f64be45cf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 10 Oct 2022 11:05:28 +0200 Subject: [PATCH 301/985] Remove system marker from Supervisor integration (#79997) --- homeassistant/components/hassio/manifest.json | 3 +-- homeassistant/generated/integrations.json | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index b087eb25807..5de80fdbd19 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -6,6 +6,5 @@ "after_dependencies": ["panel_custom"], "codeowners": ["@home-assistant/supervisor"], "iot_class": "local_polling", - "quality_scale": "internal", - "integration_type": "system" + "quality_scale": "internal" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index f09e30f4a93..1973933d735 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1700,6 +1700,11 @@ "iot_class": "local_polling", "name": "Harman Kardon AVR" }, + "hassio": { + "config_flow": false, + "iot_class": "local_polling", + "name": "Home Assistant Supervisor" + }, "haveibeenpwned": { "config_flow": false, "iot_class": "cloud_polling", From 1744b5fa0a81a64926680f82b52ab3c2c6470628 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Oct 2022 12:38:10 +0200 Subject: [PATCH 302/985] Add docstring to Sensor enums (#79983) * Add docstring to Sensor enums * Adjust MONETARY docstring --- homeassistant/components/sensor/__init__.py | 209 ++++++++++++++++---- 1 file changed, 170 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 06071eeddbe..9ebf225c637 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -85,116 +85,243 @@ SCAN_INTERVAL: Final = timedelta(seconds=30) class SensorDeviceClass(StrEnum): """Device class for sensors.""" - # apparent power (VA) APPARENT_POWER = "apparent_power" + """Apparent power. + + Unit of measurement: `VA` + """ - # Air Quality Index AQI = "aqi" + """Air Quality Index. + + Unit of measurement: `None` + """ - # % of battery that is left BATTERY = "battery" + """Percentage of battery that is left. + + Unit of measurement: `%` + """ - # ppm (parts per million) Carbon Monoxide gas concentration CO = "carbon_monoxide" + """Carbon Monoxide gas concentration. + + Unit of measurement: `ppm` (parts per million) + """ - # ppm (parts per million) Carbon Dioxide gas concentration CO2 = "carbon_dioxide" + """Carbon Dioxide gas concentration. + + Unit of measurement: `ppm` (parts per million) + """ - # current (A) CURRENT = "current" + """Current. + + Unit of measurement: `A` + """ - # date (ISO8601) DATE = "date" + """Date. + + Unit of measurement: `None` + + ISO8601 format: https://en.wikipedia.org/wiki/ISO_8601 + """ - # distance (LENGTH_*) DISTANCE = "distance" + """Generic distance. + + Unit of measurement: `LENGTH_*` units + - SI /metric: `mm`, `cm`, `m`, `km` + - USCS / imperial: `in`, `ft`, `yd`, `mi` + """ - # fixed duration (TIME_DAYS, TIME_HOURS, TIME_MINUTES, TIME_SECONDS) DURATION = "duration" + """Fixed duration. + + Unit of measurement: `d`, `h`, `min`, `s` + """ - # energy (Wh, kWh, MWh) ENERGY = "energy" + """Energy. + + Unit of measurement: `Wh`, `kWh`, `MWh` + """ - # frequency (Hz, kHz, MHz, GHz) FREQUENCY = "frequency" + """Frequency. + + Unit of measurement: `Hz`, `kHz`, `MHz`, `GHz` + """ - # gas (m³ or ft³) GAS = "gas" + """Gas. + + Unit of measurement: `m³`, `ft³` + """ - # Relative humidity (%) HUMIDITY = "humidity" + """Relative humidity. + + Unit of measurement: `%` + """ - # current light level (lx/lm) ILLUMINANCE = "illuminance" + """Illuminance. + + Unit of measurement: `lx`, `lm` + """ - # moisture (%) MOISTURE = "moisture" + """Moisture. + + Unit of measurement: `%` + """ - # Amount of money (currency) MONETARY = "monetary" + """Amount of money. + + Unit of measurement: ISO4217 currency code + + See https://en.wikipedia.org/wiki/ISO_4217#Active_codes for active codes + """ - # Amount of NO2 (µg/m³) NITROGEN_DIOXIDE = "nitrogen_dioxide" + """Amount of NO2. + + Unit of measurement: `µg/m³` + """ - # Amount of NO (µg/m³) NITROGEN_MONOXIDE = "nitrogen_monoxide" + """Amount of NO. + + Unit of measurement: `µg/m³` + """ - # Amount of N2O (µg/m³) NITROUS_OXIDE = "nitrous_oxide" + """Amount of N2O. + + Unit of measurement: `µg/m³` + """ - # Amount of O3 (µg/m³) OZONE = "ozone" + """Amount of O3. + + Unit of measurement: `µg/m³` + """ - # Particulate matter <= 0.1 μm (µg/m³) PM1 = "pm1" + """Particulate matter <= 0.1 μm. + + Unit of measurement: `µg/m³` + """ - # Particulate matter <= 10 μm (µg/m³) PM10 = "pm10" + """Particulate matter <= 10 μm. + + Unit of measurement: `µg/m³` + """ - # Particulate matter <= 2.5 μm (µg/m³) PM25 = "pm25" + """Particulate matter <= 2.5 μm. + + Unit of measurement: `µg/m³` + """ - # power factor (%) POWER_FACTOR = "power_factor" + """Power factor. + + Unit of measurement: `%` + """ - # power (W/kW) POWER = "power" + """Power. + + Unit of measurement: `W`, `kW` + """ - # pressure (hPa/mbar) PRESSURE = "pressure" + """Pressure. + + Unit of measurement: + - `mbar`, `cbar`, `bar` + - `Pa`, `hPa`, `kPa` + - `inHg` + - `psi` + """ - # reactive power (var) REACTIVE_POWER = "reactive_power" + """Reactive power. + + Unit of measurement: `var` + """ - # signal strength (dB/dBm) SIGNAL_STRENGTH = "signal_strength" + """Signal strength. + + Unit of measurement: `dB`, `dBm` + """ - # speed (SPEED_*) SPEED = "speed" + """Generic speed. + + Unit of measurement: `SPEED_*` units + - SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h` + - USCS / imperial: `in/d`, `in/h`, `ft/s`, `mph` + - Nautical: `kn` + """ - # Amount of SO2 (µg/m³) SULPHUR_DIOXIDE = "sulphur_dioxide" + """Amount of SO2. + + Unit of measurement: `µg/m³` + """ - # temperature (C/F) TEMPERATURE = "temperature" + """Temperature. + + Unit of measurement: `°C`, `°F` + """ - # timestamp (ISO8601) TIMESTAMP = "timestamp" + """Timestamp. + + Unit of measurement: `None` + + ISO8601 format: https://en.wikipedia.org/wiki/ISO_8601 + """ - # Amount of VOC (µg/m³) VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" + """Amount of VOC. + + Unit of measurement: `µg/m³` + """ - # voltage (V) VOLTAGE = "voltage" + """Voltage. + + Unit of measurement: `V` + """ - # volume (VOLUME_*) VOLUME = "volume" + """Generic volume. + + Unit of measurement: `VOLUME_*` units + - SI / metric: `mL`, `L`, `m³` + - USCS / imperial: `fl. oz.`, `gal`, `ft³` (warning: volumes expressed in + USCS/imperial units are currently assumed to be US volumes) + """ # weight/mass (g, kg, mg, µg, oz, lb) WEIGHT = "weight" - """Represent a measurement of an object's mass. + """Generic weight, represents a measurement of an object's mass. Weight is used instead of mass to fit with every day language. + + Unit of measurement: `MASS_*` units + - SI / metric: `µg`, `mg`, `g`, `kg` + - USCS / imperial: `oz`, `lb` """ @@ -208,14 +335,18 @@ DEVICE_CLASSES: Final[list[str]] = [cls.value for cls in SensorDeviceClass] class SensorStateClass(StrEnum): """State class for sensors.""" - # The state represents a measurement in present time MEASUREMENT = "measurement" + """The state represents a measurement in present time.""" - # The state represents a total amount, e.g. net energy consumption TOTAL = "total" + """The state represents a total amount. + + For example: net energy consumption""" - # The state represents a monotonically increasing total, e.g. an amount of consumed gas TOTAL_INCREASING = "total_increasing" + """The state represents a monotonically increasing total. + + For example: an amount of consumed gas""" STATE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(SensorStateClass)) From 8fe504356a8c2dcabad80a1dae54aea8e8edd732 Mon Sep 17 00:00:00 2001 From: Michael Molisani Date: Mon, 10 Oct 2022 04:37:02 -0700 Subject: [PATCH 303/985] Update to pygtfs 0.1.7 (#79975) --- homeassistant/components/gtfs/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/gtfs/manifest.json b/homeassistant/components/gtfs/manifest.json index 8dfb37ad551..9e9eb6a5585 100644 --- a/homeassistant/components/gtfs/manifest.json +++ b/homeassistant/components/gtfs/manifest.json @@ -2,7 +2,7 @@ "domain": "gtfs", "name": "General Transit Feed Specification (GTFS)", "documentation": "https://www.home-assistant.io/integrations/gtfs", - "requirements": ["pygtfs==0.1.6"], + "requirements": ["pygtfs==0.1.7"], "codeowners": [], "iot_class": "local_polling", "loggers": ["pygtfs"] diff --git a/requirements_all.txt b/requirements_all.txt index ed5017827d3..dec5140ffbc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1589,7 +1589,7 @@ pyfttt==0.3 pygatt[GATTTOOL]==4.0.5 # homeassistant.components.gtfs -pygtfs==0.1.6 +pygtfs==0.1.7 # homeassistant.components.hvv_departures pygti==0.9.3 From ffb6434776bb6b675bcd3a6983a1a0756e12aa67 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 10 Oct 2022 08:11:55 -0400 Subject: [PATCH 304/985] Add load_url service to fully_kiosk integration (#79969) --- .../components/fully_kiosk/__init__.py | 3 ++ homeassistant/components/fully_kiosk/const.py | 4 ++ .../components/fully_kiosk/services.py | 41 +++++++++++++++++++ .../components/fully_kiosk/services.yaml | 14 +++++++ tests/components/fully_kiosk/test_services.py | 36 ++++++++++++++++ 5 files changed, 98 insertions(+) create mode 100644 homeassistant/components/fully_kiosk/services.py create mode 100644 homeassistant/components/fully_kiosk/services.yaml create mode 100644 tests/components/fully_kiosk/test_services.py diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py index 86ab769e0ec..e417d7c0bcb 100644 --- a/homeassistant/components/fully_kiosk/__init__.py +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -5,6 +5,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import FullyKioskDataUpdateCoordinator +from .services import async_setup_services PLATFORMS = [ Platform.BINARY_SENSOR, @@ -26,6 +27,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await async_setup_services(hass) + return True diff --git a/homeassistant/components/fully_kiosk/const.py b/homeassistant/components/fully_kiosk/const.py index 4af7628ed63..b722d7fb4ca 100644 --- a/homeassistant/components/fully_kiosk/const.py +++ b/homeassistant/components/fully_kiosk/const.py @@ -22,3 +22,7 @@ MEDIA_SUPPORT_FULLYKIOSK = ( | MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.BROWSE_MEDIA ) + +SERVICE_LOAD_URL = "load_url" + +ATTR_URL = "url" diff --git a/homeassistant/components/fully_kiosk/services.py b/homeassistant/components/fully_kiosk/services.py new file mode 100644 index 00000000000..3d7d564f0b2 --- /dev/null +++ b/homeassistant/components/fully_kiosk/services.py @@ -0,0 +1,41 @@ +"""Services for the Fully Kiosk Browser integration.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.core import HomeAssistant, ServiceCall +import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.device_registry as dr + +from .const import ATTR_URL, DOMAIN, SERVICE_LOAD_URL + + +async def async_setup_services(hass: HomeAssistant) -> None: + """Set up the services for the Fully Kiosk Browser integration.""" + + async def async_load_url(call: ServiceCall) -> None: + """Load a URL on the Fully Kiosk Browser.""" + registry = dr.async_get(hass) + for target in call.data[ATTR_DEVICE_ID]: + + device = registry.async_get(target) + if device: + coordinator = hass.data[DOMAIN][list(device.config_entries)[0]] + await coordinator.fully.loadUrl(call.data[ATTR_URL]) + + hass.services.async_register( + DOMAIN, + SERVICE_LOAD_URL, + async_load_url, + schema=vol.Schema( + vol.All( + { + vol.Required(ATTR_DEVICE_ID): cv.ensure_list, + vol.Required( + ATTR_URL, + ): cv.string, + }, + ) + ), + ) diff --git a/homeassistant/components/fully_kiosk/services.yaml b/homeassistant/components/fully_kiosk/services.yaml new file mode 100644 index 00000000000..53ce0a8aec8 --- /dev/null +++ b/homeassistant/components/fully_kiosk/services.yaml @@ -0,0 +1,14 @@ +load_url: + name: Load URL + description: Load a URL on Fully Kiosk Browser + target: + device: + integration: fully_kiosk + fields: + url: + name: URL + description: URL to load. + example: "https://home-assistant.io" + required: true + selector: + text: diff --git a/tests/components/fully_kiosk/test_services.py b/tests/components/fully_kiosk/test_services.py new file mode 100644 index 00000000000..e3b63dad341 --- /dev/null +++ b/tests/components/fully_kiosk/test_services.py @@ -0,0 +1,36 @@ +"""Test Fully Kiosk Browser services.""" +from unittest.mock import MagicMock + +from homeassistant.components.fully_kiosk.const import ( + ATTR_URL, + DOMAIN, + SERVICE_LOAD_URL, +) +from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from tests.common import MockConfigEntry + + +async def test_services( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test the Fully Kiosk Browser services.""" + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, "abcdef-123456")} + ) + + assert device_entry + + await hass.services.async_call( + DOMAIN, + SERVICE_LOAD_URL, + {ATTR_DEVICE_ID: [device_entry.id], ATTR_URL: "https://example.com"}, + blocking=True, + ) + + assert len(mock_fully_kiosk.loadUrl.mock_calls) == 1 From c7b56f4079ac89c3d0d3d152be20d9840a08b5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Flemming=20S=C3=B8rvollen=20Skaret?= Date: Mon, 10 Oct 2022 14:12:37 +0200 Subject: [PATCH 305/985] Clean duplicate nextcloud sensor (#79900) Update __init__.py Removed duplicate of "nextcloud_database_version" --- homeassistant/components/nextcloud/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py index 48f0c330632..6ca6096458e 100644 --- a/homeassistant/components/nextcloud/__init__.py +++ b/homeassistant/components/nextcloud/__init__.py @@ -85,7 +85,6 @@ SENSORS = ( "nextcloud_server_php_upload_max_filesize", "nextcloud_database_type", "nextcloud_database_version", - "nextcloud_database_version", "nextcloud_activeUsers_last5minutes", "nextcloud_activeUsers_last1hour", "nextcloud_activeUsers_last24hours", From d0bffb6c75def1caaada9b0c4a8b279f6d811a74 Mon Sep 17 00:00:00 2001 From: Patrick ZAJDA Date: Mon, 10 Oct 2022 14:12:50 +0200 Subject: [PATCH 306/985] Migrate Switchbot to new entity naming style (#80008) Co-authored-by: Franck Nijhof --- homeassistant/components/switchbot/binary_sensor.py | 2 -- homeassistant/components/switchbot/entity.py | 2 +- homeassistant/components/switchbot/sensor.py | 8 ++++++-- tests/components/switchbot/test_sensor.py | 6 ++++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index a5378028264..296cd4c1800 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -62,8 +62,6 @@ async def async_setup_entry( class SwitchBotBinarySensor(SwitchbotEntity, BinarySensorEntity): """Representation of a Switchbot binary sensor.""" - _attr_has_entity_name = True - def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, diff --git a/homeassistant/components/switchbot/entity.py b/homeassistant/components/switchbot/entity.py index b8d08e74f5f..fcf0bdc4da2 100644 --- a/homeassistant/components/switchbot/entity.py +++ b/homeassistant/components/switchbot/entity.py @@ -24,6 +24,7 @@ class SwitchbotEntity(PassiveBluetoothCoordinatorEntity): coordinator: SwitchbotDataUpdateCoordinator _device: SwitchbotDevice + _attr_has_entity_name = True def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: """Initialize the entity.""" @@ -32,7 +33,6 @@ class SwitchbotEntity(PassiveBluetoothCoordinatorEntity): self._last_run_success: bool | None = None self._address = coordinator.ble_device.address self._attr_unique_id = coordinator.base_unique_id - self._attr_name = coordinator.device_name self._attr_device_info = DeviceInfo( connections={(dr.CONNECTION_BLUETOOTH, self._address)}, manufacturer=MANUFACTURER, diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index 8e5d0e92d5a..1077fd4fce6 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -26,6 +26,7 @@ PARALLEL_UPDATES = 0 SENSOR_TYPES: dict[str, SensorEntityDescription] = { "rssi": SensorEntityDescription( key="rssi", + name="Bluetooth signal strength", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, @@ -34,6 +35,7 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { ), "wifi_rssi": SensorEntityDescription( key="wifi_rssi", + name="Wi-Fi signal strength", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, @@ -42,6 +44,7 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { ), "battery": SensorEntityDescription( key="battery", + name="Battery", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, @@ -49,18 +52,21 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { ), "lightLevel": SensorEntityDescription( key="lightLevel", + name="Light level", native_unit_of_measurement="Level", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.ILLUMINANCE, ), "humidity": SensorEntityDescription( key="humidity", + name="Humidity", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.HUMIDITY, ), "temperature": SensorEntityDescription( key="temperature", + name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, @@ -97,8 +103,6 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity): super().__init__(coordinator) self._sensor = sensor self._attr_unique_id = f"{coordinator.base_unique_id}-{sensor}" - name = coordinator.device_name - self._attr_name = f"{name} {sensor.replace('_', ' ').title()}" self.entity_description = SENSOR_TYPES[sensor] @property diff --git a/tests/components/switchbot/test_sensor.py b/tests/components/switchbot/test_sensor.py index ae77c5a8de4..9de1403a634 100644 --- a/tests/components/switchbot/test_sensor.py +++ b/tests/components/switchbot/test_sensor.py @@ -48,10 +48,12 @@ async def test_sensors(hass, entity_registry_enabled_by_default): assert battery_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "%" assert battery_sensor_attrs[ATTR_STATE_CLASS] == "measurement" - rssi_sensor = hass.states.get("sensor.test_name_rssi") + rssi_sensor = hass.states.get("sensor.test_name_bluetooth_signal_strength") rssi_sensor_attrs = rssi_sensor.attributes assert rssi_sensor.state == "-60" - assert rssi_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Rssi" + assert ( + rssi_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Bluetooth signal strength" + ) assert rssi_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "dBm" assert await hass.config_entries.async_unload(entry.entry_id) From f8f4b059a158b870edd568f52635b172beeaae86 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 10 Oct 2022 14:19:09 +0200 Subject: [PATCH 307/985] Update black to 22.10.0 (#80006) --- .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 1635a7dcf12..6c57b9de849 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 22.10.0 hooks: - id: black args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 1bb2e0a70e4..51789f48ca5 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,7 +1,7 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.7.4 -black==22.8.0 +black==22.10.0 codespell==2.1.0 flake8-comprehensions==3.10.0 flake8-docstrings==1.6.0 From ca9bfc8b861a0987fd097ee5d807fa40427077ff Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Oct 2022 14:20:04 +0200 Subject: [PATCH 308/985] Add options to SelectEntityDescription (#78882) --- homeassistant/components/kostal_plenticore/select.py | 3 +-- homeassistant/components/lametric/select.py | 2 -- homeassistant/components/overkiz/select.py | 6 ------ homeassistant/components/renault/select.py | 6 ------ homeassistant/components/select/__init__.py | 11 ++++++++++- homeassistant/components/xiaomi_miio/select.py | 8 +++----- 6 files changed, 14 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/select.py b/homeassistant/components/kostal_plenticore/select.py index 3a2d3445a84..5f0bb8100c7 100644 --- a/homeassistant/components/kostal_plenticore/select.py +++ b/homeassistant/components/kostal_plenticore/select.py @@ -23,7 +23,6 @@ class PlenticoreRequiredKeysMixin: """A class that describes required properties for plenticore select entities.""" module_id: str - options: list[str] @dataclass @@ -65,6 +64,7 @@ async def async_setup_entry( entities = [] for description in SELECT_SETTINGS_DATA: + assert description.options is not None if description.module_id not in available_settings_data: continue needed_data_ids = { @@ -109,7 +109,6 @@ class PlenticoreDataSelect(CoordinatorEntity, SelectEntity): self.platform_name = platform_name self.module_id = description.module_id self.data_id = description.key - self._attr_options = description.options self._device_info = device_info self._attr_unique_id = f"{entry_id}_{description.module_id}" diff --git a/homeassistant/components/lametric/select.py b/homeassistant/components/lametric/select.py index fccb6a3f771..4fcdfbaf2cb 100644 --- a/homeassistant/components/lametric/select.py +++ b/homeassistant/components/lametric/select.py @@ -22,7 +22,6 @@ from .entity import LaMetricEntity class LaMetricEntityDescriptionMixin: """Mixin values for LaMetric entities.""" - options: list[str] current_fn: Callable[[Device], str] select_fn: Callable[[LaMetricDevice, str], Awaitable[Any]] @@ -77,7 +76,6 @@ class LaMetricSelectEntity(LaMetricEntity, SelectEntity): """Initiate LaMetric Select.""" super().__init__(coordinator) self.entity_description = description - self._attr_options = description.options self._attr_unique_id = f"{coordinator.data.serial_number}-{description.key}" @property diff --git a/homeassistant/components/overkiz/select.py b/homeassistant/components/overkiz/select.py index 3b40eccfbf6..6460e87b4ee 100644 --- a/homeassistant/components/overkiz/select.py +++ b/homeassistant/components/overkiz/select.py @@ -21,7 +21,6 @@ from .entity import OverkizDescriptiveEntity, OverkizDeviceClass class OverkizSelectDescriptionMixin: """Define an entity description mixin for select entities.""" - options: list[str | OverkizCommandParam] select_option: Callable[[str, Callable[..., Awaitable[None]]], Awaitable[None]] @@ -149,11 +148,6 @@ class OverkizSelect(OverkizDescriptiveEntity, SelectEntity): return None - @property - def options(self) -> list[str]: - """Return a set of selectable options.""" - return self.entity_description.options - async def async_select_option(self, option: str) -> None: """Change the selected option.""" await self.entity_description.select_option( diff --git a/homeassistant/components/renault/select.py b/homeassistant/components/renault/select.py index 9af47206e3c..2a1e207695c 100644 --- a/homeassistant/components/renault/select.py +++ b/homeassistant/components/renault/select.py @@ -24,7 +24,6 @@ class RenaultSelectRequiredKeysMixin: data_key: str icon_lambda: Callable[[RenaultSelectEntity], str] - options: list[str] @dataclass @@ -74,11 +73,6 @@ class RenaultSelectEntity( """Icon handling.""" return self.entity_description.icon_lambda(self) - @property - def options(self) -> list[str]: - """Return a set of selectable options.""" - return self.entity_description.options - async def async_select_option(self, option: str) -> None: """Change the selected option.""" await self.vehicle.vehicle.set_charge_mode(option) diff --git a/homeassistant/components/select/__init__.py b/homeassistant/components/select/__init__.py index 56ac28ae39e..20cbb86e3ae 100644 --- a/homeassistant/components/select/__init__.py +++ b/homeassistant/components/select/__init__.py @@ -72,6 +72,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class SelectEntityDescription(EntityDescription): """A class that describes select entities.""" + options: list[str] | None = None + class SelectEntity(Entity): """Representation of a Select entity.""" @@ -99,7 +101,14 @@ class SelectEntity(Entity): @property def options(self) -> list[str]: """Return a set of selectable options.""" - return self._attr_options + if hasattr(self, "_attr_options"): + return self._attr_options + if ( + hasattr(self, "entity_description") + and self.entity_description.options is not None + ): + return self.entity_description.options + raise AttributeError() @property def current_option(self) -> str | None: diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 0c573f749cd..118f3cd5c77 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -74,7 +74,6 @@ class XiaomiMiioSelectDescription(SelectEntityDescription): options_map: dict = field(default_factory=dict) set_method: str = "" set_method_error_message: str = "" - options: tuple = () class AttributeEnumMapping(NamedTuple): @@ -150,7 +149,7 @@ SELECTOR_TYPES = ( set_method_error_message="Setting the display orientation failed.", icon="mdi:tablet", device_class="xiaomi_miio__display_orientation", - options=("forward", "left", "right"), + options=["forward", "left", "right"], entity_category=EntityCategory.CONFIG, ), XiaomiMiioSelectDescription( @@ -161,7 +160,7 @@ SELECTOR_TYPES = ( set_method_error_message="Setting the led brightness failed.", icon="mdi:brightness-6", device_class="xiaomi_miio__led_brightness", - options=("bright", "dim", "off"), + options=["bright", "dim", "off"], entity_category=EntityCategory.CONFIG, ), XiaomiMiioSelectDescription( @@ -172,7 +171,7 @@ SELECTOR_TYPES = ( set_method_error_message="Setting the ptc level failed.", icon="mdi:fire-circle", device_class="xiaomi_miio__ptc_level", - options=("low", "medium", "high"), + options=["low", "medium", "high"], entity_category=EntityCategory.CONFIG, ), ) @@ -220,7 +219,6 @@ class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity): def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the generic Xiaomi attribute selector.""" super().__init__(device, entry, unique_id, coordinator) - self._attr_options = list(description.options) self.entity_description = description From 1e5908d3a86c7ee000fe5e500b3dffc2057d4bd6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 10 Oct 2022 14:21:30 +0200 Subject: [PATCH 309/985] Update apprise to 1.1.0 (#80009) --- homeassistant/components/apprise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index a1b2efcad89..c0dc9ab4497 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -2,7 +2,7 @@ "domain": "apprise", "name": "Apprise", "documentation": "https://www.home-assistant.io/integrations/apprise", - "requirements": ["apprise==1.0.0"], + "requirements": ["apprise==1.1.0"], "codeowners": ["@caronc"], "iot_class": "cloud_push", "loggers": ["apprise"] diff --git a/requirements_all.txt b/requirements_all.txt index dec5140ffbc..c9788df5331 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -327,7 +327,7 @@ anthemav==1.4.1 apcaccess==0.0.13 # homeassistant.components.apprise -apprise==1.0.0 +apprise==1.1.0 # homeassistant.components.aprs aprslib==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b60588132ca..0bb285d5bed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -293,7 +293,7 @@ anthemav==1.4.1 apcaccess==0.0.13 # homeassistant.components.apprise -apprise==1.0.0 +apprise==1.1.0 # homeassistant.components.aprs aprslib==0.7.0 From 2ee6ea9877a33e4cbc6d85de3177f15bb77ed5d2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 10 Oct 2022 14:57:22 +0200 Subject: [PATCH 310/985] Adapt group to color temperature in K (#79719) * Adapt group to color temperature in K * Adjust tests * Adjust tests --- homeassistant/components/group/light.py | 24 ++++++----- tests/components/group/test_light.py | 41 ++++++++++--------- tests/components/light/test_init.py | 6 +-- .../custom_components/test/light.py | 8 ++-- 4 files changed, 41 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 1563e811fe9..71afa5d104b 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -12,13 +12,13 @@ from homeassistant.components import light from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, - ATTR_COLOR_TEMP, + ATTR_COLOR_TEMP_KELVIN, ATTR_EFFECT, ATTR_EFFECT_LIST, ATTR_FLASH, ATTR_HS_COLOR, - ATTR_MAX_MIREDS, - ATTR_MIN_MIREDS, + ATTR_MAX_COLOR_TEMP_KELVIN, + ATTR_MIN_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, @@ -114,7 +114,7 @@ async def async_setup_entry( FORWARDED_ATTRIBUTES = frozenset( { ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, + ATTR_COLOR_TEMP_KELVIN, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, @@ -133,8 +133,8 @@ class LightGroup(GroupEntity, LightEntity): _attr_available = False _attr_icon = "mdi:lightbulb-group" - _attr_max_mireds = 500 - _attr_min_mireds = 154 + _attr_max_color_temp_kelvin = 6500 + _attr_min_color_temp_kelvin = 2000 _attr_should_poll = False def __init__( @@ -239,12 +239,14 @@ class LightGroup(GroupEntity, LightEntity): on_states, ATTR_XY_COLOR, reduce=mean_tuple ) - self._attr_color_temp = reduce_attribute(on_states, ATTR_COLOR_TEMP) - self._attr_min_mireds = reduce_attribute( - states, ATTR_MIN_MIREDS, default=154, reduce=min + self._attr_color_temp_kelvin = reduce_attribute( + on_states, ATTR_COLOR_TEMP_KELVIN ) - self._attr_max_mireds = reduce_attribute( - states, ATTR_MAX_MIREDS, default=500, reduce=max + self._attr_min_color_temp_kelvin = reduce_attribute( + states, ATTR_MIN_COLOR_TEMP_KELVIN, default=2000, reduce=min + ) + self._attr_max_color_temp_kelvin = reduce_attribute( + states, ATTR_MAX_COLOR_TEMP_KELVIN, default=6500, reduce=max ) self._attr_effect_list = None diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index be50f29fe5c..74dd74759f4 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -13,12 +13,13 @@ from homeassistant.components.light import ( ATTR_COLOR_MODE, ATTR_COLOR_NAME, ATTR_COLOR_TEMP, + ATTR_COLOR_TEMP_KELVIN, ATTR_EFFECT, ATTR_EFFECT_LIST, ATTR_FLASH, ATTR_HS_COLOR, - ATTR_MAX_MIREDS, - ATTR_MIN_MIREDS, + ATTR_MAX_COLOR_TEMP_KELVIN, + ATTR_MIN_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, @@ -76,7 +77,7 @@ async def test_default_state(hass): assert state.attributes.get(ATTR_ENTITY_ID) == ["light.kitchen", "light.bedroom"] assert state.attributes.get(ATTR_BRIGHTNESS) is None assert state.attributes.get(ATTR_HS_COLOR) is None - assert state.attributes.get(ATTR_COLOR_TEMP) is None + assert state.attributes.get(ATTR_COLOR_TEMP_KELVIN) is None assert state.attributes.get(ATTR_EFFECT_LIST) is None assert state.attributes.get(ATTR_EFFECT) is None @@ -685,7 +686,7 @@ async def test_color_temp(hass, enable_custom_integrations): entity0.supported_color_modes = {ColorMode.COLOR_TEMP} entity0.color_mode = ColorMode.COLOR_TEMP entity0.brightness = 255 - entity0.color_temp = 2 + entity0.color_temp_kelvin = 2 entity1 = platform.ENTITIES[1] entity1.supported_features = SUPPORT_COLOR_TEMP @@ -710,20 +711,20 @@ async def test_color_temp(hass, enable_custom_integrations): state = hass.states.get("light.light_group") assert state.attributes[ATTR_COLOR_MODE] == "color_temp" - assert state.attributes[ATTR_COLOR_TEMP] == 2 + assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 2 assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"] await hass.services.async_call( "light", "turn_on", - {"entity_id": [entity1.entity_id], ATTR_COLOR_TEMP: 1000}, + {"entity_id": [entity1.entity_id], ATTR_COLOR_TEMP_KELVIN: 1000}, blocking=True, ) await hass.async_block_till_done() state = hass.states.get("light.light_group") assert state.attributes[ATTR_COLOR_MODE] == "color_temp" - assert state.attributes[ATTR_COLOR_TEMP] == 501 + assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 501 assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"] @@ -736,7 +737,7 @@ async def test_color_temp(hass, enable_custom_integrations): await hass.async_block_till_done() state = hass.states.get("light.light_group") assert state.attributes[ATTR_COLOR_MODE] == "color_temp" - assert state.attributes[ATTR_COLOR_TEMP] == 1000 + assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 1000 assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"] @@ -819,14 +820,14 @@ async def test_min_max_mireds(hass, enable_custom_integrations): entity0 = platform.ENTITIES[0] entity0.supported_color_modes = {ColorMode.COLOR_TEMP} entity0.color_mode = ColorMode.COLOR_TEMP - entity0.color_temp = 2 - entity0.min_mireds = 2 - entity0.max_mireds = 5 + entity0.color_temp_kelvin = 2 + entity0.min_color_temp_kelvin = 2 + entity0.max_color_temp_kelvin = 5 entity1 = platform.ENTITIES[1] entity1.supported_features = SUPPORT_COLOR_TEMP - entity1.min_mireds = 1 - entity1.max_mireds = 1234567890 + entity1.min_color_temp_kelvin = 1 + entity1.max_color_temp_kelvin = 1234567890 assert await async_setup_component( hass, @@ -848,8 +849,8 @@ async def test_min_max_mireds(hass, enable_custom_integrations): await hass.async_block_till_done() state = hass.states.get("light.light_group") - assert state.attributes[ATTR_MIN_MIREDS] == 1 - assert state.attributes[ATTR_MAX_MIREDS] == 1234567890 + assert state.attributes[ATTR_MIN_COLOR_TEMP_KELVIN] == 1 + assert state.attributes[ATTR_MAX_COLOR_TEMP_KELVIN] == 1234567890 await hass.services.async_call( "light", @@ -859,8 +860,8 @@ async def test_min_max_mireds(hass, enable_custom_integrations): ) await hass.async_block_till_done() state = hass.states.get("light.light_group") - assert state.attributes[ATTR_MIN_MIREDS] == 1 - assert state.attributes[ATTR_MAX_MIREDS] == 1234567890 + assert state.attributes[ATTR_MIN_COLOR_TEMP_KELVIN] == 1 + assert state.attributes[ATTR_MAX_COLOR_TEMP_KELVIN] == 1234567890 await hass.services.async_call( "light", @@ -870,8 +871,8 @@ async def test_min_max_mireds(hass, enable_custom_integrations): ) await hass.async_block_till_done() state = hass.states.get("light.light_group") - assert state.attributes[ATTR_MIN_MIREDS] == 1 - assert state.attributes[ATTR_MAX_MIREDS] == 1234567890 + assert state.attributes[ATTR_MIN_COLOR_TEMP_KELVIN] == 1 + assert state.attributes[ATTR_MAX_COLOR_TEMP_KELVIN] == 1234567890 async def test_effect_list(hass): @@ -1448,7 +1449,7 @@ async def test_invalid_service_calls(hass): ATTR_BRIGHTNESS: 150, ATTR_XY_COLOR: (0.5, 0.42), ATTR_RGB_COLOR: (80, 120, 50), - ATTR_COLOR_TEMP: 1234, + ATTR_COLOR_TEMP_KELVIN: 1234, ATTR_EFFECT: "Sunshine", ATTR_TRANSITION: 4, ATTR_FLASH: "long", diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 3a7f9cfccb8..25156eef9ca 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1193,7 +1193,7 @@ async def test_light_backwards_compatibility_color_mode( entity2 = platform.ENTITIES[2] entity2.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR_TEMP - entity2.color_temp = 100 + entity2.color_temp_kelvin = 10000 entity3 = platform.ENTITIES[3] entity3.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR @@ -1204,7 +1204,7 @@ async def test_light_backwards_compatibility_color_mode( light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP ) entity4.hs_color = (240, 100) - entity4.color_temp = 100 + entity4.color_temp_kelvin = 10000 assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1893,7 +1893,7 @@ async def test_light_service_call_color_temp_conversion( assert entity1.min_mireds == 153 assert entity1.max_mireds == 500 assert entity1.min_color_temp_kelvin == 2000 - assert entity1.max_color_temp_kelvin == 6535 + assert entity1.max_color_temp_kelvin == 6500 assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index a4b5a182edc..c4b72e6405a 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -37,13 +37,13 @@ class MockLight(MockToggleEntity, LightEntity): """Mock light class.""" color_mode = None - max_mireds = 500 - min_mireds = 153 + max_color_temp_kelvin = 6500 + min_color_temp_kelvin = 2000 supported_color_modes = None supported_features = 0 brightness = None - color_temp = None + color_temp_kelvin = None hs_color = None rgb_color = None rgbw_color = None @@ -61,7 +61,7 @@ class MockLight(MockToggleEntity, LightEntity): "rgb_color", "rgbw_color", "rgbww_color", - "color_temp", + "color_temp_kelvin", ]: setattr(self, key, value) if key == "white": From 7b247a79cf93260a6d62534d0b7797ee608d113a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 10 Oct 2022 15:45:38 +0200 Subject: [PATCH 311/985] Correct min/max mireds for lights which use K for color temp (#79998) --- homeassistant/components/light/__init__.py | 15 ++++-- tests/components/group/test_light.py | 8 +-- tests/components/light/test_init.py | 50 +++++++++++++++++++ .../custom_components/test/light.py | 4 +- 4 files changed, 68 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 1038fef7a30..5bf72b7267b 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -911,11 +911,20 @@ class LightEntity(ToggleEntity): supported_color_modes = self._light_internal_supported_color_modes if ColorMode.COLOR_TEMP in supported_color_modes: - data[ATTR_MIN_MIREDS] = self.min_mireds - data[ATTR_MAX_MIREDS] = self.max_mireds data[ATTR_MIN_COLOR_TEMP_KELVIN] = self.min_color_temp_kelvin data[ATTR_MAX_COLOR_TEMP_KELVIN] = self.max_color_temp_kelvin - + if not self.max_color_temp_kelvin: + data[ATTR_MIN_MIREDS] = None + else: + data[ATTR_MIN_MIREDS] = color_util.color_temperature_kelvin_to_mired( + self.max_color_temp_kelvin + ) + if not self.min_color_temp_kelvin: + data[ATTR_MAX_MIREDS] = None + else: + data[ATTR_MAX_MIREDS] = color_util.color_temperature_kelvin_to_mired( + self.min_color_temp_kelvin + ) if supported_features & LightEntityFeature.EFFECT: data[ATTR_EFFECT_LIST] = self.effect_list diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 74dd74759f4..3ba4aaaad81 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -821,13 +821,13 @@ async def test_min_max_mireds(hass, enable_custom_integrations): entity0.supported_color_modes = {ColorMode.COLOR_TEMP} entity0.color_mode = ColorMode.COLOR_TEMP entity0.color_temp_kelvin = 2 - entity0.min_color_temp_kelvin = 2 - entity0.max_color_temp_kelvin = 5 + entity0._attr_min_color_temp_kelvin = 2 + entity0._attr_max_color_temp_kelvin = 5 entity1 = platform.ENTITIES[1] entity1.supported_features = SUPPORT_COLOR_TEMP - entity1.min_color_temp_kelvin = 1 - entity1.max_color_temp_kelvin = 1234567890 + entity1._attr_min_color_temp_kelvin = 1 + entity1._attr_max_color_temp_kelvin = 1234567890 assert await async_setup_component( hass, diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 25156eef9ca..bff46af29e9 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1903,6 +1903,10 @@ async def test_light_service_call_color_temp_conversion( light.ColorMode.COLOR_TEMP, light.ColorMode.RGBWW, ] + assert state.attributes["min_mireds"] == 153 + assert state.attributes["max_mireds"] == 500 + assert state.attributes["min_color_temp_kelvin"] == 2000 + assert state.attributes["max_color_temp_kelvin"] == 6500 state = hass.states.get(entity1.entity_id) assert state.attributes["supported_color_modes"] == [light.ColorMode.RGBWW] @@ -2001,6 +2005,52 @@ async def test_light_service_call_color_temp_conversion( assert data == {"brightness": 255, "rgbww_color": (0, 0, 0, 66, 189)} +async def test_light_mired_color_temp_conversion(hass, enable_custom_integrations): + """Test color temp conversion from K to legacy mired.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("Test_rgbww_ct", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_rgbww", STATE_ON)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = { + light.ColorMode.COLOR_TEMP, + } + entity0._attr_min_color_temp_kelvin = 1800 + entity0._attr_max_color_temp_kelvin = 6700 + + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.attributes["supported_color_modes"] == [light.ColorMode.COLOR_TEMP] + assert state.attributes["min_mireds"] == 149 + assert state.attributes["max_mireds"] == 555 + assert state.attributes["min_color_temp_kelvin"] == 1800 + assert state.attributes["max_color_temp_kelvin"] == 6700 + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + ], + "brightness_pct": 100, + "color_temp_kelvin": 3500, + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 255, "color_temp": 285, "color_temp_kelvin": 3500} + + state = hass.states.get(entity0.entity_id) + assert state.attributes["color_mode"] == light.ColorMode.COLOR_TEMP + assert state.attributes["color_temp"] == 285 + assert state.attributes["color_temp_kelvin"] == 3500 + + async def test_light_service_call_white_mode(hass, enable_custom_integrations): """Test color_mode white in service calls.""" platform = getattr(hass.components, "test.light") diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index c4b72e6405a..3c78e7acce3 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -37,8 +37,8 @@ class MockLight(MockToggleEntity, LightEntity): """Mock light class.""" color_mode = None - max_color_temp_kelvin = 6500 - min_color_temp_kelvin = 2000 + _attr_max_color_temp_kelvin = 6500 + _attr_min_color_temp_kelvin = 2000 supported_color_modes = None supported_features = 0 From 06b1a4c2b440b33cecf807c2126951ecc308e834 Mon Sep 17 00:00:00 2001 From: rappenze Date: Mon, 10 Oct 2022 19:14:43 +0200 Subject: [PATCH 312/985] Fix armed extra state attribute in fibaro entity (#80034) --- homeassistant/components/fibaro/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 9c2d252d77f..08ee4658107 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -650,8 +650,8 @@ class FibaroDevice(Entity): attr[ATTR_BATTERY_LEVEL] = int( self.fibaro_device.properties.batteryLevel ) - if "fibaroAlarmArm" in self.fibaro_device.interfaces: - attr[ATTR_ARMED] = bool(self.fibaro_device.properties.armed) + if "armed" in self.fibaro_device.properties: + attr[ATTR_ARMED] = self.fibaro_device.properties.armed.lower() == "true" except (ValueError, KeyError): pass From 62b559bf363dcdbddcae9681ed1edfc882870b2a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Oct 2022 19:36:25 +0200 Subject: [PATCH 313/985] Adjust device classes in smartthings (#79283) --- homeassistant/components/smartthings/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 64869347228..a51ee47f0dc 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -98,7 +98,7 @@ CAPABILITY_TO_SENSORS: dict[str, list[Map]] = { Attribute.body_weight_measurement, "Body Weight", MASS_KILOGRAMS, - None, + SensorDeviceClass.WEIGHT, SensorStateClass.MEASUREMENT, None, ) @@ -209,7 +209,7 @@ CAPABILITY_TO_SENSORS: dict[str, list[Map]] = { Attribute.equivalent_carbon_dioxide_measurement, "Equivalent Carbon Dioxide Measurement", CONCENTRATION_PARTS_PER_MILLION, - None, + SensorDeviceClass.CO2, SensorStateClass.MEASUREMENT, None, ) @@ -229,7 +229,7 @@ CAPABILITY_TO_SENSORS: dict[str, list[Map]] = { Attribute.gas_meter, "Gas Meter", ENERGY_KILO_WATT_HOUR, - None, + SensorDeviceClass.ENERGY, SensorStateClass.MEASUREMENT, None, ), @@ -248,7 +248,7 @@ CAPABILITY_TO_SENSORS: dict[str, list[Map]] = { Attribute.gas_meter_volume, "Gas Meter Volume", VOLUME_CUBIC_METERS, - None, + SensorDeviceClass.VOLUME, SensorStateClass.MEASUREMENT, None, ), From 1d10822cef9de32ec4ded9bdf27bb84e0a144490 Mon Sep 17 00:00:00 2001 From: Khole Date: Mon, 10 Oct 2022 18:54:31 +0100 Subject: [PATCH 314/985] Bump pyhiveapi to 0.5.14 (#79530) --- homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 406b32d86f8..b7e5b3fa9ea 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -6,7 +6,7 @@ "models": ["HHKBridge*"] }, "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": ["pyhiveapi==0.5.13"], + "requirements": ["pyhiveapi==0.5.14"], "codeowners": ["@Rendili", "@KJonline"], "iot_class": "cloud_polling", "loggers": ["apyhiveapi"] diff --git a/requirements_all.txt b/requirements_all.txt index c9788df5331..c81a14c8914 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1604,7 +1604,7 @@ pyheos==0.7.2 pyhik==0.3.0 # homeassistant.components.hive -pyhiveapi==0.5.13 +pyhiveapi==0.5.14 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0bb285d5bed..fd0b48b3d52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1126,7 +1126,7 @@ pyhaversion==22.8.0 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.5.13 +pyhiveapi==0.5.14 # homeassistant.components.homematic pyhomematic==0.1.77 From 2427d5e28c5ae7c8bb7740fbe1a5ca679f56f3cb Mon Sep 17 00:00:00 2001 From: Sven Serlier <85389871+wrt54g@users.noreply.github.com> Date: Mon, 10 Oct 2022 20:20:25 +0200 Subject: [PATCH 315/985] Update screenshot (#79459) * Delete old image * Add new screenshot * New image --- docs/screenshot-integrations.png | Bin 121315 -> 121315 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/screenshot-integrations.png b/docs/screenshot-integrations.png index 7b5297340e72c01cb20f856db41623e4f2def610..23202a578f133c4954a66ddd6fd1dffd01c51742 100644 GIT binary patch literal 121315 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 S^=$I%^;d$gRSW;|`TaMCh=Jk& literal 121315 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 x$Yx6f{pwlzZfYS3zp7hRI From 117c12d135039797e5c00e9f1c87ece7f4be13e0 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 10 Oct 2022 19:58:20 +0100 Subject: [PATCH 316/985] Fix Eve Thermo always showing as heating in homekit_controller even when off (#80019) --- .../components/homekit_controller/climate.py | 8 +++++++ .../homekit_controller/test_climate.py | 21 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 2c4d2e3871d..41788eb4cb2 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -565,6 +565,14 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity): # This characteristic describes the current mode of a device, # e.g. a thermostat is "heating" a room to 75 degrees Fahrenheit. # Can be 0 - 2 (Off, Heat, Cool) + + # If the HVAC is switched off, it must be idle + # This works around a bug in some devices (like Eve radiator valves) that + # return they are heating when they are not. + target = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) + if target == HeatingCoolingTargetValues.OFF: + return HVACAction.IDLE + value = self.service.value(CharacteristicsTypes.HEATING_COOLING_CURRENT) return CURRENT_MODE_HOMEKIT_TO_HASS.get(value) diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index c5cad7015d8..0f669c9c51f 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -619,6 +619,27 @@ async def test_hvac_mode_vs_hvac_action(hass, utcnow): assert state.attributes["hvac_action"] == "heating" +async def test_hvac_mode_vs_hvac_action_current_mode_wrong(hass, utcnow): + """Check that we cope with buggy HEATING_COOLING_CURRENT.""" + helper = await setup_test_component(hass, create_thermostat_service) + + await helper.async_update( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.TEMPERATURE_CURRENT: 22, + CharacteristicsTypes.TEMPERATURE_TARGET: 21, + CharacteristicsTypes.HEATING_COOLING_CURRENT: 1, + CharacteristicsTypes.HEATING_COOLING_TARGET: 0, + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT: 50, + CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET: 45, + }, + ) + + state = await helper.poll_and_get_state() + assert state.state == "off" + assert state.attributes["hvac_action"] == "idle" + + def create_heater_cooler_service(accessory): """Define thermostat characteristics.""" service = accessory.add_service(ServicesTypes.HEATER_COOLER) From 257ae4d8d342b81c4146a654d19ba3dd9f422221 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Tue, 11 Oct 2022 06:01:31 +1100 Subject: [PATCH 317/985] Add support for the Flame and Morph effects for Tile and Candle (#80014) --- homeassistant/components/lifx/coordinator.py | 38 ++++- homeassistant/components/lifx/light.py | 20 ++- homeassistant/components/lifx/manager.py | 109 +++++++++++++- homeassistant/components/lifx/manifest.json | 6 +- homeassistant/components/lifx/services.yaml | 86 ++++++++++- requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/lifx/__init__.py | 9 ++ tests/components/lifx/test_light.py | 146 ++++++++++++++++++- 9 files changed, 405 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index e3a66261fb2..8e9eed34ab3 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -7,7 +7,12 @@ from enum import IntEnum from functools import partial from typing import Any, cast -from aiolifx.aiolifx import Light, MultiZoneDirection, MultiZoneEffectType +from aiolifx.aiolifx import ( + Light, + MultiZoneDirection, + MultiZoneEffectType, + TileEffectType, +) from aiolifx.connection import LIFXConnection from homeassistant.const import Platform @@ -279,7 +284,11 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): self.active_effect = FirmwareEffect[self.device.effect.get("effect", "OFF")] async def async_set_multizone_effect( - self, effect: str, speed: float, direction: str, power_on: bool = True + self, + effect: str, + speed: float = 3, + direction: str = "RIGHT", + power_on: bool = True, ) -> None: """Control the firmware-based Move effect on a multizone device.""" if lifx_features(self.device)["multizone"] is True: @@ -296,6 +305,31 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): ) self.active_effect = FirmwareEffect[effect.upper()] + async def async_set_matrix_effect( + self, + effect: str, + palette: list[tuple[int, int, int, int]] | None = None, + speed: float = 3, + power_on: bool = True, + ) -> None: + """Control the firmware-based effects on a matrix device.""" + if lifx_features(self.device)["matrix"] is True: + if power_on and self.device.power_level == 0: + await self.async_set_power(True, 0) + + if palette is None: + palette = [] + + await async_execute_lifx( + partial( + self.device.set_tile_effect, + effect=TileEffectType[effect.upper()].value, + speed=speed, + palette=palette, + ) + ) + self.active_effect = FirmwareEffect[effect.upper()] + def async_get_active_effect(self) -> int: """Return the enum value of the currently active firmware effect.""" return self.active_effect.value diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index b8128df100e..3b9b83cd1fc 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -40,6 +40,8 @@ from .coordinator import FirmwareEffect, LIFXUpdateCoordinator from .entity import LIFXEntity from .manager import ( SERVICE_EFFECT_COLORLOOP, + SERVICE_EFFECT_FLAME, + SERVICE_EFFECT_MORPH, SERVICE_EFFECT_MOVE, SERVICE_EFFECT_PULSE, SERVICE_EFFECT_STOP, @@ -93,8 +95,10 @@ async def async_setup_entry( LIFX_SET_HEV_CYCLE_STATE_SCHEMA, "set_hev_cycle_state", ) - if lifx_features(device)["extended_multizone"]: - entity: LIFXLight = LIFXExtendedMultiZone(coordinator, manager, entry) + if lifx_features(device)["matrix"]: + entity: LIFXLight = LIFXMatrix(coordinator, manager, entry) + elif lifx_features(device)["extended_multizone"]: + entity = LIFXExtendedMultiZone(coordinator, manager, entry) elif lifx_features(device)["multizone"]: entity = LIFXMultiZone(coordinator, manager, entry) elif lifx_features(device)["color"]: @@ -471,3 +475,15 @@ class LIFXExtendedMultiZone(LIFXMultiZone): # set_extended_color_zones does not update the # state of the device, so we need to do that await self.get_color() + + +class LIFXMatrix(LIFXColor): + """Representation of a LIFX matrix device.""" + + _attr_effect_list = [ + SERVICE_EFFECT_COLORLOOP, + SERVICE_EFFECT_FLAME, + SERVICE_EFFECT_PULSE, + SERVICE_EFFECT_MORPH, + SERVICE_EFFECT_STOP, + ] diff --git a/homeassistant/components/lifx/manager.py b/homeassistant/components/lifx/manager.py index 2b4536656d8..d6ae45c1edc 100644 --- a/homeassistant/components/lifx/manager.py +++ b/homeassistant/components/lifx/manager.py @@ -7,6 +7,7 @@ from datetime import timedelta from typing import Any import aiolifx_effects +from aiolifx_themes.themes import Theme, ThemeLibrary import voluptuous as vol from homeassistant.components.light import ( @@ -34,9 +35,11 @@ from .util import convert_8_to_16, find_hsbk SCAN_INTERVAL = timedelta(seconds=10) -SERVICE_EFFECT_PULSE = "effect_pulse" SERVICE_EFFECT_COLORLOOP = "effect_colorloop" +SERVICE_EFFECT_FLAME = "effect_flame" +SERVICE_EFFECT_MORPH = "effect_morph" SERVICE_EFFECT_MOVE = "effect_move" +SERVICE_EFFECT_PULSE = "effect_pulse" SERVICE_EFFECT_STOP = "effect_stop" ATTR_POWER_OFF = "power_off" @@ -47,11 +50,20 @@ ATTR_SPREAD = "spread" ATTR_CHANGE = "change" ATTR_DIRECTION = "direction" ATTR_SPEED = "speed" +ATTR_PALETTE = "palette" +ATTR_THEME = "theme" +EFFECT_FLAME = "FLAME" +EFFECT_MORPH = "MORPH" EFFECT_MOVE = "MOVE" EFFECT_OFF = "OFF" -EFFECT_MOVE_DEFAULT_SPEED = 3.0 +EFFECT_FLAME_DEFAULT_SPEED = 3 + +EFFECT_MORPH_DEFAULT_SPEED = 3 +EFFECT_MORPH_DEFAULT_THEME = "exciting" + +EFFECT_MOVE_DEFAULT_SPEED = 3 EFFECT_MOVE_DEFAULT_DIRECTION = "right" EFFECT_MOVE_DIRECTION_RIGHT = "right" EFFECT_MOVE_DIRECTION_LEFT = "left" @@ -128,6 +140,37 @@ SERVICES = ( SERVICE_EFFECT_COLORLOOP, ) +LIFX_EFFECT_FLAME_SCHEMA = cv.make_entity_service_schema( + { + **LIFX_EFFECT_SCHEMA, + ATTR_SPEED: vol.All(vol.Coerce(int), vol.Clamp(min=1, max=25)), + } +) + +HSBK_SCHEMA = vol.All( + vol.Coerce(tuple), + vol.ExactSequence( + ( + vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), + vol.All(vol.Coerce(float), vol.Clamp(min=0, max=100)), + vol.All(vol.Coerce(int), vol.Clamp(min=1500, max=9000)), + ) + ), +) + +LIFX_EFFECT_MORPH_SCHEMA = cv.make_entity_service_schema( + { + **LIFX_EFFECT_SCHEMA, + ATTR_SPEED: vol.All(vol.Coerce(int), vol.Clamp(min=1, max=25)), + vol.Exclusive(ATTR_THEME, COLOR_GROUP): vol.Optional( + vol.In(ThemeLibrary().themes) + ), + vol.Exclusive(ATTR_PALETTE, COLOR_GROUP): vol.All( + cv.ensure_list, [HSBK_SCHEMA] + ), + } +) LIFX_EFFECT_MOVE_SCHEMA = cv.make_entity_service_schema( { @@ -192,6 +235,20 @@ class LIFXManager: schema=LIFX_EFFECT_COLORLOOP_SCHEMA, ) + self.hass.services.async_register( + DOMAIN, + SERVICE_EFFECT_FLAME, + service_handler, + schema=LIFX_EFFECT_FLAME_SCHEMA, + ) + + self.hass.services.async_register( + DOMAIN, + SERVICE_EFFECT_MORPH, + service_handler, + schema=LIFX_EFFECT_MORPH_SCHEMA, + ) + self.hass.services.async_register( DOMAIN, SERVICE_EFFECT_MOVE, @@ -222,7 +279,43 @@ class LIFXManager: coordinators.append(coordinator) bulbs.append(coordinator.device) - if service == SERVICE_EFFECT_MOVE: + if service == SERVICE_EFFECT_FLAME: + await asyncio.gather( + *( + coordinator.async_set_matrix_effect( + effect=EFFECT_FLAME, + speed=kwargs.get(ATTR_SPEED, EFFECT_FLAME_DEFAULT_SPEED), + power_on=kwargs.get(ATTR_POWER_ON, True), + ) + for coordinator in coordinators + ) + ) + + elif service == SERVICE_EFFECT_MORPH: + + theme_name = kwargs.get(ATTR_THEME, "exciting") + palette = kwargs.get(ATTR_PALETTE, None) + + if palette is not None: + theme = Theme() + for hsbk in palette: + theme.add_hsbk(hsbk[0], hsbk[1], hsbk[2], hsbk[3]) + else: + theme = ThemeLibrary().get_theme(theme_name) + + await asyncio.gather( + *( + coordinator.async_set_matrix_effect( + effect=EFFECT_MORPH, + speed=kwargs.get(ATTR_SPEED, EFFECT_MORPH_DEFAULT_SPEED), + palette=theme.colors, + power_on=kwargs.get(ATTR_POWER_ON, True), + ) + for coordinator in coordinators + ) + ) + + elif service == SERVICE_EFFECT_MOVE: await asyncio.gather( *( coordinator.async_set_multizone_effect( @@ -269,9 +362,9 @@ class LIFXManager: await self.effects_conductor.stop(bulbs) for coordinator in coordinators: - await coordinator.async_set_multizone_effect( - effect=EFFECT_OFF, - speed=EFFECT_MOVE_DEFAULT_SPEED, - direction=EFFECT_MOVE_DEFAULT_DIRECTION, - power_on=False, + await coordinator.async_set_matrix_effect( + effect=EFFECT_OFF, power_on=False + ) + await coordinator.async_set_multizone_effect( + effect=EFFECT_OFF, power_on=False ) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 95718b3ee83..730eceb2afa 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -3,7 +3,11 @@ "name": "LIFX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lifx", - "requirements": ["aiolifx==0.8.6", "aiolifx_effects==0.2.2"], + "requirements": [ + "aiolifx==0.8.6", + "aiolifx_effects==0.2.2", + "aiolifx_themes==0.1.1" + ], "quality_scale": "platinum", "dependencies": ["network"], "homekit": { diff --git a/homeassistant/components/lifx/services.yaml b/homeassistant/components/lifx/services.yaml index fc2e522dcd4..ced5bacf513 100644 --- a/homeassistant/components/lifx/services.yaml +++ b/homeassistant/components/lifx/services.yaml @@ -205,7 +205,91 @@ effect_move: default: true selector: boolean: - +effect_flame: + name: Flame effect + description: Start the firmware-based Flame effect on LIFX Tiles or Candle. + target: + entity: + integration: lifx + domain: light + fields: + speed: + name: Speed + description: How fast the flames will move. + default: 3 + selector: + number: + min: 1 + max: 25 + step: 1 + unit_of_measurement: seconds + power_on: + name: Power on + description: Powered off lights will be turned on before starting the effect. + default: true + selector: + boolean: +effect_morph: + name: Morph effect + description: Start the firmware-based Morph effect on LIFX Tiles on Candle. + target: + entity: + integration: lifx + domain: light + fields: + speed: + name: Speed + description: How fast the colors will move. + default: 3 + selector: + number: + min: 1 + max: 25 + step: 1 + unit_of_measurement: seconds + palette: + name: Palette + description: List of at least 2 and at most 16 colors as hue (0-360), saturation (0-100), brightness (0-100) and kelvin (1500-900) values to use for this effect. Overrides the theme attribute. + example: + - "[[0, 100, 100, 3500], [60, 100, 100, 3500]]" + selector: + object: + theme: + name: Theme + description: Predefined color theme to use for the effect. Overridden by the palette attribute. + selector: + select: + options: + - "autumn" + - "blissful" + - "cheerful" + - "dream" + - "energizing" + - "epic" + - "exciting" + - "focusing" + - "halloween" + - "hanukkah" + - "holly" + - "independence day" + - "intense" + - "mellow" + - "peaceful" + - "powerful" + - "relaxing" + - "santa" + - "serene" + - "soothing" + - "sports" + - "spring" + - "tranquil" + - "warming" + power_on: + name: Power on + description: Powered off lights will be turned on before starting the effect. + default: true + selector: + boolean: effect_stop: name: Stop effect description: Stop a running effect. diff --git a/requirements_all.txt b/requirements_all.txt index c81a14c8914..278192e78b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -198,6 +198,9 @@ aiolifx==0.8.6 # homeassistant.components.lifx aiolifx_effects==0.2.2 +# homeassistant.components.lifx +aiolifx_themes==0.1.1 + # homeassistant.components.lookin aiolookin==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd0b48b3d52..8b9df29d143 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -176,6 +176,9 @@ aiolifx==0.8.6 # homeassistant.components.lifx aiolifx_effects==0.2.2 +# homeassistant.components.lifx +aiolifx_themes==0.1.1 + # homeassistant.components.lookin aiolookin==0.1.1 diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py index acfe8f69b02..df3c41ccaca 100644 --- a/tests/components/lifx/__init__.py +++ b/tests/components/lifx/__init__.py @@ -156,6 +156,15 @@ def _mocked_light_strip() -> Light: return bulb +def _mocked_tile() -> Light: + bulb = _mocked_bulb() + bulb.product = 55 # LIFX Tile + bulb.effect = {"effect": "OFF"} + bulb.get_tile_effect = MockLifxCommand(bulb) + bulb.set_tile_effect = MockLifxCommand(bulb) + return bulb + + def _mocked_bulb_new_firmware() -> Light: bulb = _mocked_bulb() bulb.host_firmware_version = "3.90" diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index 1c424f354e3..cba5ba4636c 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -12,8 +12,11 @@ from homeassistant.components.lifx.const import ATTR_POWER from homeassistant.components.lifx.light import ATTR_INFRARED, ATTR_ZONES from homeassistant.components.lifx.manager import ( ATTR_DIRECTION, + ATTR_PALETTE, ATTR_SPEED, + ATTR_THEME, SERVICE_EFFECT_COLORLOOP, + SERVICE_EFFECT_MORPH, SERVICE_EFFECT_MOVE, ) from homeassistant.components.light import ( @@ -55,6 +58,7 @@ from . import ( _mocked_bulb_new_firmware, _mocked_clean_bulb, _mocked_light_strip, + _mocked_tile, _mocked_white_bulb, _patch_config_flow_try_connect, _patch_device, @@ -650,6 +654,146 @@ async def test_extended_multizone_messages(hass: HomeAssistant) -> None: ) +async def test_matrix_flame_morph_effects(hass: HomeAssistant) -> None: + """Test the firmware flame and morph effects on a matrix device.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + config_entry.add_to_hass(hass) + bulb = _mocked_tile() + bulb.power_level = 0 + bulb.color = [65535, 65535, 65535, 65535] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "effect_flame"}, + blocking=True, + ) + + assert len(bulb.set_power.calls) == 1 + assert len(bulb.set_tile_effect.calls) == 1 + + call_dict = bulb.set_tile_effect.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "effect": 3, + "speed": 3, + "palette": [], + } + bulb.get_tile_effect.reset_mock() + bulb.set_tile_effect.reset_mock() + bulb.set_power.reset_mock() + + bulb.power_level = 0 + await hass.services.async_call( + DOMAIN, + SERVICE_EFFECT_MORPH, + {ATTR_ENTITY_ID: entity_id, ATTR_SPEED: 4, ATTR_THEME: "autumn"}, + blocking=True, + ) + + bulb.power_level = 65535 + bulb.effect = { + "effect": "MORPH", + "speed": 4.0, + "palette": [ + (5643, 65535, 32768, 3500), + (15109, 65535, 32768, 3500), + (8920, 65535, 32768, 3500), + (10558, 65535, 32768, 3500), + ], + } + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + assert len(bulb.set_power.calls) == 1 + assert len(bulb.set_tile_effect.calls) == 1 + call_dict = bulb.set_tile_effect.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "effect": 2, + "speed": 4, + "palette": [ + (5643, 65535, 32768, 3500), + (15109, 65535, 32768, 3500), + (8920, 65535, 32768, 3500), + (10558, 65535, 32768, 3500), + ], + } + bulb.get_tile_effect.reset_mock() + bulb.set_tile_effect.reset_mock() + bulb.set_power.reset_mock() + + bulb.power_level = 0 + await hass.services.async_call( + DOMAIN, + SERVICE_EFFECT_MORPH, + { + ATTR_ENTITY_ID: entity_id, + ATTR_SPEED: 6, + ATTR_PALETTE: [ + (0, 100, 255, 3500), + (60, 100, 255, 3500), + (120, 100, 255, 3500), + (180, 100, 255, 3500), + (240, 100, 255, 3500), + (300, 100, 255, 3500), + ], + }, + blocking=True, + ) + + bulb.power_level = 65535 + bulb.effect = { + "effect": "MORPH", + "speed": 6, + "palette": [ + (0, 65535, 65535, 3500), + (10922, 65535, 65535, 3500), + (21845, 65535, 65535, 3500), + (32768, 65535, 65535, 3500), + (43690, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + ], + } + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + assert len(bulb.set_power.calls) == 1 + assert len(bulb.set_tile_effect.calls) == 1 + call_dict = bulb.set_tile_effect.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "effect": 2, + "speed": 6, + "palette": [ + (0, 65535, 65535, 3500), + (10922, 65535, 65535, 3500), + (21845, 65535, 65535, 3500), + (32768, 65535, 65535, 3500), + (43690, 65535, 65535, 3500), + (54613, 65535, 65535, 3500), + ], + } + bulb.get_tile_effect.reset_mock() + bulb.set_tile_effect.reset_mock() + bulb.set_power.reset_mock() + + async def test_lightstrip_move_effect(hass: HomeAssistant) -> None: """Test the firmware move effect on a light strip.""" config_entry = MockConfigEntry( @@ -697,7 +841,7 @@ async def test_lightstrip_move_effect(hass: HomeAssistant) -> None: ) bulb.power_level = 65535 - bulb.effect = {"name": "effect_move", "enable": 1} + bulb.effect = {"name": "MOVE", "speed": 4.5, "direction": "Left"} async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) await hass.async_block_till_done() From 3ab294e8efc00c9f3cda2993318bb582ba675f8c Mon Sep 17 00:00:00 2001 From: kingy444 Date: Tue, 11 Oct 2022 06:05:04 +1100 Subject: [PATCH 318/985] Powerview refactor prep for all shade types (#79862) --- .../hunterdouglas_powerview/cover.py | 351 +++++++++++------- .../hunterdouglas_powerview/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 211 insertions(+), 146 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index d444354b7a8..1f04c8ddbd1 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -12,16 +12,12 @@ from aiopvapi.helpers.constants import ( ATTR_POSITION1, ATTR_POSITION2, ATTR_POSITION_DATA, -) -from aiopvapi.resources.shade import ( ATTR_POSKIND1, ATTR_POSKIND2, MAX_POSITION, MIN_POSITION, - BaseShade, - ShadeTopDownBottomUp, - factory as PvShade, ) +from aiopvapi.resources.shade import BaseShade, factory as PvShade import async_timeout from homeassistant.components.cover import ( @@ -107,32 +103,6 @@ async def async_setup_entry( async_add_entities(entities) -def create_powerview_shade_entity( - coordinator: PowerviewShadeUpdateCoordinator, - device_info: PowerviewDeviceInfo, - room_name: str, - shade: BaseShade, - name_before_refresh: str, -) -> Iterable[ShadeEntity]: - """Create a PowerViewShade entity.""" - - classes: list[BaseShade] = [] - if isinstance(shade, ShadeTopDownBottomUp): - classes.extend([PowerViewShadeTDBUTop, PowerViewShadeTDBUBottom]) - elif ( # this will be extended further in next release for more defined control - shade.capability.capabilities.tiltOnClosed - or shade.capability.capabilities.tiltAnywhere - ): - classes.append(PowerViewShadeWithTilt) - else: - classes.append(PowerViewShade) - _LOGGER.debug("%s (%s) detected as %a", shade.name, shade.capability.type, classes) - return [ - cls(coordinator, device_info, room_name, shade, name_before_refresh) - for cls in classes - ] - - def hd_position_to_hass(hd_position: int, max_val: int = MAX_POSITION) -> int: """Convert hunter douglas position to hass position.""" return round((hd_position / max_val) * 100) @@ -392,6 +362,185 @@ class PowerViewShade(PowerViewShadeBase): ) +class PowerViewShadeWithTiltBase(PowerViewShade): + """Representation for PowerView shades with tilt capabilities.""" + + def __init__( + self, + coordinator: PowerviewShadeUpdateCoordinator, + device_info: PowerviewDeviceInfo, + room_name: str, + shade: BaseShade, + name: str, + ) -> None: + """Initialize the shade.""" + super().__init__(coordinator, device_info, room_name, shade, name) + self._attr_supported_features |= ( + CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.SET_TILT_POSITION + ) + if self._device_info.model != LEGACY_DEVICE_MODEL: + self._attr_supported_features |= CoverEntityFeature.STOP_TILT + self._max_tilt = self._shade.shade_limits.tilt_max + + @property + def current_cover_tilt_position(self) -> int: + """Return the current cover tile position.""" + return hd_position_to_hass(self.positions.vane, self._max_tilt) + + @property + def transition_steps(self): + """Return the steps to make a move.""" + return hd_position_to_hass( + self.positions.primary, MAX_POSITION + ) + hd_position_to_hass(self.positions.vane, self._max_tilt) + + @property + def open_tilt_position(self) -> PowerviewShadeMove: + """Return the open tilt position and required additional positions.""" + return PowerviewShadeMove(self._shade.open_position_tilt, {}) + + @property + def close_tilt_position(self) -> PowerviewShadeMove: + """Return the close tilt position and required additional positions.""" + return PowerviewShadeMove(self._shade.close_position_tilt, {}) + + async def async_close_cover_tilt(self, **kwargs: Any) -> None: + """Close the cover tilt.""" + self._async_schedule_update_for_transition(self.transition_steps) + await self._async_execute_move(self.close_tilt_position) + self.async_write_ha_state() + + async def async_open_cover_tilt(self, **kwargs: Any) -> None: + """Open the cover tilt.""" + self._async_schedule_update_for_transition(100 - self.transition_steps) + await self._async_execute_move(self.open_tilt_position) + self.async_write_ha_state() + + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: + """Move the vane to a specific position.""" + await self._async_set_cover_tilt_position(kwargs[ATTR_TILT_POSITION]) + + async def _async_set_cover_tilt_position( + self, target_hass_tilt_position: int + ) -> None: + """Move the vane to a specific position.""" + final_position = self.current_cover_position + target_hass_tilt_position + self._async_schedule_update_for_transition( + abs(self.transition_steps - final_position) + ) + await self._async_execute_move(self._get_shade_tilt(target_hass_tilt_position)) + self.async_write_ha_state() + + @callback + def _get_shade_tilt(self, target_hass_tilt_position: int) -> PowerviewShadeMove: + """Return a PowerviewShadeMove.""" + position_vane = hass_position_to_hd(target_hass_tilt_position, self._max_tilt) + return PowerviewShadeMove( + {ATTR_POSITION1: position_vane, ATTR_POSKIND1: POS_KIND_VANE}, {} + ) + + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: + """Stop the cover tilting.""" + await self.async_stop_cover() + + +class PowerViewShadeWithTiltOnClosed(PowerViewShadeWithTiltBase): + """Representation of a PowerView shade with tilt when closed capabilities. + + API Class: ShadeBottomUpTiltOnClosed + ShadeBottomUpTiltOnClosed90 + + Type 1 - Bottom Up w/ 90° Tilt + Shade 44 - a shade thought to have been a firmware issue (type 0 usually dont tilt) + """ + + @property + def open_position(self) -> PowerviewShadeMove: + """Return the open position and required additional positions.""" + return PowerviewShadeMove( + self._shade.open_position, {POS_KIND_VANE: MIN_POSITION} + ) + + @property + def close_position(self) -> PowerviewShadeMove: + """Return the close position and required additional positions.""" + return PowerviewShadeMove( + self._shade.close_position, {POS_KIND_VANE: MIN_POSITION} + ) + + @property + def open_tilt_position(self) -> PowerviewShadeMove: + """Return the open tilt position and required additional positions.""" + return PowerviewShadeMove( + self._shade.open_position_tilt, {POS_KIND_PRIMARY: MIN_POSITION} + ) + + @property + def close_tilt_position(self) -> PowerviewShadeMove: + """Return the close tilt position and required additional positions.""" + return PowerviewShadeMove( + self._shade.close_position_tilt, {POS_KIND_PRIMARY: MIN_POSITION} + ) + + @callback + def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove: + """Return a PowerviewShadeMove.""" + position_shade = hass_position_to_hd(target_hass_position) + return PowerviewShadeMove( + {ATTR_POSITION1: position_shade, ATTR_POSKIND1: POS_KIND_PRIMARY}, + {POS_KIND_VANE: MIN_POSITION}, + ) + + @callback + def _get_shade_tilt(self, target_hass_tilt_position: int) -> PowerviewShadeMove: + """Return a PowerviewShadeMove.""" + position_vane = hass_position_to_hd(target_hass_tilt_position, self._max_tilt) + return PowerviewShadeMove( + {ATTR_POSITION1: position_vane, ATTR_POSKIND1: POS_KIND_VANE}, + {POS_KIND_PRIMARY: MIN_POSITION}, + ) + + +class PowerViewShadeWithTiltAnywhere(PowerViewShadeWithTiltBase): + """Representation of a PowerView shade with tilt anywhere capabilities. + + API Class: ShadeBottomUpTiltAnywhere, ShadeVerticalTiltAnywhere + + Type 2 - Bottom Up w/ 180° Tilt + Type 4 - Vertical (Traversing) w/ 180° Tilt + """ + + @callback + def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove: + position_shade = hass_position_to_hd(target_hass_position, MAX_POSITION) + position_vane = self.positions.vane + return PowerviewShadeMove( + { + ATTR_POSITION1: position_shade, + ATTR_POSITION2: position_vane, + ATTR_POSKIND1: POS_KIND_PRIMARY, + ATTR_POSKIND2: POS_KIND_VANE, + }, + {}, + ) + + @callback + def _get_shade_tilt(self, target_hass_tilt_position: int) -> PowerviewShadeMove: + """Return a PowerviewShadeMove.""" + position_shade = self.positions.primary + position_vane = hass_position_to_hd(target_hass_tilt_position, self._max_tilt) + return PowerviewShadeMove( + { + ATTR_POSITION1: position_shade, + ATTR_POSITION2: position_vane, + ATTR_POSKIND1: POS_KIND_PRIMARY, + ATTR_POSKIND2: POS_KIND_VANE, + }, + {}, + ) + + class PowerViewShadeDualRailBase(PowerViewShade): """Representation of a shade with top/down bottom/up capabilities. @@ -528,117 +677,33 @@ class PowerViewShadeTDBUTop(PowerViewShadeDualRailBase): ) -class PowerViewShadeWithTilt(PowerViewShade): - """Representation of a PowerView shade with tilt capabilities.""" +TYPE_TO_CLASSES = { + 1: (PowerViewShadeWithTiltOnClosed,), + 2: (PowerViewShadeWithTiltAnywhere,), + 4: (PowerViewShadeWithTiltAnywhere,), + 7: (PowerViewShadeTDBUTop, PowerViewShadeTDBUBottom), +} - def __init__( - self, - coordinator: PowerviewShadeUpdateCoordinator, - device_info: PowerviewDeviceInfo, - room_name: str, - shade: BaseShade, - name: str, - ) -> None: - """Initialize the shade.""" - super().__init__(coordinator, device_info, room_name, shade, name) - self._attr_supported_features |= ( - CoverEntityFeature.OPEN_TILT - | CoverEntityFeature.CLOSE_TILT - | CoverEntityFeature.SET_TILT_POSITION - ) - if self._device_info.model != LEGACY_DEVICE_MODEL: - self._attr_supported_features |= CoverEntityFeature.STOP_TILT - self._max_tilt = self._shade.shade_limits.tilt_max - @property - def current_cover_tilt_position(self) -> int: - """Return the current cover tile position.""" - return hd_position_to_hass(self.positions.vane, self._max_tilt) - - @property - def transition_steps(self): - """Return the steps to make a move.""" - return hd_position_to_hass( - self.positions.primary, MAX_POSITION - ) + hd_position_to_hass(self.positions.vane, self._max_tilt) - - @property - def open_position(self) -> PowerviewShadeMove: - """Return the open position and required additional positions.""" - return PowerviewShadeMove( - self._shade.open_position, {POS_KIND_VANE: MIN_POSITION} - ) - - @property - def close_position(self) -> PowerviewShadeMove: - """Return the close position and required additional positions.""" - return PowerviewShadeMove( - self._shade.close_position, {POS_KIND_VANE: MIN_POSITION} - ) - - @property - def open_tilt_position(self) -> PowerviewShadeMove: - """Return the open tilt position and required additional positions.""" - # next upstream api release to include self._shade.open_tilt_position - return PowerviewShadeMove( - {ATTR_POSKIND1: POS_KIND_VANE, ATTR_POSITION1: self._max_tilt}, - {POS_KIND_PRIMARY: MIN_POSITION}, - ) - - @property - def close_tilt_position(self) -> PowerviewShadeMove: - """Return the close tilt position and required additional positions.""" - # next upstream api release to include self._shade.close_tilt_position - return PowerviewShadeMove( - {ATTR_POSKIND1: POS_KIND_VANE, ATTR_POSITION1: MIN_POSITION}, - {POS_KIND_PRIMARY: MIN_POSITION}, - ) - - async def async_close_cover_tilt(self, **kwargs: Any) -> None: - """Close the cover tilt.""" - self._async_schedule_update_for_transition(self.transition_steps) - await self._async_execute_move(self.close_tilt_position) - self.async_write_ha_state() - - async def async_open_cover_tilt(self, **kwargs: Any) -> None: - """Open the cover tilt.""" - self._async_schedule_update_for_transition(100 - self.transition_steps) - await self._async_execute_move(self.open_tilt_position) - self.async_write_ha_state() - - async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: - """Move the vane to a specific position.""" - await self._async_set_cover_tilt_position(kwargs[ATTR_TILT_POSITION]) - - async def _async_set_cover_tilt_position( - self, target_hass_tilt_position: int - ) -> None: - """Move the vane to a specific position.""" - final_position = self.current_cover_position + target_hass_tilt_position - self._async_schedule_update_for_transition( - abs(self.transition_steps - final_position) - ) - await self._async_execute_move(self._get_shade_tilt(target_hass_tilt_position)) - self.async_write_ha_state() - - @callback - def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove: - """Return a PowerviewShadeMove.""" - position_shade = hass_position_to_hd(target_hass_position) - return PowerviewShadeMove( - {ATTR_POSITION1: position_shade, ATTR_POSKIND1: POS_KIND_PRIMARY}, - {POS_KIND_VANE: MIN_POSITION}, - ) - - @callback - def _get_shade_tilt(self, target_hass_tilt_position: int) -> PowerviewShadeMove: - """Return a PowerviewShadeMove.""" - position_vane = hass_position_to_hd(target_hass_tilt_position, self._max_tilt) - return PowerviewShadeMove( - {ATTR_POSITION1: position_vane, ATTR_POSKIND1: POS_KIND_VANE}, - {POS_KIND_PRIMARY: MIN_POSITION}, - ) - - async def async_stop_cover_tilt(self, **kwargs: Any) -> None: - """Stop the cover tilting.""" - await self.async_stop_cover() +def create_powerview_shade_entity( + coordinator: PowerviewShadeUpdateCoordinator, + device_info: PowerviewDeviceInfo, + room_name: str, + shade: BaseShade, + name_before_refresh: str, +) -> Iterable[ShadeEntity]: + """Create a PowerViewShade entity.""" + classes: Iterable[BaseShade] = TYPE_TO_CLASSES.get( + shade.capability.type, (PowerViewShade,) + ) + _LOGGER.debug( + "%s (%s) detected as %a %s", + shade.name, + shade.capability.type, + classes, + shade.raw_data, + ) + return [ + cls(coordinator, device_info, room_name, shade, name_before_refresh) + for cls in classes + ] diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index 930e80733e0..9eb3019984e 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -2,7 +2,7 @@ "domain": "hunterdouglas_powerview", "name": "Hunter Douglas PowerView", "documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview", - "requirements": ["aiopvapi==2.0.2"], + "requirements": ["aiopvapi==2.0.3"], "codeowners": ["@bdraco", "@kingy444", "@trullock"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 278192e78b5..40e850750b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -232,7 +232,7 @@ aioopenexchangerates==0.4.0 aiopulse==0.4.3 # homeassistant.components.hunterdouglas_powerview -aiopvapi==2.0.2 +aiopvapi==2.0.3 # homeassistant.components.pvpc_hourly_pricing aiopvpc==3.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b9df29d143..74ae33fd803 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -207,7 +207,7 @@ aioopenexchangerates==0.4.0 aiopulse==0.4.3 # homeassistant.components.hunterdouglas_powerview -aiopvapi==2.0.2 +aiopvapi==2.0.3 # homeassistant.components.pvpc_hourly_pricing aiopvpc==3.0.0 From 82d3397a9b6847d537b1158dcd335e547a11707c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 10 Oct 2022 21:18:26 +0200 Subject: [PATCH 319/985] Adapt deCONZ binary sensors to entity descriptions (#79486) Now typing with lambdas work --- .../components/deconz/binary_sensor.py | 351 ++++++++---------- 1 file changed, 154 insertions(+), 197 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index f495fef45c3..6e0c4c86d21 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -1,7 +1,9 @@ """Support for deCONZ binary sensors.""" from __future__ import annotations -from typing import TYPE_CHECKING, TypeVar +from collections.abc import Callable +from dataclasses import dataclass +from typing import Generic, TypeVar from pydeconz.interfaces.sensors import SensorResources from pydeconz.models.event import EventType @@ -19,6 +21,7 @@ from homeassistant.components.binary_sensor import ( DOMAIN, BinarySensorDeviceClass, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE @@ -48,10 +51,130 @@ PROVIDES_EXTRA_ATTRIBUTES = ( "water", ) +T = TypeVar( + "T", + Alarm, + CarbonMonoxide, + Fire, + GenericFlag, + OpenClose, + Presence, + Vibration, + Water, + PydeconzSensorBase, +) + + +@dataclass +class DeconzBinarySensorDescriptionMixin(Generic[T]): + """Required values when describing secondary sensor attributes.""" + + update_key: str + value_fn: Callable[[T], bool | None] + + +@dataclass +class DeconzBinarySensorDescription( + BinarySensorEntityDescription, + DeconzBinarySensorDescriptionMixin[T], +): + """Class describing deCONZ binary sensor entities.""" + + instance_check: type[T] | None = None + name_suffix: str = "" + old_unique_id_suffix: str = "" + + +ENTITY_DESCRIPTIONS: tuple[DeconzBinarySensorDescription, ...] = ( + DeconzBinarySensorDescription[Alarm]( + key="alarm", + update_key="alarm", + value_fn=lambda device: device.alarm, + instance_check=Alarm, + device_class=BinarySensorDeviceClass.SAFETY, + ), + DeconzBinarySensorDescription[CarbonMonoxide]( + key="carbon_monoxide", + update_key="carbonmonoxide", + value_fn=lambda device: device.carbon_monoxide, + instance_check=CarbonMonoxide, + device_class=BinarySensorDeviceClass.CO, + ), + DeconzBinarySensorDescription[Fire]( + key="fire", + update_key="fire", + value_fn=lambda device: device.fire, + instance_check=Fire, + device_class=BinarySensorDeviceClass.SMOKE, + ), + DeconzBinarySensorDescription[Fire]( + key="in_test_mode", + update_key="test", + value_fn=lambda device: device.in_test_mode, + instance_check=Fire, + name_suffix="Test Mode", + old_unique_id_suffix="test mode", + device_class=BinarySensorDeviceClass.SMOKE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DeconzBinarySensorDescription[GenericFlag]( + key="flag", + update_key="flag", + value_fn=lambda device: device.flag, + instance_check=GenericFlag, + ), + DeconzBinarySensorDescription[OpenClose]( + key="open", + update_key="open", + value_fn=lambda device: device.open, + instance_check=OpenClose, + device_class=BinarySensorDeviceClass.OPENING, + ), + DeconzBinarySensorDescription[Presence]( + key="presence", + update_key="presence", + value_fn=lambda device: device.presence, + instance_check=Presence, + device_class=BinarySensorDeviceClass.MOTION, + ), + DeconzBinarySensorDescription[Vibration]( + key="vibration", + update_key="vibration", + value_fn=lambda device: device.vibration, + instance_check=Vibration, + device_class=BinarySensorDeviceClass.VIBRATION, + ), + DeconzBinarySensorDescription[Water]( + key="water", + update_key="water", + value_fn=lambda device: device.water, + instance_check=Water, + device_class=BinarySensorDeviceClass.MOISTURE, + ), + DeconzBinarySensorDescription[SensorResources]( + key="tampered", + update_key="tampered", + value_fn=lambda device: device.tampered, + name_suffix="Tampered", + old_unique_id_suffix="tampered", + device_class=BinarySensorDeviceClass.TAMPER, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DeconzBinarySensorDescription[SensorResources]( + key="low_battery", + update_key="lowbattery", + value_fn=lambda device: device.low_battery, + name_suffix="Low Battery", + old_unique_id_suffix="low battery", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + @callback def async_update_unique_id( - hass: HomeAssistant, unique_id: str, entity_class: DeconzBinarySensor + hass: HomeAssistant, unique_id: str, description: DeconzBinarySensorDescription ) -> None: """Update unique ID to always have a suffix. @@ -59,12 +182,12 @@ def async_update_unique_id( """ ent_reg = er.async_get(hass) - new_unique_id = f"{unique_id}-{entity_class.unique_id_suffix}" + new_unique_id = f"{unique_id}-{description.key}" if ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, new_unique_id): return - if entity_class.old_unique_id_suffix: - unique_id = f'{unique_id.split("-", 1)[0]}-{entity_class.old_unique_id_suffix}' + if description.old_unique_id_suffix: + unique_id = f'{unique_id.split("-", 1)[0]}-{description.old_unique_id_suffix}' if entity_id := ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, unique_id): ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) @@ -84,19 +207,14 @@ async def async_setup_entry( """Add sensor from deCONZ.""" sensor = gateway.api.sensors[sensor_id] - for sensor_type, entity_class in ENTITY_CLASSES: - if TYPE_CHECKING: - assert isinstance(entity_class, DeconzBinarySensor) + for description in ENTITY_DESCRIPTIONS: if ( - not isinstance(sensor, sensor_type) - or entity_class.unique_id_suffix is not None - and getattr(sensor, entity_class.unique_id_suffix) is None - ): + description.instance_check + and not isinstance(sensor, description.instance_check) + ) or description.value_fn(sensor) is None: continue - - async_update_unique_id(hass, sensor.unique_id, entity_class) - - async_add_entities([entity_class(sensor, gateway)]) + async_update_unique_id(hass, sensor.unique_id, description) + async_add_entities([DeconzBinarySensor(sensor, gateway, description)]) gateway.register_platform_add_device_callback( async_add_sensor, @@ -104,28 +222,43 @@ async def async_setup_entry( ) -class DeconzBinarySensor(DeconzDevice[_SensorDeviceT], BinarySensorEntity): +class DeconzBinarySensor(DeconzDevice[SensorResources], BinarySensorEntity): """Representation of a deCONZ binary sensor.""" - old_unique_id_suffix = "" TYPE = DOMAIN + entity_description: DeconzBinarySensorDescription - def __init__(self, device: _SensorDeviceT, gateway: DeconzGateway) -> None: + def __init__( + self, + device: SensorResources, + gateway: DeconzGateway, + description: DeconzBinarySensorDescription, + ) -> None: """Initialize deCONZ binary sensor.""" + self.entity_description = description + self.unique_id_suffix = description.key + self._update_key = description.update_key + if description.name_suffix: + self._name_suffix = description.name_suffix super().__init__(device, gateway) if ( - self.unique_id_suffix in PROVIDES_EXTRA_ATTRIBUTES + self.entity_description.key in PROVIDES_EXTRA_ATTRIBUTES and self._update_keys is not None ): self._update_keys.update({"on", "state"}) + @property + def is_on(self) -> bool | None: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self._device) + @property def extra_state_attributes(self) -> dict[str, bool | float | int | list | None]: """Return the state attributes of the sensor.""" attr: dict[str, bool | float | int | list | None] = {} - if self.unique_id_suffix not in PROVIDES_EXTRA_ATTRIBUTES: + if self.entity_description.key not in PROVIDES_EXTRA_ATTRIBUTES: return attr if self._device.on is not None: @@ -145,179 +278,3 @@ class DeconzBinarySensor(DeconzDevice[_SensorDeviceT], BinarySensorEntity): attr[ATTR_VIBRATIONSTRENGTH] = self._device.vibration_strength return attr - - -class DeconzAlarmBinarySensor(DeconzBinarySensor[Alarm]): - """Representation of a deCONZ alarm binary sensor.""" - - unique_id_suffix = "alarm" - _update_key = "alarm" - - _attr_device_class = BinarySensorDeviceClass.SAFETY - - @property - def is_on(self) -> bool: - """Return the state of the sensor.""" - return self._device.alarm - - -class DeconzCarbonMonoxideBinarySensor(DeconzBinarySensor[CarbonMonoxide]): - """Representation of a deCONZ carbon monoxide binary sensor.""" - - unique_id_suffix = "carbon_monoxide" - _update_key = "carbonmonoxide" - - _attr_device_class = BinarySensorDeviceClass.CO - - @property - def is_on(self) -> bool: - """Return the state of the sensor.""" - return self._device.carbon_monoxide - - -class DeconzFireBinarySensor(DeconzBinarySensor[Fire]): - """Representation of a deCONZ fire binary sensor.""" - - unique_id_suffix = "fire" - _update_key = "fire" - - _attr_device_class = BinarySensorDeviceClass.SMOKE - - @property - def is_on(self) -> bool: - """Return the state of the sensor.""" - return self._device.fire - - -class DeconzFireInTestModeBinarySensor(DeconzBinarySensor[Fire]): - """Representation of a deCONZ fire in-test-mode binary sensor.""" - - _name_suffix = "Test Mode" - unique_id_suffix = "in_test_mode" - old_unique_id_suffix = "test mode" - _update_key = "test" - - _attr_device_class = BinarySensorDeviceClass.SMOKE - _attr_entity_category = EntityCategory.DIAGNOSTIC - - @property - def is_on(self) -> bool: - """Return the state of the sensor.""" - return self._device.in_test_mode - - -class DeconzFlagBinarySensor(DeconzBinarySensor[GenericFlag]): - """Representation of a deCONZ generic flag binary sensor.""" - - unique_id_suffix = "flag" - _update_key = "flag" - - @property - def is_on(self) -> bool: - """Return the state of the sensor.""" - return self._device.flag - - -class DeconzOpenCloseBinarySensor(DeconzBinarySensor[OpenClose]): - """Representation of a deCONZ open/close binary sensor.""" - - unique_id_suffix = "open" - _update_key = "open" - - _attr_device_class = BinarySensorDeviceClass.OPENING - - @property - def is_on(self) -> bool: - """Return the state of the sensor.""" - return self._device.open - - -class DeconzPresenceBinarySensor(DeconzBinarySensor[Presence]): - """Representation of a deCONZ presence binary sensor.""" - - unique_id_suffix = "presence" - _update_key = "presence" - - _attr_device_class = BinarySensorDeviceClass.MOTION - - @property - def is_on(self) -> bool: - """Return the state of the sensor.""" - return self._device.presence - - -class DeconzVibrationBinarySensor(DeconzBinarySensor[Vibration]): - """Representation of a deCONZ vibration binary sensor.""" - - unique_id_suffix = "vibration" - _update_key = "vibration" - - _attr_device_class = BinarySensorDeviceClass.VIBRATION - - @property - def is_on(self) -> bool: - """Return the state of the sensor.""" - return self._device.vibration - - -class DeconzWaterBinarySensor(DeconzBinarySensor[Water]): - """Representation of a deCONZ water binary sensor.""" - - unique_id_suffix = "water" - _update_key = "water" - - _attr_device_class = BinarySensorDeviceClass.MOISTURE - - @property - def is_on(self) -> bool: - """Return the state of the sensor.""" - return self._device.water - - -class DeconzTamperedCommonBinarySensor(DeconzBinarySensor[SensorResources]): - """Representation of a deCONZ tampered binary sensor.""" - - _name_suffix = "Tampered" - unique_id_suffix = "tampered" - old_unique_id_suffix = "tampered" - _update_key = "tampered" - - _attr_device_class = BinarySensorDeviceClass.TAMPER - _attr_entity_category = EntityCategory.DIAGNOSTIC - - @property - def is_on(self) -> bool | None: - """Return the state of the sensor.""" - return self._device.tampered - - -class DeconzLowBatteryCommonBinarySensor(DeconzBinarySensor[SensorResources]): - """Representation of a deCONZ low battery binary sensor.""" - - _name_suffix = "Low Battery" - unique_id_suffix = "low_battery" - old_unique_id_suffix = "low battery" - _update_key = "lowbattery" - - _attr_device_class = BinarySensorDeviceClass.BATTERY - _attr_entity_category = EntityCategory.DIAGNOSTIC - - @property - def is_on(self) -> bool | None: - """Return the state of the sensor.""" - return self._device.low_battery - - -ENTITY_CLASSES = ( - (Alarm, DeconzAlarmBinarySensor), - (CarbonMonoxide, DeconzCarbonMonoxideBinarySensor), - (Fire, DeconzFireBinarySensor), - (Fire, DeconzFireInTestModeBinarySensor), - (GenericFlag, DeconzFlagBinarySensor), - (OpenClose, DeconzOpenCloseBinarySensor), - (Presence, DeconzPresenceBinarySensor), - (Vibration, DeconzVibrationBinarySensor), - (Water, DeconzWaterBinarySensor), - (PydeconzSensorBase, DeconzTamperedCommonBinarySensor), - (PydeconzSensorBase, DeconzLowBatteryCommonBinarySensor), -) From 7e19e56c6bbe797fcab6b3c670c1f89373101baa Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 10 Oct 2022 21:40:17 +0200 Subject: [PATCH 320/985] Update frontend to 20221010.0 (#79994) --- 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 6f243da444a..98b978964a6 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==20221006.0"], + "requirements": ["home-assistant-frontend==20221010.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 20d4d940b60..f4b0f3ce25a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.38.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20221006.0 +home-assistant-frontend==20221010.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 40e850750b5..8f05520e4ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -871,7 +871,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221006.0 +home-assistant-frontend==20221010.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 74ae33fd803..28aad9017d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -651,7 +651,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221006.0 +home-assistant-frontend==20221010.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 20d71a869ef0da6ca21e624355b2bb109f5c6ff2 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 10 Oct 2022 15:40:42 -0400 Subject: [PATCH 321/985] Add friendly entity names for ZHA sensors (#80035) * Add friendly entity names for ZHA sensors * lowercase 2nd word --- homeassistant/components/zha/sensor.py | 32 +- tests/components/zha/test_sensor.py | 72 +++-- tests/components/zha/zha_devices_list.py | 364 +++++++++++------------ 3 files changed, 248 insertions(+), 220 deletions(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 74ec924af78..ba4aec66f35 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -216,8 +216,9 @@ class Battery(Sensor): SENSOR_ATTR = "battery_percentage_remaining" _attr_device_class: SensorDeviceClass = SensorDeviceClass.BATTERY _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _unit = PERCENTAGE _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_name: str = "Battery" + _unit = PERCENTAGE @classmethod def create_entity( @@ -268,6 +269,7 @@ class ElectricalMeasurement(Sensor): _attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER _attr_should_poll = True # BaseZhaEntity defaults to False _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _attr_name: str = "Active power" _unit = POWER_WATT _div_mul_prefix = "ac_power" @@ -309,6 +311,7 @@ class ElectricalMeasurementApparentPower( SENSOR_ATTR = "apparent_power" _attr_device_class: SensorDeviceClass = SensorDeviceClass.APPARENT_POWER _attr_should_poll = False # Poll indirectly by ElectricalMeasurementSensor + _attr_name: str = "Apparent power" _unit = POWER_VOLT_AMPERE _div_mul_prefix = "ac_power" @@ -320,6 +323,7 @@ class ElectricalMeasurementRMSCurrent(ElectricalMeasurement, id_suffix="rms_curr SENSOR_ATTR = "rms_current" _attr_device_class: SensorDeviceClass = SensorDeviceClass.CURRENT _attr_should_poll = False # Poll indirectly by ElectricalMeasurementSensor + _attr_name: str = "RMS current" _unit = ELECTRIC_CURRENT_AMPERE _div_mul_prefix = "ac_current" @@ -331,6 +335,7 @@ class ElectricalMeasurementRMSVoltage(ElectricalMeasurement, id_suffix="rms_volt SENSOR_ATTR = "rms_voltage" _attr_device_class: SensorDeviceClass = SensorDeviceClass.CURRENT _attr_should_poll = False # Poll indirectly by ElectricalMeasurementSensor + _attr_name: str = "RMS voltage" _unit = ELECTRIC_POTENTIAL_VOLT _div_mul_prefix = "ac_voltage" @@ -342,6 +347,7 @@ class ElectricalMeasurementFrequency(ElectricalMeasurement, id_suffix="ac_freque SENSOR_ATTR = "ac_frequency" _attr_device_class: SensorDeviceClass = SensorDeviceClass.FREQUENCY _attr_should_poll = False # Poll indirectly by ElectricalMeasurementSensor + _attr_name: str = "AC frequency" _unit = FREQUENCY_HERTZ _div_mul_prefix = "ac_frequency" @@ -353,6 +359,7 @@ class ElectricalMeasurementPowerFactor(ElectricalMeasurement, id_suffix="power_f SENSOR_ATTR = "power_factor" _attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER_FACTOR _attr_should_poll = False # Poll indirectly by ElectricalMeasurementSensor + _attr_name: str = "Power factor" _unit = PERCENTAGE @@ -366,6 +373,7 @@ class Humidity(Sensor): SENSOR_ATTR = "measured_value" _attr_device_class: SensorDeviceClass = SensorDeviceClass.HUMIDITY _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _attr_name: str = "Humidity" _divisor = 100 _unit = PERCENTAGE @@ -377,6 +385,7 @@ class SoilMoisture(Sensor): SENSOR_ATTR = "measured_value" _attr_device_class: SensorDeviceClass = SensorDeviceClass.HUMIDITY _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _attr_name: str = "Soil moisture" _divisor = 100 _unit = PERCENTAGE @@ -388,6 +397,7 @@ class LeafWetness(Sensor): SENSOR_ATTR = "measured_value" _attr_device_class: SensorDeviceClass = SensorDeviceClass.HUMIDITY _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _attr_name: str = "Leaf wetness" _divisor = 100 _unit = PERCENTAGE @@ -399,6 +409,7 @@ class Illuminance(Sensor): SENSOR_ATTR = "measured_value" _attr_device_class: SensorDeviceClass = SensorDeviceClass.ILLUMINANCE _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _attr_name: str = "Illuminance" _unit = LIGHT_LUX def formatter(self, value: int) -> float: @@ -416,6 +427,7 @@ class SmartEnergyMetering(Sensor): SENSOR_ATTR: int | str = "instantaneous_demand" _attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _attr_name: str = "Instantaneous demand" unit_of_measure_map = { 0x00: POWER_WATT, @@ -463,6 +475,7 @@ class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered") SENSOR_ATTR: int | str = "current_summ_delivered" _attr_device_class: SensorDeviceClass = SensorDeviceClass.ENERGY _attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING + _attr_name: str = "Summation delivered" unit_of_measure_map = { 0x00: ENERGY_KILO_WATT_HOUR, @@ -513,6 +526,7 @@ class Pressure(Sensor): SENSOR_ATTR = "measured_value" _attr_device_class: SensorDeviceClass = SensorDeviceClass.PRESSURE _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _attr_name: str = "Pressure" _decimals = 0 _unit = PRESSURE_HPA @@ -524,6 +538,7 @@ class Temperature(Sensor): SENSOR_ATTR = "measured_value" _attr_device_class: SensorDeviceClass = SensorDeviceClass.TEMPERATURE _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _attr_name: str = "Temperature" _divisor = 100 _unit = TEMP_CELSIUS @@ -535,6 +550,7 @@ class DeviceTemperature(Sensor): SENSOR_ATTR = "current_temperature" _attr_device_class: SensorDeviceClass = SensorDeviceClass.TEMPERATURE _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _attr_name: str = "Device temperature" _divisor = 100 _unit = TEMP_CELSIUS _attr_entity_category = EntityCategory.DIAGNOSTIC @@ -547,6 +563,7 @@ class CarbonDioxideConcentration(Sensor): SENSOR_ATTR = "measured_value" _attr_device_class: SensorDeviceClass = SensorDeviceClass.CO2 _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _attr_name: str = "Carbon dioxide concentration" _decimals = 0 _multiplier = 1e6 _unit = CONCENTRATION_PARTS_PER_MILLION @@ -559,6 +576,7 @@ class CarbonMonoxideConcentration(Sensor): SENSOR_ATTR = "measured_value" _attr_device_class: SensorDeviceClass = SensorDeviceClass.CO _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _attr_name: str = "Carbon monoxide concentration" _decimals = 0 _multiplier = 1e6 _unit = CONCENTRATION_PARTS_PER_MILLION @@ -572,6 +590,7 @@ class VOCLevel(Sensor): SENSOR_ATTR = "measured_value" _attr_device_class: SensorDeviceClass = SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _attr_name: str = "VOC level" _decimals = 0 _multiplier = 1e6 _unit = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -588,6 +607,7 @@ class PPBVOCLevel(Sensor): SENSOR_ATTR = "measured_value" _attr_device_class: SensorDeviceClass = SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _attr_name: str = "VOC level" _decimals = 0 _multiplier = 1 _unit = CONCENTRATION_PARTS_PER_BILLION @@ -599,6 +619,7 @@ class PM25(Sensor): SENSOR_ATTR = "measured_value" _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _attr_name: str = "Particulate matter" _decimals = 0 _multiplier = 1 _unit = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER @@ -610,6 +631,7 @@ class FormaldehydeConcentration(Sensor): SENSOR_ATTR = "measured_value" _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _attr_name: str = "Formaldehyde concentration" _decimals = 0 _multiplier = 1e6 _unit = CONCENTRATION_PARTS_PER_MILLION @@ -619,6 +641,8 @@ class FormaldehydeConcentration(Sensor): class ThermostatHVACAction(Sensor, id_suffix="hvac_action"): """Thermostat HVAC action sensor.""" + _attr_name: str = "HVAC action" + @classmethod def create_entity( cls: type[_ThermostatHVACActionSelfT], @@ -744,6 +768,7 @@ class RSSISensor(Sensor, id_suffix="rssi"): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_registry_enabled_default = False _attr_should_poll = True # BaseZhaEntity defaults to False + _attr_name: str = "RSSI" unique_id_suffix: str @classmethod @@ -773,6 +798,8 @@ class RSSISensor(Sensor, id_suffix="rssi"): class LQISensor(RSSISensor, id_suffix="lqi"): """LQI sensor for a device.""" + _attr_name: str = "LQI" + @MULTI_MATCH( channel_names="tuya_manufacturer", @@ -786,6 +813,7 @@ class TimeLeft(Sensor, id_suffix="time_left"): SENSOR_ATTR = "timer_time_left" _attr_device_class: SensorDeviceClass = SensorDeviceClass.DURATION _attr_icon = "mdi:timer" + _attr_name: str = "Time left" _unit = TIME_MINUTES @@ -796,6 +824,7 @@ class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"): SENSOR_ATTR = "device_run_time" _attr_device_class: SensorDeviceClass = SensorDeviceClass.DURATION _attr_icon = "mdi:timer" + _attr_name: str = "Device run time" _unit = TIME_MINUTES @@ -806,4 +835,5 @@ class IkeaFilterRunTime(Sensor, id_suffix="filter_run_time"): SENSOR_ATTR = "filter_run_time" _attr_device_class: SensorDeviceClass = SensorDeviceClass.DURATION _attr_icon = "mdi:timer" + _attr_name: str = "Filter run time" _unit = TIME_MINUTES diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 0698c07db9e..55ea9833caa 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -309,7 +309,7 @@ async def async_test_device_temperature(hass, cluster, entity_id): ), ( smartenergy.Metering.cluster_id, - "smartenergy_metering", + "instantaneous_demand", async_test_metering, 1, { @@ -323,7 +323,7 @@ async def async_test_device_temperature(hass, cluster, entity_id): ), ( smartenergy.Metering.cluster_id, - "smartenergy_summation", + "summation_delivered", async_test_smart_energy_summation, 1, { @@ -339,7 +339,7 @@ async def async_test_device_temperature(hass, cluster, entity_id): ), ( homeautomation.ElectricalMeasurement.cluster_id, - "electrical_measurement", + "active_power", async_test_electrical_measurement, 7, {"ac_power_divisor": 1000, "ac_power_multiplier": 1}, @@ -347,7 +347,7 @@ async def async_test_device_temperature(hass, cluster, entity_id): ), ( homeautomation.ElectricalMeasurement.cluster_id, - "electrical_measurement_apparent_power", + "apparent_power", async_test_em_apparent_power, 7, {"ac_power_divisor": 1000, "ac_power_multiplier": 1}, @@ -355,7 +355,7 @@ async def async_test_device_temperature(hass, cluster, entity_id): ), ( homeautomation.ElectricalMeasurement.cluster_id, - "electrical_measurement_rms_current", + "rms_current", async_test_em_rms_current, 7, {"ac_current_divisor": 1000, "ac_current_multiplier": 1}, @@ -363,7 +363,7 @@ async def async_test_device_temperature(hass, cluster, entity_id): ), ( homeautomation.ElectricalMeasurement.cluster_id, - "electrical_measurement_rms_voltage", + "rms_voltage", async_test_em_rms_voltage, 7, {"ac_voltage_divisor": 10, "ac_voltage_multiplier": 1}, @@ -437,7 +437,7 @@ async def test_sensor( zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100 cluster.PLUGGED_ATTR_READS = read_plug zha_device = await zha_device_joined_restored(zigpy_device) - entity_id = ENTITY_ID_PREFIX.format(entity_suffix.replace("_", "")) + entity_id = ENTITY_ID_PREFIX.format(entity_suffix) await async_enable_traffic(hass, [zha_device], enabled=False) await hass.async_block_till_done() @@ -642,37 +642,37 @@ async def test_electrical_measurement_init( homeautomation.ElectricalMeasurement.cluster_id, {"apparent_power", "rms_voltage", "rms_current"}, { - "electrical_measurement", - "electrical_measurement_frequency", - "electrical_measurement_power_factor", + "active_power", + "ac_frequency", + "power_factor", }, { - "electrical_measurement_apparent_power", - "electrical_measurement_rms_voltage", - "electrical_measurement_rms_current", + "apparent_power", + "rms_voltage", + "rms_current", }, ), ( homeautomation.ElectricalMeasurement.cluster_id, {"apparent_power", "rms_current", "ac_frequency", "power_factor"}, - {"electrical_measurement_rms_voltage", "electrical_measurement"}, + {"rms_voltage", "active_power"}, { - "electrical_measurement_apparent_power", - "electrical_measurement_rms_current", - "electrical_measurement_frequency", - "electrical_measurement_power_factor", + "apparent_power", + "rms_current", + "ac_frequency", + "power_factor", }, ), ( homeautomation.ElectricalMeasurement.cluster_id, set(), { - "electrical_measurement_rms_voltage", - "electrical_measurement", - "electrical_measurement_apparent_power", - "electrical_measurement_rms_current", - "electrical_measurement_frequency", - "electrical_measurement_power_factor", + "rms_voltage", + "active_power", + "apparent_power", + "rms_current", + "ac_frequency", + "power_factor", }, set(), ), @@ -682,10 +682,10 @@ async def test_electrical_measurement_init( "instantaneous_demand", }, { - "smartenergy_summation", + "summation_delivered", }, { - "smartenergy_metering", + "instantaneous_demand", }, ), ( @@ -693,16 +693,16 @@ async def test_electrical_measurement_init( {"instantaneous_demand", "current_summ_delivered"}, {}, { - "smartenergy_summation", - "smartenergy_metering", + "summation_delivered", + "instantaneous_demand", }, ), ( smartenergy.Metering.cluster_id, {}, { - "smartenergy_summation", - "smartenergy_metering", + "summation_delivered", + "instantaneous_demand", }, {}, ), @@ -719,10 +719,8 @@ async def test_unsupported_attributes_sensor( ): """Test zha sensor platform.""" - entity_ids = {ENTITY_ID_PREFIX.format(e.replace("_", "")) for e in entity_ids} - missing_entity_ids = { - ENTITY_ID_PREFIX.format(e.replace("_", "")) for e in missing_entity_ids - } + entity_ids = {ENTITY_ID_PREFIX.format(e) for e in entity_ids} + missing_entity_ids = {ENTITY_ID_PREFIX.format(e) for e in missing_entity_ids} zigpy_device = zigpy_device_mock( { @@ -836,7 +834,7 @@ async def test_se_summation_uom( ): """Test zha smart energy summation.""" - entity_id = ENTITY_ID_PREFIX.format("smartenergysummation") + entity_id = ENTITY_ID_PREFIX.format("summation_delivered") zigpy_device = zigpy_device_mock( { 1: { @@ -890,7 +888,7 @@ async def test_elec_measurement_sensor_type( ): """Test zha electrical measurement sensor type.""" - entity_id = ENTITY_ID_PREFIX.format("electricalmeasurement") + entity_id = ENTITY_ID_PREFIX.format("active_power") zigpy_dev = elec_measurement_zigpy_dev zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS[ "measurement_type" @@ -939,7 +937,7 @@ async def test_elec_measurement_skip_unsupported_attribute( ): """Test zha electrical measurement skipping update of unsupported attributes.""" - entity_id = ENTITY_ID_PREFIX.format("electricalmeasurement") + entity_id = ENTITY_ID_PREFIX.format("active_power") zha_dev = elec_measurement_zha_dev cluster = zha_dev.device.endpoints[1].electrical_measurement diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index f79ba06f721..72ce080781d 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -177,14 +177,14 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ "button.centralite_3210_l_identifybutton", - "sensor.centralite_3210_l_electricalmeasurement", - "sensor.centralite_3210_l_electricalmeasurementapparentpower", - "sensor.centralite_3210_l_electricalmeasurementrmscurrent", - "sensor.centralite_3210_l_electricalmeasurementrmsvoltage", - "sensor.centralite_3210_l_electricalmeasurementfrequency", - "sensor.centralite_3210_l_electricalmeasurementpowerfactor", - "sensor.centralite_3210_l_smartenergymetering", - "sensor.centralite_3210_l_smartenergysummation", + "sensor.centralite_3210_l_active_power", + "sensor.centralite_3210_l_apparent_power", + "sensor.centralite_3210_l_rms_current", + "sensor.centralite_3210_l_rms_voltage", + "sensor.centralite_3210_l_ac_frequency", + "sensor.centralite_3210_l_power_factor", + "sensor.centralite_3210_l_instantaneous_demand", + "sensor.centralite_3210_l_summation_delivered", "switch.centralite_3210_l_switch", "sensor.centralite_3210_l_rssi", "sensor.centralite_3210_l_lqi", @@ -203,42 +203,42 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurement", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_active_power", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementapparentpower", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_apparent_power", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementrmscurrent", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_rms_current", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementrmsvoltage", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_rms_voltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementfrequency", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_ac_frequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementpowerfactor", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_power_factor", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_smartenergymetering", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_instantaneous_demand", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_smartenergysummation", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_summation_delivered", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], @@ -590,8 +590,8 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["4:0x0019"], DEV_SIG_ENTITIES: [ "button.climaxtechnology_psmp5_00_00_02_02tc_identifybutton", - "sensor.climaxtechnology_psmp5_00_00_02_02tc_smartenergymetering", - "sensor.climaxtechnology_psmp5_00_00_02_02tc_smartenergysummation", + "sensor.climaxtechnology_psmp5_00_00_02_02tc_instantaneous_demand", + "sensor.climaxtechnology_psmp5_00_00_02_02tc_summation_delivered", "switch.climaxtechnology_psmp5_00_00_02_02tc_switch", "sensor.climaxtechnology_psmp5_00_00_02_02tc_rssi", "sensor.climaxtechnology_psmp5_00_00_02_02tc_lqi", @@ -610,12 +610,12 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_smartenergymetering", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_instantaneous_demand", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_smartenergysummation", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_summation_delivered", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], @@ -1580,8 +1580,8 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006", "2:0x0008"], DEV_SIG_ENTITIES: [ "button.jasco_products_45852_identifybutton", - "sensor.jasco_products_45852_smartenergymetering", - "sensor.jasco_products_45852_smartenergysummation", + "sensor.jasco_products_45852_instantaneous_demand", + "sensor.jasco_products_45852_summation_delivered", "light.jasco_products_45852_light", "sensor.jasco_products_45852_rssi", "sensor.jasco_products_45852_lqi", @@ -1600,12 +1600,12 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_smartenergymetering", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_instantaneous_demand", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_smartenergysummation", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_summation_delivered", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], @@ -1644,8 +1644,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.jasco_products_45856_identifybutton", "light.jasco_products_45856_light", - "sensor.jasco_products_45856_smartenergymetering", - "sensor.jasco_products_45856_smartenergysummation", + "sensor.jasco_products_45856_instantaneous_demand", + "sensor.jasco_products_45856_summation_delivered", "sensor.jasco_products_45856_rssi", "sensor.jasco_products_45856_lqi", ], @@ -1663,12 +1663,12 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_smartenergymetering", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_instantaneous_demand", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_smartenergysummation", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_summation_delivered", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], @@ -1707,8 +1707,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.jasco_products_45857_identifybutton", "light.jasco_products_45857_light", - "sensor.jasco_products_45857_smartenergymetering", - "sensor.jasco_products_45857_smartenergysummation", + "sensor.jasco_products_45857_instantaneous_demand", + "sensor.jasco_products_45857_summation_delivered", "sensor.jasco_products_45857_rssi", "sensor.jasco_products_45857_lqi", ], @@ -1726,12 +1726,12 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_smartenergymetering", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_instantaneous_demand", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_smartenergysummation", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_summation_delivered", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], @@ -2239,19 +2239,19 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ "button.lumi_lumi_plug_maus01_identifybutton", - "sensor.lumi_lumi_plug_maus01_electricalmeasurement", - "sensor.lumi_lumi_plug_maus01_electricalmeasurementapparentpower", - "sensor.lumi_lumi_plug_maus01_electricalmeasurementrmscurrent", - "sensor.lumi_lumi_plug_maus01_electricalmeasurementrmsvoltage", - "sensor.lumi_lumi_plug_maus01_electricalmeasurementfrequency", - "sensor.lumi_lumi_plug_maus01_electricalmeasurementpowerfactor", + "sensor.lumi_lumi_plug_maus01_active_power", + "sensor.lumi_lumi_plug_maus01_apparent_power", + "sensor.lumi_lumi_plug_maus01_rms_current", + "sensor.lumi_lumi_plug_maus01_rms_voltage", + "sensor.lumi_lumi_plug_maus01_ac_frequency", + "sensor.lumi_lumi_plug_maus01_power_factor", "sensor.lumi_lumi_plug_maus01_analoginput", "sensor.lumi_lumi_plug_maus01_analoginput_2", "binary_sensor.lumi_lumi_plug_maus01_binaryinput", "switch.lumi_lumi_plug_maus01_switch", "sensor.lumi_lumi_plug_maus01_rssi", "sensor.lumi_lumi_plug_maus01_lqi", - "sensor.lumi_lumi_plug_maus01_devicetemperature", + "sensor.lumi_lumi_plug_maus01_device_temperature", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { @@ -2262,7 +2262,7 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-2"): { DEV_SIG_CHANNELS: ["device_temperature"], DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_devicetemperature", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_device_temperature", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], @@ -2272,32 +2272,32 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurement", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_active_power", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementapparentpower", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_apparent_power", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementrmscurrent", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_rms_current", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementrmsvoltage", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_rms_voltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementfrequency", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_ac_frequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementpowerfactor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_power_factor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], @@ -2352,15 +2352,15 @@ DEVICES = [ "button.lumi_lumi_relay_c2acn01_identifybutton", "light.lumi_lumi_relay_c2acn01_light", "light.lumi_lumi_relay_c2acn01_light_2", - "sensor.lumi_lumi_relay_c2acn01_electricalmeasurement", - "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementapparentpower", - "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementrmscurrent", - "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementrmsvoltage", - "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementfrequency", - "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementpowerfactor", + "sensor.lumi_lumi_relay_c2acn01_active_power", + "sensor.lumi_lumi_relay_c2acn01_apparent_power", + "sensor.lumi_lumi_relay_c2acn01_rms_current", + "sensor.lumi_lumi_relay_c2acn01_rms_voltage", + "sensor.lumi_lumi_relay_c2acn01_ac_frequency", + "sensor.lumi_lumi_relay_c2acn01_power_factor", "sensor.lumi_lumi_relay_c2acn01_rssi", "sensor.lumi_lumi_relay_c2acn01_lqi", - "sensor.lumi_lumi_relay_c2acn01_devicetemperature", + "sensor.lumi_lumi_relay_c2acn01_device_temperature", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { @@ -2371,7 +2371,7 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-2"): { DEV_SIG_CHANNELS: ["device_temperature"], DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_devicetemperature", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_device_temperature", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], @@ -2381,32 +2381,32 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurement", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_active_power", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementapparentpower", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_apparent_power", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementrmscurrent", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_rms_current", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementrmsvoltage", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_rms_voltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementfrequency", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_ac_frequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementpowerfactor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_power_factor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], @@ -3513,7 +3513,7 @@ DEVICES = [ "binary_sensor.lumi_lumi_sensor_wleak_aq1_iaszone", "sensor.lumi_lumi_sensor_wleak_aq1_rssi", "sensor.lumi_lumi_sensor_wleak_aq1_lqi", - "sensor.lumi_lumi_sensor_wleak_aq1_devicetemperature", + "sensor.lumi_lumi_sensor_wleak_aq1_device_temperature", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { @@ -3524,7 +3524,7 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-2"): { DEV_SIG_CHANNELS: ["device_temperature"], DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_devicetemperature", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_device_temperature", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], @@ -3966,12 +3966,12 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.osram_lightify_rt_tunable_white_identifybutton", "light.osram_lightify_rt_tunable_white_light", - "sensor.osram_lightify_rt_tunable_white_electricalmeasurement", - "sensor.osram_lightify_rt_tunable_white_electricalmeasurementapparentpower", - "sensor.osram_lightify_rt_tunable_white_electricalmeasurementrmscurrent", - "sensor.osram_lightify_rt_tunable_white_electricalmeasurementrmsvoltage", - "sensor.osram_lightify_rt_tunable_white_electricalmeasurementfrequency", - "sensor.osram_lightify_rt_tunable_white_electricalmeasurementpowerfactor", + "sensor.osram_lightify_rt_tunable_white_active_power", + "sensor.osram_lightify_rt_tunable_white_apparent_power", + "sensor.osram_lightify_rt_tunable_white_rms_current", + "sensor.osram_lightify_rt_tunable_white_rms_voltage", + "sensor.osram_lightify_rt_tunable_white_ac_frequency", + "sensor.osram_lightify_rt_tunable_white_power_factor", "sensor.osram_lightify_rt_tunable_white_rssi", "sensor.osram_lightify_rt_tunable_white_lqi", ], @@ -3989,32 +3989,32 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-3-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurement", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_active_power", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementapparentpower", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_apparent_power", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementrmscurrent", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_rms_current", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementrmsvoltage", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_rms_voltage", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementfrequency", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_ac_frequency", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementpowerfactor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_power_factor", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], @@ -4045,12 +4045,12 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["3:0x0019"], DEV_SIG_ENTITIES: [ "button.osram_plug_01_identifybutton", - "sensor.osram_plug_01_electricalmeasurement", - "sensor.osram_plug_01_electricalmeasurementapparentpower", - "sensor.osram_plug_01_electricalmeasurementrmscurrent", - "sensor.osram_plug_01_electricalmeasurementrmsvoltage", - "sensor.osram_plug_01_electricalmeasurementfrequency", - "sensor.osram_plug_01_electricalmeasurementpowerfactor", + "sensor.osram_plug_01_active_power", + "sensor.osram_plug_01_apparent_power", + "sensor.osram_plug_01_rms_current", + "sensor.osram_plug_01_rms_voltage", + "sensor.osram_plug_01_ac_frequency", + "sensor.osram_plug_01_power_factor", "switch.osram_plug_01_switch", "sensor.osram_plug_01_rssi", "sensor.osram_plug_01_lqi", @@ -4069,32 +4069,32 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-3-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurement", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_active_power", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementapparentpower", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_apparent_power", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementrmscurrent", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_rms_current", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementrmsvoltage", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_rms_voltage", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementfrequency", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_ac_frequency", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementpowerfactor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_power_factor", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], @@ -4449,12 +4449,12 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0019"], DEV_SIG_ENTITIES: [ "button.securifi_ltd_unk_model_identifybutton", - "sensor.securifi_ltd_unk_model_electricalmeasurement", - "sensor.securifi_ltd_unk_model_electricalmeasurementapparentpower", - "sensor.securifi_ltd_unk_model_electricalmeasurementrmscurrent", - "sensor.securifi_ltd_unk_model_electricalmeasurementrmsvoltage", - "sensor.securifi_ltd_unk_model_electricalmeasurementfrequency", - "sensor.securifi_ltd_unk_model_electricalmeasurementpowerfactor", + "sensor.securifi_ltd_unk_model_active_power", + "sensor.securifi_ltd_unk_model_apparent_power", + "sensor.securifi_ltd_unk_model_rms_current", + "sensor.securifi_ltd_unk_model_rms_voltage", + "sensor.securifi_ltd_unk_model_ac_frequency", + "sensor.securifi_ltd_unk_model_power_factor", "switch.securifi_ltd_unk_model_switch", "sensor.securifi_ltd_unk_model_rssi", "sensor.securifi_ltd_unk_model_lqi", @@ -4468,32 +4468,32 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurement", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_active_power", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementapparentpower", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_apparent_power", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementrmscurrent", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_rms_current", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementrmsvoltage", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_rms_voltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementfrequency", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_ac_frequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementpowerfactor", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_power_factor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], @@ -4592,14 +4592,14 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006"], DEV_SIG_ENTITIES: [ "button.sercomm_corp_sz_esw01_identifybutton", - "sensor.sercomm_corp_sz_esw01_electricalmeasurement", - "sensor.sercomm_corp_sz_esw01_electricalmeasurementapparentpower", - "sensor.sercomm_corp_sz_esw01_electricalmeasurementrmscurrent", - "sensor.sercomm_corp_sz_esw01_electricalmeasurementrmsvoltage", - "sensor.sercomm_corp_sz_esw01_electricalmeasurementfrequency", - "sensor.sercomm_corp_sz_esw01_electricalmeasurementpowerfactor", - "sensor.sercomm_corp_sz_esw01_smartenergymetering", - "sensor.sercomm_corp_sz_esw01_smartenergysummation", + "sensor.sercomm_corp_sz_esw01_active_power", + "sensor.sercomm_corp_sz_esw01_apparent_power", + "sensor.sercomm_corp_sz_esw01_rms_current", + "sensor.sercomm_corp_sz_esw01_rms_voltage", + "sensor.sercomm_corp_sz_esw01_ac_frequency", + "sensor.sercomm_corp_sz_esw01_power_factor", + "sensor.sercomm_corp_sz_esw01_instantaneous_demand", + "sensor.sercomm_corp_sz_esw01_summation_delivered", "light.sercomm_corp_sz_esw01_light", "sensor.sercomm_corp_sz_esw01_rssi", "sensor.sercomm_corp_sz_esw01_lqi", @@ -4618,42 +4618,42 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurement", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_active_power", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementapparentpower", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_apparent_power", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementrmscurrent", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_rms_current", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementrmsvoltage", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_rms_voltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementfrequency", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_ac_frequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementpowerfactor", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_power_factor", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_smartenergymetering", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_instantaneous_demand", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_smartenergysummation", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_summation_delivered", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], @@ -4746,12 +4746,12 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ "button.sinope_technologies_rm3250zb_identifybutton", - "sensor.sinope_technologies_rm3250zb_electricalmeasurement", - "sensor.sinope_technologies_rm3250zb_electricalmeasurementapparentpower", - "sensor.sinope_technologies_rm3250zb_electricalmeasurementrmscurrent", - "sensor.sinope_technologies_rm3250zb_electricalmeasurementrmsvoltage", - "sensor.sinope_technologies_rm3250zb_electricalmeasurementfrequency", - "sensor.sinope_technologies_rm3250zb_electricalmeasurementpowerfactor", + "sensor.sinope_technologies_rm3250zb_active_power", + "sensor.sinope_technologies_rm3250zb_apparent_power", + "sensor.sinope_technologies_rm3250zb_rms_current", + "sensor.sinope_technologies_rm3250zb_rms_voltage", + "sensor.sinope_technologies_rm3250zb_ac_frequency", + "sensor.sinope_technologies_rm3250zb_power_factor", "switch.sinope_technologies_rm3250zb_switch", "sensor.sinope_technologies_rm3250zb_rssi", "sensor.sinope_technologies_rm3250zb_lqi", @@ -4765,32 +4765,32 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurement", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_active_power", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementapparentpower", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_apparent_power", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementrmscurrent", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_rms_current", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementrmsvoltage", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_rms_voltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementfrequency", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_ac_frequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementpowerfactor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_power_factor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], @@ -4833,14 +4833,14 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ "button.sinope_technologies_th1123zb_identifybutton", - "sensor.sinope_technologies_th1123zb_electricalmeasurement", - "sensor.sinope_technologies_th1123zb_electricalmeasurementapparentpower", - "sensor.sinope_technologies_th1123zb_electricalmeasurementrmscurrent", - "sensor.sinope_technologies_th1123zb_electricalmeasurementrmsvoltage", - "sensor.sinope_technologies_th1123zb_electricalmeasurementfrequency", - "sensor.sinope_technologies_th1123zb_electricalmeasurementpowerfactor", + "sensor.sinope_technologies_th1123zb_active_power", + "sensor.sinope_technologies_th1123zb_apparent_power", + "sensor.sinope_technologies_th1123zb_rms_current", + "sensor.sinope_technologies_th1123zb_rms_voltage", + "sensor.sinope_technologies_th1123zb_ac_frequency", + "sensor.sinope_technologies_th1123zb_power_factor", "sensor.sinope_technologies_th1123zb_temperature", - "sensor.sinope_technologies_th1123zb_sinopehvacaction", + "sensor.sinope_technologies_th1123zb_hvac_action", "climate.sinope_technologies_th1123zb_thermostat", "sensor.sinope_technologies_th1123zb_rssi", "sensor.sinope_technologies_th1123zb_lqi", @@ -4859,32 +4859,32 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurement", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_active_power", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementapparentpower", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_apparent_power", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementrmscurrent", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_rms_current", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementrmsvoltage", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_rms_voltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementfrequency", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_ac_frequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementpowerfactor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_power_factor", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], @@ -4904,7 +4904,7 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_sinopehvacaction", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_hvac_action", }, }, }, @@ -4932,14 +4932,14 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ "button.sinope_technologies_th1124zb_identifybutton", - "sensor.sinope_technologies_th1124zb_electricalmeasurement", - "sensor.sinope_technologies_th1124zb_electricalmeasurementapparentpower", - "sensor.sinope_technologies_th1124zb_electricalmeasurementrmscurrent", - "sensor.sinope_technologies_th1124zb_electricalmeasurementrmsvoltage", - "sensor.sinope_technologies_th1124zb_electricalmeasurementfrequency", - "sensor.sinope_technologies_th1124zb_electricalmeasurementpowerfactor", + "sensor.sinope_technologies_th1124zb_active_power", + "sensor.sinope_technologies_th1124zb_apparent_power", + "sensor.sinope_technologies_th1124zb_rms_current", + "sensor.sinope_technologies_th1124zb_rms_voltage", + "sensor.sinope_technologies_th1124zb_ac_frequency", + "sensor.sinope_technologies_th1124zb_power_factor", "sensor.sinope_technologies_th1124zb_temperature", - "sensor.sinope_technologies_th1124zb_sinopehvacaction", + "sensor.sinope_technologies_th1124zb_hvac_action", "climate.sinope_technologies_th1124zb_thermostat", "sensor.sinope_technologies_th1124zb_rssi", "sensor.sinope_technologies_th1124zb_lqi", @@ -4958,32 +4958,32 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurement", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_active_power", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementapparentpower", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_apparent_power", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementrmscurrent", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_rms_current", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementrmsvoltage", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_rms_voltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementfrequency", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_ac_frequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementpowerfactor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_power_factor", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], @@ -5003,7 +5003,7 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_sinopehvacaction", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_hvac_action", }, }, }, @@ -5024,12 +5024,12 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ "button.smartthings_outletv4_identifybutton", - "sensor.smartthings_outletv4_electricalmeasurement", - "sensor.smartthings_outletv4_electricalmeasurementapparentpower", - "sensor.smartthings_outletv4_electricalmeasurementrmscurrent", - "sensor.smartthings_outletv4_electricalmeasurementrmsvoltage", - "sensor.smartthings_outletv4_electricalmeasurementfrequency", - "sensor.smartthings_outletv4_electricalmeasurementpowerfactor", + "sensor.smartthings_outletv4_active_power", + "sensor.smartthings_outletv4_apparent_power", + "sensor.smartthings_outletv4_rms_current", + "sensor.smartthings_outletv4_rms_voltage", + "sensor.smartthings_outletv4_ac_frequency", + "sensor.smartthings_outletv4_power_factor", "binary_sensor.smartthings_outletv4_binaryinput", "switch.smartthings_outletv4_switch", "sensor.smartthings_outletv4_rssi", @@ -5049,32 +5049,32 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurement", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_active_power", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementapparentpower", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_apparent_power", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementrmscurrent", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_rms_current", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementrmsvoltage", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_rms_voltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementfrequency", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_ac_frequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementpowerfactor", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_power_factor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], @@ -5311,7 +5311,7 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.zen_within_zen_01_identifybutton", "sensor.zen_within_zen_01_battery", - "sensor.zen_within_zen_01_thermostathvacaction", + "sensor.zen_within_zen_01_hvac_action", "climate.zen_within_zen_01_zenwithinthermostat", "sensor.zen_within_zen_01_rssi", "sensor.zen_within_zen_01_lqi", @@ -5345,7 +5345,7 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "ThermostatHVACAction", - DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_thermostathvacaction", + DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_hvac_action", }, }, }, @@ -5494,8 +5494,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.sengled_e11_g13_identifybutton", "light.sengled_e11_g13_mintransitionlight", - "sensor.sengled_e11_g13_smartenergymetering", - "sensor.sengled_e11_g13_smartenergysummation", + "sensor.sengled_e11_g13_instantaneous_demand", + "sensor.sengled_e11_g13_summation_delivered", "sensor.sengled_e11_g13_rssi", "sensor.sengled_e11_g13_lqi", ], @@ -5513,12 +5513,12 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_smartenergymetering", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_instantaneous_demand", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_smartenergysummation", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_summation_delivered", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], @@ -5550,8 +5550,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.sengled_e12_n14_identifybutton", "light.sengled_e12_n14_mintransitionlight", - "sensor.sengled_e12_n14_smartenergymetering", - "sensor.sengled_e12_n14_smartenergysummation", + "sensor.sengled_e12_n14_instantaneous_demand", + "sensor.sengled_e12_n14_summation_delivered", "sensor.sengled_e12_n14_rssi", "sensor.sengled_e12_n14_lqi", ], @@ -5569,12 +5569,12 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_smartenergymetering", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_instantaneous_demand", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_smartenergysummation", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_summation_delivered", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], @@ -5606,8 +5606,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "button.sengled_z01_a19nae26_identifybutton", "light.sengled_z01_a19nae26_mintransitionlight", - "sensor.sengled_z01_a19nae26_smartenergymetering", - "sensor.sengled_z01_a19nae26_smartenergysummation", + "sensor.sengled_z01_a19nae26_instantaneous_demand", + "sensor.sengled_z01_a19nae26_summation_delivered", "sensor.sengled_z01_a19nae26_rssi", "sensor.sengled_z01_a19nae26_lqi", ], @@ -5625,12 +5625,12 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_smartenergymetering", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_instantaneous_demand", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_smartenergysummation", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_summation_delivered", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], @@ -5962,7 +5962,7 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ "sensor.efektalab_ru_efekta_pws_battery", - "sensor.efektalab_ru_efekta_pws_soilmoisture", + "sensor.efektalab_ru_efekta_pws_soil_moisture", "sensor.efektalab_ru_efekta_pws_temperature", "sensor.efektalab_ru_efekta_pws_rssi", "sensor.efektalab_ru_efekta_pws_lqi", @@ -5976,7 +5976,7 @@ DEVICES = [ ("sensor", "00:11:22:33:44:55:66:77-1-1032"): { DEV_SIG_CHANNELS: ["soil_moisture"], DEV_SIG_ENT_MAP_CLASS: "SoilMoisture", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_soilmoisture", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_soil_moisture", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], From e8af0071243cb4eb239ba0744cafae54a0d3618b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 10 Oct 2022 21:42:38 +0200 Subject: [PATCH 322/985] Disable echo for non SQLite databases (#80032) * Disable echo for non SQLite databases * Add test --- homeassistant/components/recorder/core.py | 4 ++- tests/components/recorder/test_init.py | 30 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index c0f19f2e864..0511b42ebe4 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -1122,7 +1122,9 @@ class Recorder(threading.Thread): # it tried to import it below. with contextlib.suppress(ImportError): kwargs["connect_args"] = {"conv": build_mysqldb_conv()} - else: + + # Disable extended logging for non SQLite databases + if not self.db_url.startswith(SQLITE_URL_PREFIX): kwargs["echo"] = False if self._using_file_sqlite: diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 05ae1f1a372..4a801574ebb 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1596,3 +1596,33 @@ async def test_async_block_till_done(hass, async_setup_recorder_instance): states = await instance.async_add_executor_job(_fetch_states) assert len(states) == 2 await hass.async_block_till_done() + + +@pytest.mark.parametrize( + "db_url, echo", + ( + ("sqlite://blabla", None), + ("mariadb://blabla", False), + ("mysql://blabla", False), + ("mariadb+pymysql://blabla", False), + ("mysql+pymysql://blabla", False), + ("postgresql://blabla", False), + ), +) +async def test_disable_echo(hass, db_url, echo, caplog): + """Test echo is disabled for non sqlite databases.""" + recorder_helper.async_initialize_recorder(hass) + + class MockEvent: + def listen(self, _, _2, callback): + callback(None, None) + + mock_event = MockEvent() + with patch( + "homeassistant.components.recorder.core.create_engine" + ) as create_engine_mock, patch( + "homeassistant.components.recorder.core.sqlalchemy_event", mock_event + ): + await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: db_url}}) + create_engine_mock.assert_called_once() + assert create_engine_mock.mock_calls[0][2].get("echo") == echo From aa58d7fbd66efc05b7394703e501c7dd3c449202 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 10 Oct 2022 22:04:41 +0200 Subject: [PATCH 323/985] Fix Netatmo device trigger (#80047) --- .../components/netatmo/device_trigger.py | 8 +++---- .../components/netatmo/test_device_trigger.py | 22 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/netatmo/device_trigger.py b/homeassistant/components/netatmo/device_trigger.py index b037f45533f..c6a519a37d0 100644 --- a/homeassistant/components/netatmo/device_trigger.py +++ b/homeassistant/components/netatmo/device_trigger.py @@ -38,10 +38,10 @@ from .const import ( CONF_SUBTYPE = "subtype" DEVICES = { - "NACamera": INDOOR_CAMERA_TRIGGERS, - "NOC": OUTDOOR_CAMERA_TRIGGERS, - "NATherm1": CLIMATE_TRIGGERS, - "NRV": CLIMATE_TRIGGERS, + "Smart Indoor Camera": INDOOR_CAMERA_TRIGGERS, + "Smart Outdoor Camera": OUTDOOR_CAMERA_TRIGGERS, + "Smart Thermostat": CLIMATE_TRIGGERS, + "Smart Valve": CLIMATE_TRIGGERS, } SUBTYPES = { diff --git a/tests/components/netatmo/test_device_trigger.py b/tests/components/netatmo/test_device_trigger.py index 25b86f8410e..f7a3772b404 100644 --- a/tests/components/netatmo/test_device_trigger.py +++ b/tests/components/netatmo/test_device_trigger.py @@ -47,10 +47,10 @@ def calls(hass): @pytest.mark.parametrize( "platform,device_type,event_types", [ - ("camera", "NOC", OUTDOOR_CAMERA_TRIGGERS), - ("camera", "NACamera", INDOOR_CAMERA_TRIGGERS), - ("climate", "NRV", CLIMATE_TRIGGERS), - ("climate", "NATherm1", CLIMATE_TRIGGERS), + ("camera", "Smart Outdoor Camera", OUTDOOR_CAMERA_TRIGGERS), + ("camera", "Smart Indoor Camera", INDOOR_CAMERA_TRIGGERS), + ("climate", "Smart Valve", CLIMATE_TRIGGERS), + ("climate", "Smart Thermostat", CLIMATE_TRIGGERS), ], ) async def test_get_triggers( @@ -105,15 +105,15 @@ async def test_get_triggers( @pytest.mark.parametrize( "platform,camera_type,event_type", - [("camera", "NOC", trigger) for trigger in OUTDOOR_CAMERA_TRIGGERS] - + [("camera", "NACamera", trigger) for trigger in INDOOR_CAMERA_TRIGGERS] + [("camera", "Smart Outdoor Camera", trigger) for trigger in OUTDOOR_CAMERA_TRIGGERS] + + [("camera", "Smart Indoor Camera", trigger) for trigger in INDOOR_CAMERA_TRIGGERS] + [ - ("climate", "NRV", trigger) + ("climate", "Smart Valve", trigger) for trigger in CLIMATE_TRIGGERS if trigger not in SUBTYPES ] + [ - ("climate", "NATherm1", trigger) + ("climate", "Smart Thermostat", trigger) for trigger in CLIMATE_TRIGGERS if trigger not in SUBTYPES ], @@ -183,12 +183,12 @@ async def test_if_fires_on_event( @pytest.mark.parametrize( "platform,camera_type,event_type,sub_type", [ - ("climate", "NRV", trigger, subtype) + ("climate", "Smart Valve", trigger, subtype) for trigger in SUBTYPES for subtype in SUBTYPES[trigger] ] + [ - ("climate", "NATherm1", trigger, subtype) + ("climate", "Smart Thermostat", trigger, subtype) for trigger in SUBTYPES for subtype in SUBTYPES[trigger] ], @@ -262,7 +262,7 @@ async def test_if_fires_on_event_with_subtype( @pytest.mark.parametrize( "platform,device_type,event_type", - [("climate", "NAPLUG", trigger) for trigger in CLIMATE_TRIGGERS], + [("climate", "NAPlug", trigger) for trigger in CLIMATE_TRIGGERS], ) async def test_if_invalid_device( hass, device_reg, entity_reg, platform, device_type, event_type From b3ad0eebcd87437ea67fd4b7aa9c8b2c0888e0f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Oct 2022 10:10:28 -1000 Subject: [PATCH 324/985] Bump pySwitchbot to 0.19.15 (#79972) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 1d245d3fd81..282bf6aa447 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.19.13"], + "requirements": ["PySwitchbot==0.19.15"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8f05520e4ca..3b4223ba992 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.19.13 +PySwitchbot==0.19.15 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 28aad9017d9..8976e261e6c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.19.13 +PySwitchbot==0.19.15 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From ac44c8e34d659305e7b67064eed8e1813835f379 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 10 Oct 2022 22:15:58 +0200 Subject: [PATCH 325/985] Update codeowners for upnp component (#80042) Drop @ehendrix23 from codeowners --- CODEOWNERS | 4 ++-- homeassistant/components/upnp/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9890a8f3502..529925b90f6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1200,8 +1200,8 @@ build.json @home-assistant/supervisor /tests/components/upcloud/ @scop /homeassistant/components/update/ @home-assistant/core /tests/components/update/ @home-assistant/core -/homeassistant/components/upnp/ @StevenLooman @ehendrix23 -/tests/components/upnp/ @StevenLooman @ehendrix23 +/homeassistant/components/upnp/ @StevenLooman +/tests/components/upnp/ @StevenLooman /homeassistant/components/uptime/ @frenck /tests/components/uptime/ @frenck /homeassistant/components/uptimerobot/ @ludeeus @chemelli74 diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index a4b913ec4c8..8d1912f2fc4 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/upnp", "requirements": ["async-upnp-client==0.31.2", "getmac==0.8.2"], "dependencies": ["network", "ssdp"], - "codeowners": ["@StevenLooman", "@ehendrix23"], + "codeowners": ["@StevenLooman"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" From 65ff7c18d2fe8ba71a121fe6a7c6f1b7697d1704 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 10 Oct 2022 22:16:10 +0200 Subject: [PATCH 326/985] Move options to SelectEntityDescription in senseme (#80016) --- homeassistant/components/senseme/select.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/senseme/select.py b/homeassistant/components/senseme/select.py index 1fedc7f75d4..251e6c385d8 100644 --- a/homeassistant/components/senseme/select.py +++ b/homeassistant/components/senseme/select.py @@ -49,6 +49,7 @@ FAN_SELECTS = [ name="Smart Mode", value_fn=lambda device: SMART_MODE_TO_HASS[device.fan_smartmode], set_fn=_set_smart_mode, + options=list(SMART_MODE_TO_HASS.values()), ), ] @@ -70,7 +71,6 @@ class HASensemeSelect(SensemeEntity, SelectEntity): """SenseME select component.""" entity_description: SenseMESelectEntityDescription - _attr_options = list(SMART_MODE_TO_HASS.values()) def __init__( self, device: SensemeFan, description: SenseMESelectEntityDescription From 395636dfc2d52a17f7b31762c3bf19fbafed1273 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 10 Oct 2022 22:27:21 +0200 Subject: [PATCH 327/985] Bump aiounifi to v39 (#80043) --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index eeb974242e9..365ce086fb0 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==38"], + "requirements": ["aiounifi==39"], "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 3b4223ba992..5aa0073b683 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -279,7 +279,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==38 +aiounifi==39 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8976e261e6c..3597ca0e302 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -254,7 +254,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==38 +aiounifi==39 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From 46aa3b5e3d041c1eefd954b840683b3cf53f8642 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 10 Oct 2022 16:43:31 -0400 Subject: [PATCH 328/985] Bump ZHA dependencies (#80049) --- 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 426ac24bbe3..7067491a12a 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -10,8 +10,8 @@ "zha-quirks==0.0.82", "zigpy-deconz==0.19.0", "zigpy==0.51.3", - "zigpy-xbee==0.16.1", - "zigpy-zigate==0.10.1", + "zigpy-xbee==0.16.2", + "zigpy-zigate==0.10.2", "zigpy-znp==0.9.1" ], "usb": [ diff --git a/requirements_all.txt b/requirements_all.txt index 5aa0073b683..33e9f006e66 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2610,10 +2610,10 @@ ziggo-mediabox-xl==1.1.0 zigpy-deconz==0.19.0 # homeassistant.components.zha -zigpy-xbee==0.16.1 +zigpy-xbee==0.16.2 # homeassistant.components.zha -zigpy-zigate==0.10.1 +zigpy-zigate==0.10.2 # homeassistant.components.zha zigpy-znp==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3597ca0e302..8815bccf204 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1805,10 +1805,10 @@ zha-quirks==0.0.82 zigpy-deconz==0.19.0 # homeassistant.components.zha -zigpy-xbee==0.16.1 +zigpy-xbee==0.16.2 # homeassistant.components.zha -zigpy-zigate==0.10.1 +zigpy-zigate==0.10.2 # homeassistant.components.zha zigpy-znp==0.9.1 From 262d1ad2a0d43b788d0b9e9900db6ebd13acb9eb Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Mon, 10 Oct 2022 14:43:49 -0600 Subject: [PATCH 329/985] Bump pylitterbot to 2022.10.0 (#80050) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index ed813983674..61f74bf5a64 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,7 +3,7 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2022.9.6"], + "requirements": ["pylitterbot==2022.10.0"], "codeowners": ["@natekspencer", "@tkdrob"], "dhcp": [{ "hostname": "litter-robot4" }], "iot_class": "cloud_push", diff --git a/requirements_all.txt b/requirements_all.txt index 33e9f006e66..9bf9f8ef736 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1688,7 +1688,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.9.6 +pylitterbot==2022.10.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.16.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8815bccf204..baee4fb2692 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1189,7 +1189,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.9.6 +pylitterbot==2022.10.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.16.0 From ce4d93b0c1b11c7cde8d9543c1582bf2cc385dd2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 10 Oct 2022 23:05:23 +0200 Subject: [PATCH 330/985] Update google-cloud-texttospeech to 2.12.3 (#80051) --- homeassistant/components/google_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_cloud/manifest.json b/homeassistant/components/google_cloud/manifest.json index 9ce8085d5e8..f919f795181 100644 --- a/homeassistant/components/google_cloud/manifest.json +++ b/homeassistant/components/google_cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "google_cloud", "name": "Google Cloud Platform", "documentation": "https://www.home-assistant.io/integrations/google_cloud", - "requirements": ["google-cloud-texttospeech==2.12.1"], + "requirements": ["google-cloud-texttospeech==2.12.3"], "codeowners": ["@lufton"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 9bf9f8ef736..a0e26209ab9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -777,7 +777,7 @@ goodwe==0.2.18 google-cloud-pubsub==2.11.0 # homeassistant.components.google_cloud -google-cloud-texttospeech==2.12.1 +google-cloud-texttospeech==2.12.3 # homeassistant.components.nest google-nest-sdm==2.0.0 From 4281384d2a0bfde60af20e5b8630c269d40c5704 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Oct 2022 00:40:40 +0200 Subject: [PATCH 331/985] Move options to SelectEntityDescription in lifx (#80015) --- homeassistant/components/lifx/select.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/lifx/select.py b/homeassistant/components/lifx/select.py index a1cfb4624d5..a89159968b1 100644 --- a/homeassistant/components/lifx/select.py +++ b/homeassistant/components/lifx/select.py @@ -16,10 +16,9 @@ INFRARED_BRIGHTNESS_ENTITY = SelectEntityDescription( key=INFRARED_BRIGHTNESS, name="Infrared brightness", entity_category=EntityCategory.CONFIG, + options=list(INFRARED_BRIGHTNESS_VALUES_MAP.values()), ) -INFRARED_BRIGHTNESS_OPTIONS = list(INFRARED_BRIGHTNESS_VALUES_MAP.values()) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -41,7 +40,6 @@ class LIFXInfraredBrightnessSelectEntity(LIFXEntity, SelectEntity): """LIFX Nightvision infrared brightness configuration entity.""" _attr_has_entity_name = True - _attr_options = INFRARED_BRIGHTNESS_OPTIONS def __init__( self, coordinator: LIFXUpdateCoordinator, description: SelectEntityDescription From 7d097d18b0c6041475080b3c400e37b25185faba Mon Sep 17 00:00:00 2001 From: Austin Brunkhorst Date: Mon, 10 Oct 2022 16:14:27 -0700 Subject: [PATCH 332/985] Add support for Snooz BLE devices (#78790) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/components/snooz/__init__.py | 62 ++++ homeassistant/components/snooz/config_flow.py | 206 +++++++++++ homeassistant/components/snooz/const.py | 6 + homeassistant/components/snooz/fan.py | 119 +++++++ homeassistant/components/snooz/manifest.json | 18 + homeassistant/components/snooz/models.py | 15 + homeassistant/components/snooz/strings.json | 27 ++ .../components/snooz/translations/en.json | 27 ++ homeassistant/generated/bluetooth.py | 8 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 5 + mypy.ini | 10 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/snooz/__init__.py | 105 ++++++ tests/components/snooz/conftest.py | 23 ++ tests/components/snooz/test_config.py | 26 ++ tests/components/snooz/test_config_flow.py | 325 ++++++++++++++++++ tests/components/snooz/test_fan.py | 264 ++++++++++++++ 22 files changed, 1257 insertions(+) create mode 100644 homeassistant/components/snooz/__init__.py create mode 100644 homeassistant/components/snooz/config_flow.py create mode 100644 homeassistant/components/snooz/const.py create mode 100644 homeassistant/components/snooz/fan.py create mode 100644 homeassistant/components/snooz/manifest.json create mode 100644 homeassistant/components/snooz/models.py create mode 100644 homeassistant/components/snooz/strings.json create mode 100644 homeassistant/components/snooz/translations/en.json create mode 100644 tests/components/snooz/__init__.py create mode 100644 tests/components/snooz/conftest.py create mode 100644 tests/components/snooz/test_config.py create mode 100644 tests/components/snooz/test_config_flow.py create mode 100644 tests/components/snooz/test_fan.py diff --git a/.coveragerc b/.coveragerc index e4d9a242604..939a6e092b0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1163,6 +1163,7 @@ omit = homeassistant/components/smtp/notify.py homeassistant/components/snapcast/* homeassistant/components/snmp/* + homeassistant/components/snooz/__init__.py homeassistant/components/solaredge/__init__.py homeassistant/components/solaredge/coordinator.py homeassistant/components/solaredge/sensor.py diff --git a/.strict-typing b/.strict-typing index e47bb51f173..1322adf99e1 100644 --- a/.strict-typing +++ b/.strict-typing @@ -240,6 +240,7 @@ homeassistant.components.skybell.* homeassistant.components.slack.* homeassistant.components.sleepiq.* homeassistant.components.smhi.* +homeassistant.components.snooz.* homeassistant.components.sonarr.* homeassistant.components.ssdp.* homeassistant.components.statistics.* diff --git a/CODEOWNERS b/CODEOWNERS index 529925b90f6..08310e49520 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1041,6 +1041,8 @@ build.json @home-assistant/supervisor /homeassistant/components/smhi/ @gjohansson-ST /tests/components/smhi/ @gjohansson-ST /homeassistant/components/sms/ @ocalvo +/homeassistant/components/snooz/ @AustinBrunkhorst +/tests/components/snooz/ @AustinBrunkhorst /homeassistant/components/solaredge/ @frenck /tests/components/solaredge/ @frenck /homeassistant/components/solaredge_local/ @drobtravels @scheric diff --git a/homeassistant/components/snooz/__init__.py b/homeassistant/components/snooz/__init__.py new file mode 100644 index 00000000000..8349f781cf8 --- /dev/null +++ b/homeassistant/components/snooz/__init__.py @@ -0,0 +1,62 @@ +"""The Snooz component.""" +from __future__ import annotations + +import logging + +from pysnooz.device import SnoozDevice + +from homeassistant.components.bluetooth import async_ble_device_from_address +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS, CONF_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN, PLATFORMS +from .models import SnoozConfigurationData + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Snooz device from a config entry.""" + address: str = entry.data[CONF_ADDRESS] + token: str = entry.data[CONF_TOKEN] + + # transitions info logs are verbose. Only enable warnings + logging.getLogger("transitions.core").setLevel(logging.WARNING) + + if not (ble_device := async_ble_device_from_address(hass, address)): + raise ConfigEntryNotReady( + f"Could not find Snooz with address {address}. Try power cycling the device" + ) + + device = SnoozDevice(ble_device, token) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SnoozConfigurationData( + ble_device, device, entry.title + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + return True + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + data: SnoozConfigurationData = hass.data[DOMAIN][entry.entry_id] + if entry.title != data.title: + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + data: SnoozConfigurationData = hass.data[DOMAIN][entry.entry_id] + + # also called by fan entities, but do it here too for good measure + await data.device.async_disconnect() + + hass.data[DOMAIN].pop(entry.entry_id) + + if not hass.config_entries.async_entries(DOMAIN): + hass.data.pop(DOMAIN) + + return unload_ok diff --git a/homeassistant/components/snooz/config_flow.py b/homeassistant/components/snooz/config_flow.py new file mode 100644 index 00000000000..48f9370e403 --- /dev/null +++ b/homeassistant/components/snooz/config_flow.py @@ -0,0 +1,206 @@ +"""Config flow for Snooz component.""" +from __future__ import annotations + +import asyncio +from dataclasses import dataclass +from typing import Any + +from pysnooz.advertisement import SnoozAdvertisementData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothScanningMode, + BluetoothServiceInfo, + async_discovered_service_info, + async_process_advertisements, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TOKEN +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + +# number of seconds to wait for a device to be put in pairing mode +WAIT_FOR_PAIRING_TIMEOUT = 30 + + +@dataclass +class DiscoveredSnooz: + """Represents a discovered Snooz device.""" + + info: BluetoothServiceInfo + device: SnoozAdvertisementData + + +class SnoozConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Snooz.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery: DiscoveredSnooz | None = None + self._discovered_devices: dict[str, DiscoveredSnooz] = {} + self._pairing_task: asyncio.Task | None = None + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = SnoozAdvertisementData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery = DiscoveredSnooz(discovery_info, device) + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovery is not None + + if user_input is not None: + if not self._discovery.device.is_pairing: + return await self.async_step_wait_for_pairing_mode() + + return self._create_snooz_entry(self._discovery) + + self._set_confirm_only() + assert self._discovery.device.display_name + placeholders = {"name": self._discovery.device.display_name} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + name = user_input[CONF_NAME] + + discovered = self._discovered_devices.get(name) + + assert discovered is not None + + self._discovery = discovered + + if not discovered.device.is_pairing: + return await self.async_step_wait_for_pairing_mode() + + address = discovered.info.address + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self._create_snooz_entry(discovered) + + configured_addresses = self._async_current_ids() + + for info in async_discovered_service_info(self.hass): + address = info.address + if address in configured_addresses: + continue + device = SnoozAdvertisementData() + if device.supported(info): + assert device.display_name + self._discovered_devices[device.display_name] = DiscoveredSnooz( + info, device + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_NAME): vol.In( + [ + d.device.display_name + for d in self._discovered_devices.values() + ] + ) + } + ), + ) + + async def async_step_wait_for_pairing_mode( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Wait for device to enter pairing mode.""" + if not self._pairing_task: + self._pairing_task = self.hass.async_create_task( + self._async_wait_for_pairing_mode() + ) + return self.async_show_progress( + step_id="wait_for_pairing_mode", + progress_action="wait_for_pairing_mode", + ) + + try: + await self._pairing_task + except asyncio.TimeoutError: + self._pairing_task = None + return self.async_show_progress_done(next_step_id="pairing_timeout") + + self._pairing_task = None + + return self.async_show_progress_done(next_step_id="pairing_complete") + + async def async_step_pairing_complete( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Create a configuration entry for a device that entered pairing mode.""" + assert self._discovery + + await self.async_set_unique_id( + self._discovery.info.address, raise_on_progress=False + ) + self._abort_if_unique_id_configured() + + return self._create_snooz_entry(self._discovery) + + async def async_step_pairing_timeout( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Inform the user that the device never entered pairing mode.""" + if user_input is not None: + return await self.async_step_wait_for_pairing_mode() + + self._set_confirm_only() + return self.async_show_form(step_id="pairing_timeout") + + def _create_snooz_entry(self, discovery: DiscoveredSnooz) -> FlowResult: + assert discovery.device.display_name + return self.async_create_entry( + title=discovery.device.display_name, + data={ + CONF_ADDRESS: discovery.info.address, + CONF_TOKEN: discovery.device.pairing_token, + }, + ) + + async def _async_wait_for_pairing_mode(self) -> None: + """Process advertisements until pairing mode is detected.""" + assert self._discovery + device = self._discovery.device + + def is_device_in_pairing_mode( + service_info: BluetoothServiceInfo, + ) -> bool: + return device.supported(service_info) and device.is_pairing + + try: + await async_process_advertisements( + self.hass, + is_device_in_pairing_mode, + {"address": self._discovery.info.address}, + BluetoothScanningMode.ACTIVE, + WAIT_FOR_PAIRING_TIMEOUT, + ) + finally: + self.hass.async_create_task( + self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) + ) diff --git a/homeassistant/components/snooz/const.py b/homeassistant/components/snooz/const.py new file mode 100644 index 00000000000..9ce16b80e05 --- /dev/null +++ b/homeassistant/components/snooz/const.py @@ -0,0 +1,6 @@ +"""Constants for the Snooz component.""" + +from homeassistant.const import Platform + +DOMAIN = "snooz" +PLATFORMS: list[Platform] = [Platform.FAN] diff --git a/homeassistant/components/snooz/fan.py b/homeassistant/components/snooz/fan.py new file mode 100644 index 00000000000..8ad2372924b --- /dev/null +++ b/homeassistant/components/snooz/fan.py @@ -0,0 +1,119 @@ +"""Fan representation of a Snooz device.""" +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + +from pysnooz.api import UnknownSnoozState +from pysnooz.commands import ( + SnoozCommandData, + SnoozCommandResultStatus, + set_volume, + turn_off, + turn_on, +) + +from homeassistant.components.fan import ATTR_PERCENTAGE, FanEntity, FanEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity + +from .const import DOMAIN +from .models import SnoozConfigurationData + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Snooz device from a config entry.""" + + data: SnoozConfigurationData = hass.data[DOMAIN][entry.entry_id] + + async_add_entities([SnoozFan(data)]) + + +class SnoozFan(FanEntity, RestoreEntity): + """Fan representation of a Snooz device.""" + + def __init__(self, data: SnoozConfigurationData) -> None: + """Initialize a Snooz fan entity.""" + self._device = data.device + self._attr_name = data.title + self._attr_unique_id = data.device.address + self._attr_supported_features = FanEntityFeature.SET_SPEED + self._attr_should_poll = False + self._is_on: bool | None = None + self._percentage: int | None = None + + @callback + def _async_write_state_changed(self) -> None: + # cache state for restore entity + if not self.assumed_state: + self._is_on = self._device.state.on + self._percentage = self._device.state.volume + + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Restore state and subscribe to device events.""" + await super().async_added_to_hass() + + if last_state := await self.async_get_last_state(): + if last_state.state in (STATE_ON, STATE_OFF): + self._is_on = last_state.state == STATE_ON + else: + self._is_on = None + self._percentage = last_state.attributes.get(ATTR_PERCENTAGE) + + self.async_on_remove(self._async_subscribe_to_device_change()) + + @callback + def _async_subscribe_to_device_change(self) -> Callable[[], None]: + return self._device.subscribe_to_state_change(self._async_write_state_changed) + + @property + def percentage(self) -> int | None: + """Volume level of the device.""" + return self._percentage if self.assumed_state else self._device.state.volume + + @property + def is_on(self) -> bool | None: + """Power state of the device.""" + return self._is_on if self.assumed_state else self._device.state.on + + @property + def assumed_state(self) -> bool: + """Return True if unable to access real state of the entity.""" + return not self._device.is_connected or self._device.state is UnknownSnoozState + + async def async_turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Turn on the device.""" + await self._async_execute_command(turn_on(percentage)) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the device.""" + await self._async_execute_command(turn_off()) + + async def async_set_percentage(self, percentage: int) -> None: + """Set the volume of the device. A value of 0 will turn off the device.""" + await self._async_execute_command( + set_volume(percentage) if percentage > 0 else turn_off() + ) + + async def _async_execute_command(self, command: SnoozCommandData) -> None: + result = await self._device.async_execute_command(command) + + if result.status == SnoozCommandResultStatus.SUCCESSFUL: + self._async_write_state_changed() + elif result.status != SnoozCommandResultStatus.CANCELLED: + raise HomeAssistantError( + f"Command {command} failed with status {result.status.name} after {result.duration}" + ) diff --git a/homeassistant/components/snooz/manifest.json b/homeassistant/components/snooz/manifest.json new file mode 100644 index 00000000000..1384767e8b8 --- /dev/null +++ b/homeassistant/components/snooz/manifest.json @@ -0,0 +1,18 @@ +{ + "domain": "snooz", + "name": "Snooz", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/snooz", + "requirements": ["pysnooz==0.8.2"], + "dependencies": ["bluetooth"], + "codeowners": ["@AustinBrunkhorst"], + "bluetooth": [ + { + "local_name": "Snooz*" + }, + { + "service_uuid": "729f0608-496a-47fe-a124-3a62aaa3fbc0" + } + ], + "iot_class": "local_push" +} diff --git a/homeassistant/components/snooz/models.py b/homeassistant/components/snooz/models.py new file mode 100644 index 00000000000..d1c49fe9dc6 --- /dev/null +++ b/homeassistant/components/snooz/models.py @@ -0,0 +1,15 @@ +"""Data models for the Snooz component.""" + +from dataclasses import dataclass + +from bleak.backends.device import BLEDevice +from pysnooz.device import SnoozDevice + + +@dataclass +class SnoozConfigurationData: + """Configuration data for Snooz.""" + + ble_device: BLEDevice + device: SnoozDevice + title: str diff --git a/homeassistant/components/snooz/strings.json b/homeassistant/components/snooz/strings.json new file mode 100644 index 00000000000..2f957f87072 --- /dev/null +++ b/homeassistant/components/snooz/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + }, + "pairing_timeout": { + "description": "The device did not enter pairing mode. Click Submit to try again.\n\n### Troubleshooting\n1. Check that the device isn't connected to the mobile app.\n2. Unplug the device for 5 seconds, then plug it back in." + } + }, + "progress": { + "wait_for_pairing_mode": "To complete setup, put this device in pairing mode.\n\n### How to enter pairing mode\n1. Force quit SNOOZ mobile apps.\n2. Press and hold the power button on the device. Release when the lights start blinking (approximately 5 seconds)." + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/snooz/translations/en.json b/homeassistant/components/snooz/translations/en.json new file mode 100644 index 00000000000..a536a87be5b --- /dev/null +++ b/homeassistant/components/snooz/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network" + }, + "flow_title": "{name}", + "progress": { + "wait_for_pairing_mode": "To complete setup, put this device in pairing mode.\n\n### How to enter pairing mode\n1. Force quit SNOOZ mobile apps.\n2. Press and hold the power button on the device. Release when the lights start blinking (approximately 5 seconds)." + }, + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "pairing_timeout": { + "description": "The device did not enter pairing mode. Click Submit to try again.\n\n### Troubleshooting\n1. Check that the device isn't connected to the mobile app.\n2. Unplug the device for 5 seconds, then plug it back in." + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index b24d9e1986e..afb35e40e83 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -269,6 +269,14 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "local_name": "SensorPush*", "connectable": False, }, + { + "domain": "snooz", + "local_name": "Snooz*", + }, + { + "domain": "snooz", + "service_uuid": "729f0608-496a-47fe-a124-3a62aaa3fbc0", + }, { "domain": "switchbot", "service_data_uuid": "00000d00-0000-1000-8000-00805f9b34fb", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 98f1d3ab7f8..b56e712ae27 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -356,6 +356,7 @@ FLOWS = { "smarttub", "smhi", "sms", + "snooz", "solaredge", "solarlog", "solax", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 1973933d735..00a3848f8c4 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3964,6 +3964,11 @@ "iot_class": "local_polling", "name": "SNMP" }, + "snooz": { + "config_flow": true, + "iot_class": "local_push", + "name": "Snooz" + }, "solaredge": { "name": "SolarEdge", "integrations": { diff --git a/mypy.ini b/mypy.ini index b96efc4b8c3..63e35c3076e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2152,6 +2152,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.snooz.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.sonarr.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index a0e26209ab9..62ad00ed40b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1910,6 +1910,9 @@ pysml==0.0.8 # homeassistant.components.snmp pysnmplib==5.0.15 +# homeassistant.components.snooz +pysnooz==0.8.2 + # homeassistant.components.soma pysoma==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index baee4fb2692..a92a76db6fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1345,6 +1345,9 @@ pysmartthings==0.7.6 # homeassistant.components.snmp pysnmplib==5.0.15 +# homeassistant.components.snooz +pysnooz==0.8.2 + # homeassistant.components.soma pysoma==0.0.10 diff --git a/tests/components/snooz/__init__.py b/tests/components/snooz/__init__.py new file mode 100644 index 00000000000..d5802642c37 --- /dev/null +++ b/tests/components/snooz/__init__.py @@ -0,0 +1,105 @@ +"""Tests for the Snooz component.""" +from __future__ import annotations + +from dataclasses import dataclass +from unittest.mock import patch + +from bleak import BLEDevice +from pysnooz.commands import SnoozCommandData +from pysnooz.testing import MockSnoozDevice + +from homeassistant.components.snooz.const import DOMAIN +from homeassistant.const import CONF_ADDRESS, CONF_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +from tests.common import MockConfigEntry + +TEST_ADDRESS = "00:00:00:00:AB:CD" +TEST_SNOOZ_LOCAL_NAME = "Snooz-ABCD" +TEST_SNOOZ_DISPLAY_NAME = "Snooz ABCD" +TEST_PAIRING_TOKEN = "deadbeef" + +NOT_SNOOZ_SERVICE_INFO = BluetoothServiceInfo( + name="Definitely not snooz", + address=TEST_ADDRESS, + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +SNOOZ_SERVICE_INFO_PAIRING = BluetoothServiceInfo( + name=TEST_SNOOZ_LOCAL_NAME, + address=TEST_ADDRESS, + rssi=-63, + manufacturer_data={65552: bytes([4]) + bytes.fromhex(TEST_PAIRING_TOKEN)}, + service_uuids=[ + "80c37f00-cc16-11e4-8830-0800200c9a66", + "90759319-1668-44da-9ef3-492d593bd1e5", + ], + service_data={}, + source="local", +) + +SNOOZ_SERVICE_INFO_NOT_PAIRING = BluetoothServiceInfo( + name=TEST_SNOOZ_LOCAL_NAME, + address=TEST_ADDRESS, + rssi=-63, + manufacturer_data={65552: bytes([4]) + bytes([0] * 8)}, + service_uuids=[ + "80c37f00-cc16-11e4-8830-0800200c9a66", + "90759319-1668-44da-9ef3-492d593bd1e5", + ], + service_data={}, + source="local", +) + + +@dataclass +class SnoozFixture: + """Snooz test fixture.""" + + entry: MockConfigEntry + device: MockSnoozDevice + + +async def create_mock_snooz( + connected: bool = True, + initial_state: SnoozCommandData = SnoozCommandData(on=False, volume=0), +) -> MockSnoozDevice: + """Create a mock device.""" + + ble_device = SNOOZ_SERVICE_INFO_NOT_PAIRING + device = MockSnoozDevice(ble_device, initial_state=initial_state) + + # execute a command to initiate the connection + if connected is True: + await device.async_execute_command(initial_state) + + return device + + +async def create_mock_snooz_config_entry( + hass: HomeAssistant, device: MockSnoozDevice +) -> MockConfigEntry: + """Create a mock config entry.""" + + with patch( + "homeassistant.components.snooz.SnoozDevice", return_value=device + ), patch( + "homeassistant.components.snooz.async_ble_device_from_address", + return_value=BLEDevice(device.address, device.name), + ): + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_ADDRESS, + data={CONF_ADDRESS: TEST_ADDRESS, CONF_TOKEN: TEST_PAIRING_TOKEN}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/snooz/conftest.py b/tests/components/snooz/conftest.py new file mode 100644 index 00000000000..f99dcfeba72 --- /dev/null +++ b/tests/components/snooz/conftest.py @@ -0,0 +1,23 @@ +"""Snooz test fixtures and configuration.""" +from __future__ import annotations + +import pytest + +from homeassistant.core import HomeAssistant + +from . import SnoozFixture, create_mock_snooz, create_mock_snooz_config_entry + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" + + +@pytest.fixture() +async def mock_connected_snooz(hass: HomeAssistant): + """Mock a Snooz configuration entry and device.""" + + device = await create_mock_snooz() + entry = await create_mock_snooz_config_entry(hass, device) + + yield SnoozFixture(entry, device) diff --git a/tests/components/snooz/test_config.py b/tests/components/snooz/test_config.py new file mode 100644 index 00000000000..e8848aa48e0 --- /dev/null +++ b/tests/components/snooz/test_config.py @@ -0,0 +1,26 @@ +"""Test Snooz configuration.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant + +from . import SnoozFixture + + +async def test_removing_entry_cleans_up_connections( + hass: HomeAssistant, mock_connected_snooz: SnoozFixture +): + """Tests setup and removal of a config entry, ensuring connections are cleaned up.""" + await hass.config_entries.async_remove(mock_connected_snooz.entry.entry_id) + await hass.async_block_till_done() + + assert not mock_connected_snooz.device.is_connected + + +async def test_reloading_entry_cleans_up_connections( + hass: HomeAssistant, mock_connected_snooz: SnoozFixture +): + """Test reloading an entry disconnects any existing connections.""" + await hass.config_entries.async_reload(mock_connected_snooz.entry.entry_id) + await hass.async_block_till_done() + + assert not mock_connected_snooz.device.is_connected diff --git a/tests/components/snooz/test_config_flow.py b/tests/components/snooz/test_config_flow.py new file mode 100644 index 00000000000..65076bf2e03 --- /dev/null +++ b/tests/components/snooz/test_config_flow.py @@ -0,0 +1,325 @@ +"""Test the Snooz config flow.""" +from __future__ import annotations + +import asyncio +from asyncio import Event +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.snooz import DOMAIN +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import ( + NOT_SNOOZ_SERVICE_INFO, + SNOOZ_SERVICE_INFO_NOT_PAIRING, + SNOOZ_SERVICE_INFO_PAIRING, + TEST_ADDRESS, + TEST_PAIRING_TOKEN, + TEST_SNOOZ_DISPLAY_NAME, +) + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass: HomeAssistant): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SNOOZ_SERVICE_INFO_PAIRING, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + await _test_setup_entry(hass, result["flow_id"]) + + +async def test_async_step_bluetooth_waits_to_pair(hass: HomeAssistant): + """Test discovery via bluetooth with a device that's not in pairing mode, but enters pairing mode to complete setup.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SNOOZ_SERVICE_INFO_NOT_PAIRING, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + await _test_pairs(hass, result["flow_id"]) + + +async def test_async_step_bluetooth_retries_pairing(hass: HomeAssistant): + """Test discovery via bluetooth with a device that's not in pairing mode, times out waiting, but eventually complete setup.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SNOOZ_SERVICE_INFO_NOT_PAIRING, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + retry_id = await _test_pairs_timeout(hass, result["flow_id"]) + await _test_pairs(hass, retry_id) + + +async def test_async_step_bluetooth_not_snooz(hass: HomeAssistant): + """Test discovery via bluetooth not Snooz.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_SNOOZ_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass: HomeAssistant): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass: HomeAssistant): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.snooz.config_flow.async_discovered_service_info", + return_value=[SNOOZ_SERVICE_INFO_PAIRING], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["data_schema"] + # ensure discovered devices are listed as options + assert result["data_schema"].schema["name"].container == [TEST_SNOOZ_DISPLAY_NAME] + await _test_setup_entry( + hass, result["flow_id"], {CONF_NAME: TEST_SNOOZ_DISPLAY_NAME} + ) + + +async def test_async_step_user_with_found_devices_waits_to_pair(hass: HomeAssistant): + """Test setup from service info cache with devices found that require pairing mode.""" + with patch( + "homeassistant.components.snooz.config_flow.async_discovered_service_info", + return_value=[SNOOZ_SERVICE_INFO_NOT_PAIRING], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + await _test_pairs(hass, result["flow_id"], {CONF_NAME: TEST_SNOOZ_DISPLAY_NAME}) + + +async def test_async_step_user_with_found_devices_retries_pairing(hass: HomeAssistant): + """Test setup from service info cache with devices found that require pairing mode, times out, then completes.""" + with patch( + "homeassistant.components.snooz.config_flow.async_discovered_service_info", + return_value=[SNOOZ_SERVICE_INFO_NOT_PAIRING], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + user_input = {CONF_NAME: TEST_SNOOZ_DISPLAY_NAME} + + retry_id = await _test_pairs_timeout(hass, result["flow_id"], user_input) + await _test_pairs(hass, retry_id, user_input) + + +async def test_async_step_user_device_added_between_steps(hass: HomeAssistant): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.snooz.config_flow.async_discovered_service_info", + return_value=[SNOOZ_SERVICE_INFO_PAIRING], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_ADDRESS, + data={CONF_NAME: TEST_SNOOZ_DISPLAY_NAME, CONF_TOKEN: TEST_PAIRING_TOKEN}, + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.snooz.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_NAME: TEST_SNOOZ_DISPLAY_NAME}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_async_step_user_with_found_devices_already_setup(hass: HomeAssistant): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_ADDRESS, + data={CONF_NAME: TEST_SNOOZ_DISPLAY_NAME, CONF_TOKEN: TEST_PAIRING_TOKEN}, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.snooz.config_flow.async_discovered_service_info", + return_value=[SNOOZ_SERVICE_INFO_PAIRING], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_ADDRESS, + data={CONF_NAME: TEST_SNOOZ_DISPLAY_NAME, CONF_TOKEN: TEST_PAIRING_TOKEN}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SNOOZ_SERVICE_INFO_PAIRING, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SNOOZ_SERVICE_INFO_PAIRING, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SNOOZ_SERVICE_INFO_PAIRING, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass: HomeAssistant): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SNOOZ_SERVICE_INFO_PAIRING, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.snooz.config_flow.async_discovered_service_info", + return_value=[SNOOZ_SERVICE_INFO_PAIRING], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + await _test_setup_entry( + hass, result["flow_id"], {CONF_NAME: TEST_SNOOZ_DISPLAY_NAME} + ) + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress() + + +async def _test_pairs( + hass: HomeAssistant, flow_id: str, user_input: dict | None = None +) -> None: + pairing_mode_entered = Event() + + async def _async_process_advertisements( + _hass, _callback, _matcher, _mode, _timeout + ): + await pairing_mode_entered.wait() + service_info = SNOOZ_SERVICE_INFO_PAIRING + assert _callback(service_info) + return service_info + + with patch( + "homeassistant.components.snooz.config_flow.async_process_advertisements", + _async_process_advertisements, + ): + result = await hass.config_entries.flow.async_configure( + flow_id, + user_input=user_input or {}, + ) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "wait_for_pairing_mode" + + pairing_mode_entered.set() + await hass.async_block_till_done() + + await _test_setup_entry(hass, result["flow_id"], user_input) + + +async def _test_pairs_timeout( + hass: HomeAssistant, flow_id: str, user_input: dict | None = None +) -> str: + with patch( + "homeassistant.components.snooz.config_flow.async_process_advertisements", + side_effect=asyncio.TimeoutError(), + ): + result = await hass.config_entries.flow.async_configure( + flow_id, user_input=user_input or {} + ) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "wait_for_pairing_mode" + await hass.async_block_till_done() + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "pairing_timeout" + + return result2["flow_id"] + + +async def _test_setup_entry( + hass: HomeAssistant, flow_id: str, user_input: dict | None = None +) -> None: + with patch("homeassistant.components.snooz.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + flow_id, + user_input=user_input or {}, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_ADDRESS: TEST_ADDRESS, + CONF_TOKEN: TEST_PAIRING_TOKEN, + } + assert result["result"].unique_id == TEST_ADDRESS diff --git a/tests/components/snooz/test_fan.py b/tests/components/snooz/test_fan.py new file mode 100644 index 00000000000..30528336e2d --- /dev/null +++ b/tests/components/snooz/test_fan.py @@ -0,0 +1,264 @@ +"""Test Snooz fan entity.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import Mock + +from pysnooz.api import SnoozDeviceState, UnknownSnoozState +from pysnooz.commands import SnoozCommandResult, SnoozCommandResultStatus +from pysnooz.testing import MockSnoozDevice +import pytest + +from homeassistant.components import fan +from homeassistant.components.snooz.const import DOMAIN +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry + +from . import SnoozFixture, create_mock_snooz, create_mock_snooz_config_entry + + +async def test_turn_on(hass: HomeAssistant, snooz_fan_entity_id: str): + """Test turning on the device.""" + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [snooz_fan_entity_id]}, + blocking=True, + ) + + state = hass.states.get(snooz_fan_entity_id) + assert state.state == STATE_ON + assert ATTR_ASSUMED_STATE not in state.attributes + + +@pytest.mark.parametrize("percentage", [1, 22, 50, 99, 100]) +async def test_turn_on_with_percentage( + hass: HomeAssistant, snooz_fan_entity_id: str, percentage: int +): + """Test turning on the device with a percentage.""" + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [snooz_fan_entity_id], fan.ATTR_PERCENTAGE: percentage}, + blocking=True, + ) + + state = hass.states.get(snooz_fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_PERCENTAGE] == percentage + assert ATTR_ASSUMED_STATE not in state.attributes + + +@pytest.mark.parametrize("percentage", [1, 22, 50, 99, 100]) +async def test_set_percentage( + hass: HomeAssistant, snooz_fan_entity_id: str, percentage: int +): + """Test setting the fan percentage.""" + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: [snooz_fan_entity_id], fan.ATTR_PERCENTAGE: percentage}, + blocking=True, + ) + + state = hass.states.get(snooz_fan_entity_id) + assert state.attributes[fan.ATTR_PERCENTAGE] == percentage + assert ATTR_ASSUMED_STATE not in state.attributes + + +async def test_set_0_percentage_turns_off( + hass: HomeAssistant, snooz_fan_entity_id: str +): + """Test turning off the device by setting the percentage/volume to 0.""" + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [snooz_fan_entity_id], fan.ATTR_PERCENTAGE: 66}, + blocking=True, + ) + + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: [snooz_fan_entity_id], fan.ATTR_PERCENTAGE: 0}, + blocking=True, + ) + + state = hass.states.get(snooz_fan_entity_id) + assert state.state == STATE_OFF + # doesn't overwrite percentage when turning off + assert state.attributes[fan.ATTR_PERCENTAGE] == 66 + assert ATTR_ASSUMED_STATE not in state.attributes + + +async def test_turn_off(hass: HomeAssistant, snooz_fan_entity_id: str): + """Test turning off the device.""" + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: [snooz_fan_entity_id]}, + blocking=True, + ) + + state = hass.states.get(snooz_fan_entity_id) + assert state.state == STATE_OFF + assert ATTR_ASSUMED_STATE not in state.attributes + + +async def test_push_events( + hass: HomeAssistant, mock_connected_snooz: SnoozFixture, snooz_fan_entity_id: str +): + """Test state update events from snooz device.""" + mock_connected_snooz.device.trigger_state(SnoozDeviceState(False, 64)) + + state = hass.states.get(snooz_fan_entity_id) + assert ATTR_ASSUMED_STATE not in state.attributes + assert state.state == STATE_OFF + assert state.attributes[fan.ATTR_PERCENTAGE] == 64 + + mock_connected_snooz.device.trigger_state(SnoozDeviceState(True, 12)) + + state = hass.states.get(snooz_fan_entity_id) + assert ATTR_ASSUMED_STATE not in state.attributes + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_PERCENTAGE] == 12 + + mock_connected_snooz.device.trigger_disconnect() + + state = hass.states.get(snooz_fan_entity_id) + assert state.attributes[ATTR_ASSUMED_STATE] is True + + +async def test_restore_state(hass: HomeAssistant): + """Tests restoring entity state.""" + device = await create_mock_snooz(connected=False, initial_state=UnknownSnoozState) + + entry = await create_mock_snooz_config_entry(hass, device) + entity_id = get_fan_entity_id(hass, device) + + # call service to store state + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [entity_id], fan.ATTR_PERCENTAGE: 33}, + blocking=True, + ) + + # unload entry + await hass.config_entries.async_unload(entry.entry_id) + + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + + # reload entry + await create_mock_snooz_config_entry(hass, device) + + # should match last known state + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_PERCENTAGE] == 33 + assert state.attributes[ATTR_ASSUMED_STATE] is True + + +async def test_restore_unknown_state(hass: HomeAssistant): + """Tests restoring entity state that was unknown.""" + device = await create_mock_snooz(connected=False, initial_state=UnknownSnoozState) + + entry = await create_mock_snooz_config_entry(hass, device) + entity_id = get_fan_entity_id(hass, device) + + # unload entry + await hass.config_entries.async_unload(entry.entry_id) + + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + + # reload entry + await create_mock_snooz_config_entry(hass, device) + + # should match last known state + state = hass.states.get(entity_id) + assert state.state == STATE_UNKNOWN + + +async def test_command_results( + hass: HomeAssistant, mock_connected_snooz: SnoozFixture, snooz_fan_entity_id: str +): + """Test device command results.""" + mock_execute = Mock(spec=mock_connected_snooz.device.async_execute_command) + + mock_connected_snooz.device.async_execute_command = mock_execute + + mock_execute.return_value = SnoozCommandResult( + SnoozCommandResultStatus.SUCCESSFUL, timedelta() + ) + mock_connected_snooz.device.state = SnoozDeviceState(on=True, volume=56) + + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [snooz_fan_entity_id]}, + blocking=True, + ) + + state = hass.states.get(snooz_fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_PERCENTAGE] == 56 + + mock_execute.return_value = SnoozCommandResult( + SnoozCommandResultStatus.CANCELLED, timedelta() + ) + mock_connected_snooz.device.state = SnoozDeviceState(on=False, volume=15) + + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [snooz_fan_entity_id]}, + blocking=True, + ) + + # the device state shouldn't be written when cancelled + state = hass.states.get(snooz_fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_PERCENTAGE] == 56 + + mock_execute.return_value = SnoozCommandResult( + SnoozCommandResultStatus.UNEXPECTED_ERROR, timedelta() + ) + + with pytest.raises(HomeAssistantError) as failure: + await hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [snooz_fan_entity_id]}, + blocking=True, + ) + + assert failure.match("failed with status") + + +@pytest.fixture(name="snooz_fan_entity_id") +async def fixture_snooz_fan_entity_id( + hass: HomeAssistant, mock_connected_snooz: SnoozFixture +) -> str: + """Mock a Snooz fan entity and config entry.""" + + yield get_fan_entity_id(hass, mock_connected_snooz.device) + + +def get_fan_entity_id(hass: HomeAssistant, device: MockSnoozDevice) -> str: + """Get the entity ID for a mock device.""" + + return entity_registry.async_get(hass).async_get_entity_id( + Platform.FAN, DOMAIN, device.address + ) From 22590bf71d75bc39dfadaf61ee7c702cd0952305 Mon Sep 17 00:00:00 2001 From: Garrett <7310260+G-Two@users.noreply.github.com> Date: Mon, 10 Oct 2022 19:39:14 -0400 Subject: [PATCH 333/985] Bump to subarulink v0.6.1 (#80056) --- homeassistant/components/subaru/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/subaru/manifest.json b/homeassistant/components/subaru/manifest.json index 6bae6e8422d..df3a97cbda3 100644 --- a/homeassistant/components/subaru/manifest.json +++ b/homeassistant/components/subaru/manifest.json @@ -3,7 +3,7 @@ "name": "Subaru", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/subaru", - "requirements": ["subarulink==0.6.0"], + "requirements": ["subarulink==0.6.1"], "codeowners": ["@G-Two"], "iot_class": "cloud_polling", "loggers": ["stdiomask", "subarulink"] diff --git a/requirements_all.txt b/requirements_all.txt index 62ad00ed40b..5470850ca89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2338,7 +2338,7 @@ streamlabswater==1.0.1 stringcase==1.2.0 # homeassistant.components.subaru -subarulink==0.6.0 +subarulink==0.6.1 # homeassistant.components.solarlog sunwatcher==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a92a76db6fa..4f5f5aaa28c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1617,7 +1617,7 @@ stookalert==0.1.4 stringcase==1.2.0 # homeassistant.components.subaru -subarulink==0.6.0 +subarulink==0.6.1 # homeassistant.components.solarlog sunwatcher==0.2.1 From eac1a1e51334055523d8da2935f0c9cda3e9da4d Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 11 Oct 2022 00:31:56 +0000 Subject: [PATCH 334/985] [ci skip] Translation update --- .../binary_sensor/translations/id.json | 2 +- .../components/generic/translations/sv.json | 7 +++++++ .../components/huawei_lte/translations/no.json | 11 ++++++++++- .../components/huawei_lte/translations/sv.json | 11 ++++++++++- .../openexchangerates/translations/et.json | 4 ++-- .../openexchangerates/translations/fr.json | 2 +- .../openexchangerates/translations/no.json | 4 ++-- .../openexchangerates/translations/pl.json | 4 ++-- .../openexchangerates/translations/zh-Hant.json | 4 ++-- .../components/overkiz/translations/fr.json | 3 ++- .../components/overkiz/translations/no.json | 3 ++- .../components/overkiz/translations/pt-BR.json | 3 ++- .../components/overkiz/translations/sv.json | 3 ++- .../plugwise/translations/select.sv.json | 11 +++++++++++ .../components/update/translations/id.json | 2 +- .../components/zha/translations/sv.json | 16 ++++++++++++++++ 16 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/plugwise/translations/select.sv.json diff --git a/homeassistant/components/binary_sensor/translations/id.json b/homeassistant/components/binary_sensor/translations/id.json index e01ee10cc3c..c5ef3775d52 100644 --- a/homeassistant/components/binary_sensor/translations/id.json +++ b/homeassistant/components/binary_sensor/translations/id.json @@ -217,7 +217,7 @@ "on": "Terdeteksi" }, "update": { - "off": "Diperbarui", + "off": "Terbaru", "on": "Pembaruan tersedia" }, "vibration": { diff --git a/homeassistant/components/generic/translations/sv.json b/homeassistant/components/generic/translations/sv.json index 616931824b9..4db8e007a1d 100644 --- a/homeassistant/components/generic/translations/sv.json +++ b/homeassistant/components/generic/translations/sv.json @@ -45,6 +45,13 @@ "verify_ssl": "Verifiera SSL-certifikat" }, "description": "Skriv in inst\u00e4llningarna f\u00f6r att ansluta till kameran." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "Bilden ser bra ut." + }, + "description": "![F\u00f6rhandsvisning stillbild]({preview_url})", + "title": "F\u00f6rhandsvisning" } } }, diff --git a/homeassistant/components/huawei_lte/translations/no.json b/homeassistant/components/huawei_lte/translations/no.json index d1cd33ce2b0..f9f16fdf2b2 100644 --- a/homeassistant/components/huawei_lte/translations/no.json +++ b/homeassistant/components/huawei_lte/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Ikke en Huawei LTE-enhet" + "not_huawei_lte": "Ikke en Huawei LTE-enhet", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "connection_timeout": "Tilkoblingsavbrudd", @@ -15,6 +16,14 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Angi legitimasjon for enhetstilgang.", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "password": "Passord", diff --git a/homeassistant/components/huawei_lte/translations/sv.json b/homeassistant/components/huawei_lte/translations/sv.json index b58dcb07da2..96317c05545 100644 --- a/homeassistant/components/huawei_lte/translations/sv.json +++ b/homeassistant/components/huawei_lte/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Inte en Huawei LTE-enhet" + "not_huawei_lte": "Inte en Huawei LTE-enhet", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "connection_timeout": "Timeout f\u00f6r anslutning", @@ -15,6 +16,14 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + }, + "description": "Logga in p\u00e5 enheten", + "title": "\u00c5terautentisera integrationen." + }, "user": { "data": { "password": "L\u00f6senord", diff --git a/homeassistant/components/openexchangerates/translations/et.json b/homeassistant/components/openexchangerates/translations/et.json index fbeed4f8443..45ed7fb1cfc 100644 --- a/homeassistant/components/openexchangerates/translations/et.json +++ b/homeassistant/components/openexchangerates/translations/et.json @@ -26,8 +26,8 @@ }, "issues": { "deprecated_yaml": { - "description": "Open Exchange Rates konfigureerimine YAML-i abil eemaldatakse.\n\nOlemasolev YAML-konfiguratsioon on automaatselt kasutajaliidesesse imporditud.\n\nEemalda Open Exchange Rates YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivita Home Assistant uuesti, et see probleem lahendada.", - "title": "Open Exchange Rates YAML-konfiguratsioon eemaldatakse" + "description": "Open Exchange Rates konfigureerimine YAML-i abil eemaldati.\n\nEemalda Open Exchange Rates YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivita Home Assistant uuesti, et see probleem lahendada.", + "title": "Open Exchange Rates YAML-konfiguratsioon eemaldati" } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/fr.json b/homeassistant/components/openexchangerates/translations/fr.json index 2e8b1c42cac..c6d0bb24444 100644 --- a/homeassistant/components/openexchangerates/translations/fr.json +++ b/homeassistant/components/openexchangerates/translations/fr.json @@ -26,7 +26,7 @@ }, "issues": { "deprecated_yaml": { - "title": "La configuration YAML pour Open Exchange Rates sera bient\u00f4t supprim\u00e9e" + "title": "La configuration YAML pour Open Exchange Rates a \u00e9t\u00e9 supprim\u00e9e" } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/no.json b/homeassistant/components/openexchangerates/translations/no.json index 93a85a767c8..fbd3cd7c206 100644 --- a/homeassistant/components/openexchangerates/translations/no.json +++ b/homeassistant/components/openexchangerates/translations/no.json @@ -26,8 +26,8 @@ }, "issues": { "deprecated_yaml": { - "description": "Konfigurering av \u00e5pne valutakurser ved hjelp av YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Open Exchange Rates YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", - "title": "Open Exchange Rates YAML-konfigurasjonen blir fjernet" + "description": "Konfigurering av \u00e5pne valutakurser med YAML er fjernet. \n\n Fjern Open Exchange Rates YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Open Exchange Rates YAML-konfigurasjonen er fjernet" } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/pl.json b/homeassistant/components/openexchangerates/translations/pl.json index e7de6c30cec..a9bb2278d90 100644 --- a/homeassistant/components/openexchangerates/translations/pl.json +++ b/homeassistant/components/openexchangerates/translations/pl.json @@ -26,8 +26,8 @@ }, "issues": { "deprecated_yaml": { - "description": "Konfiguracja Open Exchange Rates przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", - "title": "Konfiguracja YAML dla Open Exchange Rates zostanie usuni\u0119ta" + "description": "Konfiguracja Open Exchange Rates przy u\u017cyciu YAML zosta\u0142a usuni\u0119ta. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Open Exchange Rates zosta\u0142a usuni\u0119ta" } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/zh-Hant.json b/homeassistant/components/openexchangerates/translations/zh-Hant.json index c9f8df654e4..d2b9b7ecb27 100644 --- a/homeassistant/components/openexchangerates/translations/zh-Hant.json +++ b/homeassistant/components/openexchangerates/translations/zh-Hant.json @@ -26,8 +26,8 @@ }, "issues": { "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Open Exchange Rates \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Open Exchange Rates YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Open Exchange Rates YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Open Exchange Rates \u5df2\u79fb\u9664\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Open Exchange Rates YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Open Exchange Rates YAML \u8a2d\u5b9a\u5df2\u79fb\u9664" } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/fr.json b/homeassistant/components/overkiz/translations/fr.json index 89d7af10f33..0fd17d822f5 100644 --- a/homeassistant/components/overkiz/translations/fr.json +++ b/homeassistant/components/overkiz/translations/fr.json @@ -12,7 +12,8 @@ "too_many_attempts": "Trop de tentatives avec un jeton non valide\u00a0: banni temporairement", "too_many_requests": "Trop de demandes, r\u00e9essayez plus tard.", "unknown": "Erreur inattendue", - "unknown_user": "Utilisateur inconnu. Les comptes Somfy Protect ne sont pas pris en charge par cette int\u00e9gration." + "unknown_user": "Utilisateur inconnu. Les comptes Somfy Protect ne sont pas pris en charge par cette int\u00e9gration.", + "unsupported_hardware": "Votre mat\u00e9riel {unsupported_device} n'est pas pris en charge par cette int\u00e9gration." }, "flow_title": "Passerelle\u00a0: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/no.json b/homeassistant/components/overkiz/translations/no.json index 2da02db164d..589b85be025 100644 --- a/homeassistant/components/overkiz/translations/no.json +++ b/homeassistant/components/overkiz/translations/no.json @@ -12,7 +12,8 @@ "too_many_attempts": "For mange fors\u00f8k med et ugyldig token, midlertidig utestengt", "too_many_requests": "For mange foresp\u00f8rsler. Pr\u00f8v igjen senere", "unknown": "Uventet feil", - "unknown_user": "Ukjent bruker. Somfy Protect-kontoer st\u00f8ttes ikke av denne integrasjonen." + "unknown_user": "Ukjent bruker. Somfy Protect-kontoer st\u00f8ttes ikke av denne integrasjonen.", + "unsupported_hardware": "Maskinvaren din for {unsupported_device} st\u00f8ttes ikke av denne integrasjonen." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/pt-BR.json b/homeassistant/components/overkiz/translations/pt-BR.json index 3e2ff048560..206b8632656 100644 --- a/homeassistant/components/overkiz/translations/pt-BR.json +++ b/homeassistant/components/overkiz/translations/pt-BR.json @@ -12,7 +12,8 @@ "too_many_attempts": "Muitas tentativas com um token inv\u00e1lido, banido temporariamente", "too_many_requests": "Muitas solicita\u00e7\u00f5es, tente novamente mais tarde", "unknown": "Erro inesperado", - "unknown_user": "Usu\u00e1rio desconhecido. As contas Somfy Protect n\u00e3o s\u00e3o suportadas por esta integra\u00e7\u00e3o." + "unknown_user": "Usu\u00e1rio desconhecido. As contas Somfy Protect n\u00e3o s\u00e3o suportadas por esta integra\u00e7\u00e3o.", + "unsupported_hardware": "Seu hardware {unsupported_device} n\u00e3o \u00e9 compat\u00edvel com esta integra\u00e7\u00e3o." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/sv.json b/homeassistant/components/overkiz/translations/sv.json index 32565cac512..2ae1ca66d32 100644 --- a/homeassistant/components/overkiz/translations/sv.json +++ b/homeassistant/components/overkiz/translations/sv.json @@ -12,7 +12,8 @@ "too_many_attempts": "F\u00f6r m\u00e5nga f\u00f6rs\u00f6k med en ogiltig token, tillf\u00e4lligt avst\u00e4ngd", "too_many_requests": "F\u00f6r m\u00e5nga f\u00f6rfr\u00e5gningar, f\u00f6rs\u00f6k igen senare", "unknown": "Ov\u00e4ntat fel", - "unknown_user": "Ok\u00e4nd anv\u00e4ndare. Somfy Protect-konton st\u00f6ds inte av denna integration." + "unknown_user": "Ok\u00e4nd anv\u00e4ndare. Somfy Protect-konton st\u00f6ds inte av denna integration.", + "unsupported_hardware": "Din {unsupported_device} h\u00e5rdvara st\u00f6ds inte av den h\u00e4r integrationen." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/homeassistant/components/plugwise/translations/select.sv.json b/homeassistant/components/plugwise/translations/select.sv.json new file mode 100644 index 00000000000..3667fd8a09a --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.sv.json @@ -0,0 +1,11 @@ +{ + "state": { + "plugwise__regulation_mode": { + "bleeding_cold": "Riktigt kallt", + "bleeding_hot": "Riktigt varmt", + "cooling": "Kylning", + "heating": "V\u00e4rme", + "off": "Av" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/id.json b/homeassistant/components/update/translations/id.json index c4e6c43ab5c..23a6690fab4 100644 --- a/homeassistant/components/update/translations/id.json +++ b/homeassistant/components/update/translations/id.json @@ -3,7 +3,7 @@ "trigger_type": { "changed_states": "Ketersediaan pembaruan {entity_name} berubah", "turned_off": "{entity_name} menjadi yang terbaru", - "turned_on": "{entity_name} mendapat pembaruan yang tersedia" + "turned_on": "Tersedia pembaruan untuk {entity_name}" } }, "title": "Versi Baru" diff --git a/homeassistant/components/zha/translations/sv.json b/homeassistant/components/zha/translations/sv.json index 8c0bd645a65..a95dc2970fc 100644 --- a/homeassistant/components/zha/translations/sv.json +++ b/homeassistant/components/zha/translations/sv.json @@ -212,6 +212,14 @@ "description": "ZHA kommer att stoppas. Vill du forts\u00e4tta?", "title": "Konfigurera om ZHA" }, + "instruct_unplug": { + "description": "Din gamla radio har blivit fabriks\u00e5terst\u00e4lld. Om h\u00e5rdvaran inte l\u00e4ngre beh\u00f6vs kan du plugga ur den.", + "title": "Koppla ur din gamla radio" + }, + "intent_migrate": { + "description": "Din gamla radio blir fabriks\u00e5terst\u00e4lld. Om du anv\u00e4nder en kombinerad Z-Wave och Zigbee adapter som exempelvis HUSBZB-1, kommer enbart Zigbee delen \u00e5terst\u00e4llas.\n\nVill du forts\u00e4tta?", + "title": "Migrera till ny radio" + }, "manual_pick_radio_type": { "data": { "radio_type": "Radiotyp" @@ -235,6 +243,14 @@ "description": "Din s\u00e4kerhetskopia har en annan IEEE-adress \u00e4n din radio. F\u00f6r att ditt n\u00e4tverk ska fungera korrekt b\u00f6r IEEE-adressen f\u00f6r din radio ocks\u00e5 \u00e4ndras. \n\n Detta \u00e4r en permanent \u00e5tg\u00e4rd.", "title": "Skriv \u00f6ver Radio IEEE-adress" }, + "prompt_migrate_or_reconfigure": { + "description": "Migrerar du till ny radio eller omkonfigurerar du nuvarande radio?", + "menu_options": { + "intent_migrate": "Migrera till ny radio", + "intent_reconfigure": "Omkonfigurera nuvarande radio" + }, + "title": "Migrera eller omkonfigurera" + }, "upload_manual_backup": { "data": { "uploaded_backup_file": "Ladda upp en fil" From 8aa5a785b5990ccf97c75d5cce843f0a6dbc0da2 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Mon, 10 Oct 2022 20:24:00 -0500 Subject: [PATCH 335/985] Improve client info reported to Jellyfin (#79974) --- homeassistant/components/jellyfin/__init__.py | 5 ++++- homeassistant/components/jellyfin/const.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/jellyfin/__init__.py b/homeassistant/components/jellyfin/__init__.py index 0126c05e4f2..c0839cafa09 100644 --- a/homeassistant/components/jellyfin/__init__.py +++ b/homeassistant/components/jellyfin/__init__.py @@ -20,7 +20,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry_data[CONF_CLIENT_DEVICE_ID] = entry.entry_id hass.config_entries.async_update_entry(entry, data=entry_data) - client = create_client(device_id=entry.data[CONF_CLIENT_DEVICE_ID]) + client = create_client( + device_id=entry.data[CONF_CLIENT_DEVICE_ID], + device_name=hass.config.location_name, + ) try: await validate_input(hass, dict(entry.data), client) diff --git a/homeassistant/components/jellyfin/const.py b/homeassistant/components/jellyfin/const.py index 182144806d2..d11ae195892 100644 --- a/homeassistant/components/jellyfin/const.py +++ b/homeassistant/components/jellyfin/const.py @@ -2,9 +2,11 @@ from typing import Final +from homeassistant.const import __version__ as hass_version + DOMAIN: Final = "jellyfin" -CLIENT_VERSION: Final = "1.0" +CLIENT_VERSION: Final = hass_version COLLECTION_TYPE_MOVIES: Final = "movies" COLLECTION_TYPE_MUSIC: Final = "music" From fe5534666dec21a7b31cab0a82b1313a83e316b1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Oct 2022 18:52:44 -1000 Subject: [PATCH 336/985] Bump dbus-fast to 1.41.0 (#80062) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 9b240f0c612..d5f0539bda7 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.4", - "dbus-fast==1.38.0" + "dbus-fast==1.41.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f4b0f3ce25a..85b8aee71ac 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.4 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.38.0 +dbus-fast==1.41.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 5470850ca89..47769d5425c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -546,7 +546,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.38.0 +dbus-fast==1.41.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f5f5aaa28c..91dd1495317 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -426,7 +426,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.38.0 +dbus-fast==1.41.0 # homeassistant.components.debugpy debugpy==1.6.3 From 884577e62245313373fc36713653c9befea2121c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Oct 2022 09:00:36 +0200 Subject: [PATCH 337/985] Bump actions/setup-python from 4.1.0 to 4.3.0 (#80068) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4.1.0 to 4.3.0. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4.1.0...v4.3.0) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 6 +++--- .github/workflows/ci.yaml | 24 ++++++++++++------------ .github/workflows/translations.yaml | 4 ++-- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 4c3dc19a040..a0ae1552f34 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -29,7 +29,7 @@ jobs: fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -70,7 +70,7 @@ jobs: uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -115,7 +115,7 @@ jobs: - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5f4b745091e..7fd4b284191 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -172,7 +172,7 @@ jobs: uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment @@ -210,7 +210,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v4.3.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -259,7 +259,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v4.3.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -311,7 +311,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v4.3.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -352,7 +352,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v4.3.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -475,7 +475,7 @@ jobs: uses: actions/checkout@v3.1.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ matrix.python-version }} - name: Generate partial pip restore key @@ -538,7 +538,7 @@ jobs: uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment @@ -570,7 +570,7 @@ jobs: uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment @@ -603,7 +603,7 @@ jobs: uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment @@ -647,7 +647,7 @@ jobs: uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment @@ -695,7 +695,7 @@ jobs: uses: actions/checkout@v3.1.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment @@ -749,7 +749,7 @@ jobs: uses: actions/checkout@v3.1.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 54864b9e0c0..cbf8e26c9ec 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} From 4e5b5dfb93c0532f2433d72c3fd80eee1cb082b2 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 11 Oct 2022 09:04:52 +0200 Subject: [PATCH 338/985] Update pyupgrade to 3.1.0 (#80058) * Update pyupgrade to 3.1.0 * Remove redundant open modes - text is the default --- .pre-commit-config.yaml | 2 +- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/config.py | 16 ++++++++-------- requirements_test_pre_commit.txt | 2 +- script/version_bump.py | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6c57b9de849..3ba82c4aa58 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.38.0 + rev: v3.1.0 hooks: - id: pyupgrade args: [--py39-plus] diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 62aad6ca7fe..36299c72608 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -375,7 +375,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unsub = await async_subscribe(hass, call.data["topic"], collect_msg) def write_dump(): - with open(hass.config.path("mqtt_dump.txt"), "wt", encoding="utf8") as fp: + with open(hass.config.path("mqtt_dump.txt"), "w", encoding="utf8") as fp: for msg in messages: fp.write(",".join(msg) + "\n") diff --git a/homeassistant/config.py b/homeassistant/config.py index 91f94bbbf40..0f68e0bd235 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -304,26 +304,26 @@ def _write_default_config(config_dir: str) -> bool: # Writing files with YAML does not create the most human readable results # So we're hard coding a YAML template. try: - with open(config_path, "wt", encoding="utf8") as config_file: + with open(config_path, "w", encoding="utf8") as config_file: config_file.write(DEFAULT_CONFIG) if not os.path.isfile(secret_path): - with open(secret_path, "wt", encoding="utf8") as secret_file: + with open(secret_path, "w", encoding="utf8") as secret_file: secret_file.write(DEFAULT_SECRETS) - with open(version_path, "wt", encoding="utf8") as version_file: + with open(version_path, "w", encoding="utf8") as version_file: version_file.write(__version__) if not os.path.isfile(automation_yaml_path): - with open(automation_yaml_path, "wt", encoding="utf8") as automation_file: + with open(automation_yaml_path, "w", encoding="utf8") as automation_file: automation_file.write("[]") if not os.path.isfile(script_yaml_path): - with open(script_yaml_path, "wt", encoding="utf8"): + with open(script_yaml_path, "w", encoding="utf8"): pass if not os.path.isfile(scene_yaml_path): - with open(scene_yaml_path, "wt", encoding="utf8"): + with open(scene_yaml_path, "w", encoding="utf8"): pass return True @@ -421,7 +421,7 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: _LOGGER.info("Migrating google tts to google_translate tts") config_raw = config_raw.replace(TTS_PRE_92, TTS_92) try: - with open(config_path, "wt", encoding="utf-8") as config_file: + with open(config_path, "w", encoding="utf-8") as config_file: config_file.write(config_raw) except OSError: _LOGGER.exception("Migrating to google_translate tts failed") @@ -433,7 +433,7 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: if os.path.isdir(lib_path): shutil.rmtree(lib_path) - with open(version_path, "wt", encoding="utf8") as outp: + with open(version_path, "w", encoding="utf8") as outp: outp.write(__version__) diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 51789f48ca5..87df8c733cb 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -12,5 +12,5 @@ mccabe==0.6.1 pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 -pyupgrade==2.38.0 +pyupgrade==3.1.0 yamllint==1.27.1 diff --git a/script/version_bump.py b/script/version_bump.py index f7dc37b5e22..4a38adbd677 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -116,7 +116,7 @@ def write_version(version): "PATCH_VERSION: Final = .*\n", f'PATCH_VERSION: Final = "{patch}"\n', content ) - with open("homeassistant/const.py", "wt") as fil: + with open("homeassistant/const.py", "w") as fil: fil.write(content) From edad6d0f268e8c7cb98dbb303341eb822bd81527 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Tue, 11 Oct 2022 09:49:06 +0200 Subject: [PATCH 339/985] Remove old import logic for google_travel_time (#80018) Remove old import logic --- .../components/google_travel_time/__init__.py | 11 ---------- .../google_travel_time/test_init.py | 21 ------------------- 2 files changed, 32 deletions(-) delete mode 100644 tests/components/google_travel_time/test_init.py diff --git a/homeassistant/components/google_travel_time/__init__.py b/homeassistant/components/google_travel_time/__init__.py index 7d125be5025..b2778a34c10 100644 --- a/homeassistant/components/google_travel_time/__init__.py +++ b/homeassistant/components/google_travel_time/__init__.py @@ -2,23 +2,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import ( - async_entries_for_config_entry, - async_get, -) PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Google Maps Travel Time from a config entry.""" - if entry.unique_id is not None: - hass.config_entries.async_update_entry(entry, unique_id=None) - - ent_reg = async_get(hass) - for entity in async_entries_for_config_entry(ent_reg, entry.entry_id): - ent_reg.async_update_entity(entity.entity_id, new_unique_id=entry.entry_id) - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/tests/components/google_travel_time/test_init.py b/tests/components/google_travel_time/test_init.py deleted file mode 100644 index 583cd4dc7ce..00000000000 --- a/tests/components/google_travel_time/test_init.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Test Google Maps Travel Time initialization.""" -from homeassistant.components.google_travel_time.const import DOMAIN -from homeassistant.helpers.entity_registry import async_get - -from tests.common import MockConfigEntry - - -async def test_migration(hass, bypass_platform_setup): - """Test migration logic for unique id.""" - config_entry = MockConfigEntry( - domain=DOMAIN, version=1, entry_id="test", unique_id="test" - ) - ent_reg = async_get(hass) - ent_entry = ent_reg.async_get_or_create( - "sensor", DOMAIN, unique_id="replaceable_unique_id", config_entry=config_entry - ) - entity_id = ent_entry.entity_id - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.unique_id is None - assert ent_reg.async_get(entity_id).unique_id == config_entry.entry_id From 69d935b7bd9f107ba474e6a4c5fa580617bccfb6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Oct 2022 10:32:01 +0200 Subject: [PATCH 340/985] Teach long term statistics that unit 'rpm' is same as 'RPM' (#80012) * Teach long term statistics that unit 'rpm' is same as 'RPM' * Add tests --- homeassistant/components/sensor/recorder.py | 24 ++- tests/components/sensor/test_recorder.py | 197 ++++++++++++++++++++ 2 files changed, 217 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index eee13c09813..c2aa99659cd 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -23,7 +23,7 @@ from homeassistant.components.recorder.models import ( StatisticMetaData, StatisticResult, ) -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, REVOLUTIONS_PER_MINUTE from homeassistant.core import HomeAssistant, State, split_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import entity_sources @@ -47,6 +47,10 @@ DEFAULT_STATISTICS = { STATE_CLASS_TOTAL_INCREASING: {"sum"}, } +EQUIVALENT_UNITS = { + "RPM": REVOLUTIONS_PER_MINUTE, +} + # Keep track of entities for which a warning about decreasing value has been logged SEEN_DIP = "sensor_seen_total_increasing_dip" WARN_DIP = "sensor_warn_total_increasing_dip" @@ -113,10 +117,20 @@ def _time_weighted_average( def _get_units(fstates: list[tuple[float, State]]) -> set[str | None]: - """Return True if all states have the same unit.""" + """Return a set of all units.""" return {item[1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) for item in fstates} +def _equivalent_units(units: set[str | None]) -> bool: + """Return True if the units are equivalent.""" + if len(units) == 1: + return True + units = { + EQUIVALENT_UNITS[unit] if unit in EQUIVALENT_UNITS else unit for unit in units + } + return len(units) == 1 + + def _parse_float(state: str) -> float: """Parse a float string, throw on inf or nan.""" fstate = float(state) @@ -165,7 +179,7 @@ def _normalize_states( # The unit used by this sensor doesn't support unit conversion all_units = _get_units(fstates) - if len(all_units) > 1: + if not _equivalent_units(all_units): if WARN_UNSTABLE_UNIT not in hass.data: hass.data[WARN_UNSTABLE_UNIT] = set() if entity_id not in hass.data[WARN_UNSTABLE_UNIT]: @@ -442,7 +456,9 @@ def _compile_statistics( # noqa: C901 ) in to_process: # Check metadata if old_metadata := old_metadatas.get(entity_id): - if old_metadata[1]["unit_of_measurement"] != statistics_unit: + if not _equivalent_units( + {old_metadata[1]["unit_of_measurement"], statistics_unit} + ): if WARN_UNSTABLE_UNIT not in hass.data: hass.data[WARN_UNSTABLE_UNIT] = set() if entity_id not in hass.data[WARN_UNSTABLE_UNIT]: diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 3f15b35d7b1..73804e83d94 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -2192,6 +2192,203 @@ def test_compile_hourly_statistics_changing_units_3( assert "Error while processing event StatisticsTask" not in caplog.text +@pytest.mark.parametrize( + "device_class, state_unit, state_unit2, unit_class, mean, mean2, min, max", + [ + (None, "RPM", "rpm", None, 13.050847, 13.333333, -10, 30), + (None, "rpm", "RPM", None, 13.050847, 13.333333, -10, 30), + ], +) +def test_compile_hourly_statistics_equivalent_units_1( + hass_recorder, + caplog, + device_class, + state_unit, + state_unit2, + unit_class, + mean, + mean2, + min, + max, +): + """Test compiling hourly statistics where units change from one hour to the next.""" + zero = dt_util.utcnow() + hass = hass_recorder() + setup_component(hass, "sensor", {}) + wait_recording_done(hass) # Wait for the sensor recorder platform to be added + attributes = { + "device_class": device_class, + "state_class": "measurement", + "unit_of_measurement": state_unit, + } + four, states = record_states(hass, zero, "sensor.test1", attributes) + attributes["unit_of_measurement"] = state_unit2 + four, _states = record_states( + hass, zero + timedelta(minutes=5), "sensor.test1", attributes + ) + states["sensor.test1"] += _states["sensor.test1"] + four, _states = record_states( + hass, zero + timedelta(minutes=10), "sensor.test1", attributes + ) + states["sensor.test1"] += _states["sensor.test1"] + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + + do_adhoc_statistics(hass, start=zero) + wait_recording_done(hass) + assert "can not be converted to the unit of previously" not in caplog.text + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + { + "statistic_id": "sensor.test1", + "has_mean": True, + "has_sum": False, + "name": None, + "source": "recorder", + "statistics_unit_of_measurement": state_unit, + "unit_class": unit_class, + }, + ] + stats = statistics_during_period(hass, zero, period="5minute") + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "mean": approx(mean), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) + wait_recording_done(hass) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + { + "statistic_id": "sensor.test1", + "has_mean": True, + "has_sum": False, + "name": None, + "source": "recorder", + "statistics_unit_of_measurement": state_unit2, + "unit_class": unit_class, + }, + ] + stats = statistics_during_period(hass, zero, period="5minute") + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "mean": approx(mean), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat( + zero + timedelta(minutes=10) + ), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=15)), + "mean": approx(mean2), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + }, + ] + } + assert "Error while processing event StatisticsTask" not in caplog.text + + +@pytest.mark.parametrize( + "device_class, state_unit, state_unit2, unit_class, mean, min, max", + [ + (None, "RPM", "rpm", None, 13.333333, -10, 30), + (None, "rpm", "RPM", None, 13.333333, -10, 30), + ], +) +def test_compile_hourly_statistics_equivalent_units_2( + hass_recorder, + caplog, + device_class, + state_unit, + state_unit2, + unit_class, + mean, + min, + max, +): + """Test compiling hourly statistics where units change during an hour.""" + zero = dt_util.utcnow() + hass = hass_recorder() + setup_component(hass, "sensor", {}) + wait_recording_done(hass) # Wait for the sensor recorder platform to be added + attributes = { + "device_class": device_class, + "state_class": "measurement", + "unit_of_measurement": state_unit, + } + four, states = record_states(hass, zero, "sensor.test1", attributes) + attributes["unit_of_measurement"] = state_unit2 + four, _states = record_states( + hass, zero + timedelta(minutes=5), "sensor.test1", attributes + ) + states["sensor.test1"] += _states["sensor.test1"] + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + + do_adhoc_statistics(hass, start=zero + timedelta(seconds=30 * 5)) + wait_recording_done(hass) + assert "The unit of sensor.test1 is changing" not in caplog.text + assert "and matches the unit of already compiled statistics" not in caplog.text + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + { + "statistic_id": "sensor.test1", + "has_mean": True, + "has_sum": False, + "name": None, + "source": "recorder", + "statistics_unit_of_measurement": state_unit, + "unit_class": unit_class, + }, + ] + stats = statistics_during_period(hass, zero, period="5minute") + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat( + zero + timedelta(seconds=30 * 5) + ), + "end": process_timestamp_to_utc_isoformat( + zero + timedelta(seconds=30 * 15) + ), + "mean": approx(mean), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + }, + ] + } + + assert "Error while processing event StatisticsTask" not in caplog.text + + @pytest.mark.parametrize( "device_class, state_unit, statistic_unit, unit_class, mean1, mean2, min, max", [ From 6f7cb158d84c9de476911531e186d53d9217384b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Oct 2022 10:40:10 +0200 Subject: [PATCH 341/985] Cleanup blockchain sensor (#80077) --- .strict-typing | 1 + homeassistant/components/blockchain/sensor.py | 16 ++++++---------- mypy.ini | 10 ++++++++++ 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/.strict-typing b/.strict-typing index 1322adf99e1..623653ac203 100644 --- a/.strict-typing +++ b/.strict-typing @@ -66,6 +66,7 @@ homeassistant.components.backup.* homeassistant.components.baf.* homeassistant.components.bayesian.* homeassistant.components.binary_sensor.* +homeassistant.components.blockchain.* homeassistant.components.bluetooth.* homeassistant.components.bluetooth_tracker.* homeassistant.components.bmw_connected_drive.* diff --git a/homeassistant/components/blockchain/sensor.py b/homeassistant/components/blockchain/sensor.py index 4feb7a9fa6a..6c65987ef57 100644 --- a/homeassistant/components/blockchain/sensor.py +++ b/homeassistant/components/blockchain/sensor.py @@ -8,7 +8,7 @@ from pyblockchain import get_balance, validate_address import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -16,14 +16,10 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by blockchain.com" - CONF_ADDRESSES = "addresses" DEFAULT_NAME = "Bitcoin Balance" -ICON = "mdi:currency-btc" - SCAN_INTERVAL = timedelta(minutes=5) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -42,8 +38,8 @@ def setup_platform( ) -> None: """Set up the Blockchain.com sensors.""" - addresses = config[CONF_ADDRESSES] - name = config[CONF_NAME] + addresses: list[str] = config[CONF_ADDRESSES] + name: str = config[CONF_NAME] for address in addresses: if not validate_address(address): @@ -56,11 +52,11 @@ def setup_platform( class BlockchainSensor(SensorEntity): """Representation of a Blockchain.com sensor.""" - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} - _attr_icon = ICON + _attr_attribution = "Data provided by blockchain.com" + _attr_icon = "mdi:currency-btc" _attr_native_unit_of_measurement = "BTC" - def __init__(self, name, addresses): + def __init__(self, name: str, addresses: list[str]) -> None: """Initialize the sensor.""" self._attr_name = name self.addresses = addresses diff --git a/mypy.ini b/mypy.ini index 63e35c3076e..7e5133f38e9 100644 --- a/mypy.ini +++ b/mypy.ini @@ -412,6 +412,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.blockchain.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.bluetooth.*] check_untyped_defs = true disallow_incomplete_defs = true From 8aa30cce26715255d43c9a02102d9c9668f6546f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 11 Oct 2022 10:49:54 +0200 Subject: [PATCH 342/985] Fix state saving when sharing topics for MQTT entities (#79421) * Do not write old state sharing availability topic * Add a test * Support for all availability topics * delay async_write_ha_state till last callback * Process write req after processing callback jobs * Do not count subscription callbacks * Simplify * Stale docsting * No topic needed for delays state write * No need to clear when reloading * Move test to test_mixins.py * Only set up sensor platform for test --- .../components/mqtt/alarm_control_panel.py | 4 +- .../components/mqtt/binary_sensor.py | 3 +- homeassistant/components/mqtt/client.py | 1 + homeassistant/components/mqtt/climate.py | 14 ++-- homeassistant/components/mqtt/cover.py | 6 +- .../mqtt/device_tracker/schema_discovery.py | 3 +- homeassistant/components/mqtt/fan.py | 12 +-- homeassistant/components/mqtt/humidifier.py | 12 +-- .../components/mqtt/light/schema_basic.py | 22 +++--- .../components/mqtt/light/schema_json.py | 4 +- .../components/mqtt/light/schema_template.py | 3 +- homeassistant/components/mqtt/lock.py | 3 +- homeassistant/components/mqtt/mixins.py | 6 +- homeassistant/components/mqtt/models.py | 21 ++++++ homeassistant/components/mqtt/number.py | 3 +- homeassistant/components/mqtt/select.py | 3 +- homeassistant/components/mqtt/sensor.py | 6 +- homeassistant/components/mqtt/siren.py | 3 +- homeassistant/components/mqtt/subscription.py | 7 +- homeassistant/components/mqtt/switch.py | 3 +- .../components/mqtt/vacuum/schema_legacy.py | 4 +- .../components/mqtt/vacuum/schema_state.py | 4 +- tests/components/mqtt/test_mixins.py | 73 +++++++++++++++++++ 23 files changed, 164 insertions(+), 56 deletions(-) create mode 100644 tests/components/mqtt/test_mixins.py diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index c3502cd8e64..ed1990d919e 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -49,7 +49,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -211,7 +211,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): _LOGGER.warning("Received unexpected payload: %s", msg.payload) return self._state = payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index f0e5ecc9df8..915a2780283 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -47,6 +47,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttValueTemplate +from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -260,7 +261,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): self.hass, off_delay, off_delay_listener ) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index b0ce53d75fd..bf3f24c950e 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -659,6 +659,7 @@ class MQTT: timestamp, ), ) + self._mqtt_data.state_write_requests.process_write_state_requests() def _mqtt_on_callback(self, _mqttc, _userdata, mid, _granted_qos=None) -> None: """Publish / Subscribe / Unsubscribe callback.""" diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 96c7ca3665b..9f98fcfebdc 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -55,7 +55,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -494,7 +494,7 @@ class MqttClimate(MqttEntity, ClimateEntity): payload, ) return - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_subscription(topics, CONF_ACTION_TOPIC, handle_action_received) @@ -505,7 +505,7 @@ class MqttClimate(MqttEntity, ClimateEntity): try: setattr(self, attr, float(payload)) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) except ValueError: _LOGGER.error("Could not parse temperature from %s", payload) @@ -564,7 +564,7 @@ class MqttClimate(MqttEntity, ClimateEntity): _LOGGER.error("Invalid %s mode: %s", mode_list, payload) else: setattr(self, attr, payload) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @callback @log_messages(self.hass, self.entity_id) @@ -623,7 +623,7 @@ class MqttClimate(MqttEntity, ClimateEntity): else: _LOGGER.error("Invalid %s mode: %s", attr, payload) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @callback @log_messages(self.hass, self.entity_id) @@ -640,7 +640,7 @@ class MqttClimate(MqttEntity, ClimateEntity): preset_mode = render_template(msg, CONF_PRESET_MODE_VALUE_TEMPLATE) if preset_mode in [PRESET_NONE, PAYLOAD_NONE]: self._preset_mode = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return if not preset_mode: _LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic) @@ -654,7 +654,7 @@ class MqttClimate(MqttEntity, ClimateEntity): ) else: self._preset_mode = preset_mode - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_subscription( topics, CONF_PRESET_MODE_STATE_TOPIC, handle_preset_mode_received diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 1f5d26c3a78..6ed12b8adef 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -51,7 +51,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -405,7 +405,7 @@ class MqttCover(MqttEntity, CoverEntity): ) return - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @callback @log_messages(self.hass, self.entity_id) @@ -451,7 +451,7 @@ class MqttCover(MqttEntity, CoverEntity): else STATE_OPEN ) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_GET_POSITION_TOPIC): topics["get_position_topic"] = { diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 907d424e8a4..1d3f9d109f6 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -29,6 +29,7 @@ from ..const import CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper from ..models import MqttValueTemplate +from ..util import get_mqtt_data CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" @@ -106,7 +107,7 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity): else: self._location_name = msg.payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 584df08e7d7..866b429c68f 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -55,7 +55,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic" CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic" @@ -391,7 +391,7 @@ class MqttFan(MqttEntity, FanEntity): self._state = False elif payload == PAYLOAD_NONE: self._state = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: topics[CONF_STATE_TOPIC] = { @@ -413,7 +413,7 @@ class MqttFan(MqttEntity, FanEntity): return if rendered_percentage_payload == self._payload["PERCENTAGE_RESET"]: self._percentage = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return try: percentage = ranged_value_to_percentage( @@ -436,7 +436,7 @@ class MqttFan(MqttEntity, FanEntity): ) return self._percentage = percentage - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_PERCENTAGE_STATE_TOPIC] is not None: topics[CONF_PERCENTAGE_STATE_TOPIC] = { @@ -469,7 +469,7 @@ class MqttFan(MqttEntity, FanEntity): return self._preset_mode = preset_mode - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_PRESET_MODE_STATE_TOPIC] is not None: topics[CONF_PRESET_MODE_STATE_TOPIC] = { @@ -492,7 +492,7 @@ class MqttFan(MqttEntity, FanEntity): self._oscillation = True elif payload == self._payload["OSCILLATE_OFF_PAYLOAD"]: self._oscillation = False - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None: topics[CONF_OSCILLATION_STATE_TOPIC] = { diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 837bbb8b909..7514f0ff672 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -51,7 +51,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic CONF_AVAILABLE_MODES_LIST = "modes" CONF_DEVICE_CLASS = "device_class" @@ -309,7 +309,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): self._state = False elif payload == PAYLOAD_NONE: self._state = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: topics[CONF_STATE_TOPIC] = { @@ -331,7 +331,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): return if rendered_target_humidity_payload == self._payload["HUMIDITY_RESET"]: self._target_humidity = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return try: target_humidity = round(float(rendered_target_humidity_payload)) @@ -355,7 +355,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): ) return self._target_humidity = target_humidity - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_TARGET_HUMIDITY_STATE_TOPIC] is not None: topics[CONF_TARGET_HUMIDITY_STATE_TOPIC] = { @@ -373,7 +373,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): mode = self._value_templates[ATTR_MODE](msg.payload) if mode == self._payload["MODE_RESET"]: self._mode = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return if not mode: _LOGGER.debug("Ignoring empty mode from '%s'", msg.topic) @@ -388,7 +388,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): return self._mode = mode - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_MODE_STATE_TOPIC] is not None: topics[CONF_MODE_STATE_TOPIC] = { diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index e2805781f45..d435d4e91ad 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -51,7 +51,7 @@ from ..const import ( from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from ..models import MqttCommandTemplate, MqttValueTemplate -from ..util import valid_publish_topic, valid_subscribe_topic +from ..util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -438,7 +438,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._state = False elif payload == PAYLOAD_NONE: self._state = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: topics[CONF_STATE_TOPIC] = { @@ -462,7 +462,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): device_value = float(payload) percent_bright = device_value / self._config[CONF_BRIGHTNESS_SCALE] self._brightness = percent_bright * 255 - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_BRIGHTNESS_STATE_TOPIC, brightness_received) @@ -493,7 +493,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if not rgb: return self._rgb_color = rgb - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_RGB_STATE_TOPIC, rgb_received) @@ -510,7 +510,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if not rgbw: return self._rgbw_color = rgbw - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_RGBW_STATE_TOPIC, rgbw_received) @@ -527,7 +527,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if not rgbww: return self._rgbww_color = rgbww - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_RGBWW_STATE_TOPIC, rgbww_received) @@ -543,7 +543,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): return self._color_mode = payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_COLOR_MODE_STATE_TOPIC, color_mode_received) @@ -561,7 +561,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if self._optimistic_color_mode: self._color_mode = ColorMode.COLOR_TEMP self._color_temp = int(payload) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_COLOR_TEMP_STATE_TOPIC, color_temp_received) @@ -577,7 +577,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): return self._effect = payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_EFFECT_STATE_TOPIC, effect_received) @@ -594,7 +594,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if self._optimistic_color_mode: self._color_mode = ColorMode.HS self._hs_color = hs_color - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) except ValueError: _LOGGER.debug("Failed to parse hs state update: '%s'", payload) @@ -613,7 +613,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if self._optimistic_color_mode: self._color_mode = ColorMode.XY self._xy_color = xy_color - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_XY_STATE_TOPIC, xy_received) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 8843e8542eb..a4a76673176 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -58,7 +58,7 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity -from ..util import valid_subscribe_topic +from ..util import get_mqtt_data, valid_subscribe_topic from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import ( CONF_BRIGHTNESS_SCALE, @@ -401,7 +401,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): with suppress(KeyError): self._effect = values["effect"] - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: self._sub_state = subscription.async_prepare_subscribe_topics( diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index dacc977a036..33c7f1cea1b 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -41,6 +41,7 @@ from ..const import ( from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from ..models import MqttValueTemplate +from ..util import get_mqtt_data from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import MQTT_LIGHT_ATTRIBUTES_BLOCKED @@ -256,7 +257,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): else: _LOGGER.warning("Unsupported effect value received") - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topics[CONF_STATE_TOPIC] is not None: self._sub_state = subscription.async_prepare_subscribe_topics( diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index dca02f909dc..c9bdd696896 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -33,6 +33,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttValueTemplate +from .util import get_mqtt_data CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_UNLOCK = "payload_unlock" @@ -158,7 +159,7 @@ class MqttLock(MqttEntity, LockEntity): elif payload == self._config[CONF_STATE_UNLOCKED]: self._state = False - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 8022a6e91ae..b5c870a196e 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -435,7 +435,9 @@ class MqttAttributes(Entity): and k not in self._attributes_extra_blocked } self._attributes = filtered_dict - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request( + self + ) else: _LOGGER.warning("JSON result was not a dictionary") self._attributes = None @@ -547,7 +549,7 @@ class MqttAvailability(Entity): self._available[topic] = False self._available_latest = False - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) self._available = { topic: (self._available[topic] if topic in self._available else False) diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index b7cb81b2ea4..f2f30419b4c 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -236,6 +236,26 @@ class MqttValueTemplate: ) +class EntityTopicState: + """Manage entity state write requests for subscribed topics.""" + + def __init__(self) -> None: + """Register topic.""" + self.subscribe_calls: dict[str, Entity] = {} + + @callback + def process_write_state_requests(self) -> None: + """Process the write state requests.""" + while self.subscribe_calls: + _, entity = self.subscribe_calls.popitem() + entity.async_write_ha_state() + + @callback + def write_state_request(self, entity: Entity) -> None: + """Register write state request.""" + self.subscribe_calls[entity.entity_id] = entity + + @dataclass class MqttData: """Keep the MQTT entry data.""" @@ -264,6 +284,7 @@ class MqttData: default_factory=dict ) reload_needed: bool = False + state_write_requests: EntityTopicState = field(default_factory=EntityTopicState) subscriptions_to_restore: list[Subscription] = field(default_factory=list) tags: dict[str, dict[str, MQTTTagScanner]] = field(default_factory=dict) updated_config: ConfigType = field(default_factory=dict) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 09f9d122b98..25ef7af8d6e 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -49,6 +49,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate +from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -222,7 +223,7 @@ class MqttNumber(MqttEntity, RestoreNumber): return self._current_number = num_value - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index a6de0495690..12593550e2f 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -35,6 +35,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate +from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -169,7 +170,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): return self._attr_current_option = payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index b3869cb8afe..d95d669e72f 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -46,7 +46,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttValueTemplate -from .util import valid_subscribe_topic +from .util import get_mqtt_data, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -300,7 +300,7 @@ class MqttSensor(MqttEntity, RestoreSensor): or self._config[CONF_LAST_RESET_TOPIC] == self._config[CONF_STATE_TOPIC] ): _update_last_reset(msg) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) topics["state_topic"] = { "topic": self._config[CONF_STATE_TOPIC], @@ -314,7 +314,7 @@ class MqttSensor(MqttEntity, RestoreSensor): def last_reset_message_received(msg): """Handle new last_reset messages.""" _update_last_reset(msg) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if ( CONF_LAST_RESET_TOPIC in self._config diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index c8332046092..2ab226e44c0 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -54,6 +54,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate +from .util import get_mqtt_data DEFAULT_NAME = "MQTT Siren" DEFAULT_PAYLOAD_ON = "ON" @@ -283,7 +284,7 @@ class MqttSiren(MqttEntity, SirenEntity): ) return self._update(process_turn_on_params(self, json_payload)) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index d0af533f294..05f7f3934ee 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -26,9 +26,12 @@ class EntitySubscription: qos: int = attr.ib(default=0) encoding: str = attr.ib(default="utf-8") - def resubscribe_if_necessary(self, hass, other): + def resubscribe_if_necessary( + self, hass: HomeAssistant, other: EntitySubscription | None + ) -> None: """Re-subscribe to the new topic if necessary.""" if not self._should_resubscribe(other): + assert other self.unsubscribe_callback = other.unsubscribe_callback return @@ -56,7 +59,7 @@ class EntitySubscription: return self.unsubscribe_callback = await self.subscribe_task - def _should_resubscribe(self, other): + def _should_resubscribe(self, other: EntitySubscription | None) -> bool: """Check if we should re-subscribe to the topic using the old state.""" if other is None: return True diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index af16b14bea1..f8bf2f5bc6a 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -47,6 +47,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttValueTemplate +from .util import get_mqtt_data DEFAULT_NAME = "MQTT Switch" DEFAULT_PAYLOAD_ON = "ON" @@ -168,7 +169,7 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): elif payload == PAYLOAD_NONE: self._state = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 6b957aded5c..09c4448fda7 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -20,7 +20,7 @@ from ..const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema from ..models import MqttValueTemplate -from ..util import valid_publish_topic +from ..util import get_mqtt_data, valid_publish_topic from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -320,7 +320,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): if fan_speed: self._fan_speed = fan_speed - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) topics_list = {topic for topic in self._state_topics.values() if topic} self._sub_state = subscription.async_prepare_subscribe_topics( diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index af6c8d289d8..8dfaba80109 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -32,7 +32,7 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema -from ..util import valid_publish_topic +from ..util import get_mqtt_data, valid_publish_topic from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -211,7 +211,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): ) del payload[STATE] self._state_attrs.update(payload) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC): topics["state_position_topic"] = { diff --git a/tests/components/mqtt/test_mixins.py b/tests/components/mqtt/test_mixins.py new file mode 100644 index 00000000000..97a959b8dbe --- /dev/null +++ b/tests/components/mqtt/test_mixins.py @@ -0,0 +1,73 @@ +"""The tests for shared code of the MQTT platform.""" + +from unittest.mock import patch + +from homeassistant.components import mqtt, sensor +from homeassistant.const import EVENT_STATE_CHANGED, Platform +import homeassistant.core as ha +from homeassistant.setup import async_setup_component + +from tests.common import async_fire_mqtt_message + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) +async def test_availability_with_shared_state_topic( + hass, + mqtt_mock_entry_with_yaml_config, +): + """Test the state is not changed twice. + + When an entity with a shared state_topic and availability_topic becomes available + The state should only change once. + """ + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "availability_topic": "test-topic", + "payload_available": True, + "payload_not_available": False, + "value_template": "{{ int(value) or '' }}", + "availability_template": "{{ value != '0' }}", + } + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + events = [] + + @ha.callback + def callback(event): + events.append(event) + + hass.bus.async_listen(EVENT_STATE_CHANGED, callback) + + async_fire_mqtt_message(hass, "test-topic", "100") + await hass.async_block_till_done() + # Initially the state and the availability change + assert len(events) == 1 + + events.clear() + async_fire_mqtt_message(hass, "test-topic", "50") + await hass.async_block_till_done() + assert len(events) == 1 + + events.clear() + async_fire_mqtt_message(hass, "test-topic", "0") + await hass.async_block_till_done() + # Only the availability is changed since the template resukts in an empty payload + # This does not change the state + assert len(events) == 1 + + events.clear() + async_fire_mqtt_message(hass, "test-topic", "10") + await hass.async_block_till_done() + # The availability is changed but the topic is shared, + # hence there the state will be written when the value is updated + assert len(events) == 1 From 65187ab227f9136c2f0e89b49ab4772ccf4fb678 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 11 Oct 2022 10:51:35 +0200 Subject: [PATCH 343/985] Use selectors for basic broker and options for MQTT config flow (#79791) Use selectors for basic broker en options --- homeassistant/components/mqtt/config_flow.py | 56 ++++++++++++++------ 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 47eeceb56c2..fa3f7d14c31 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -21,6 +21,15 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.selector import ( + BooleanSelector, + NumberSelector, + NumberSelectorConfig, + NumberSelectorMode, + TextSelector, + TextSelectorConfig, + TextSelectorType, +) from homeassistant.helpers.typing import ConfigType from .client import MqttClientSetup @@ -42,6 +51,19 @@ from .util import MQTT_WILL_BIRTH_SCHEMA, get_mqtt_data MQTT_TIMEOUT = 5 +BOOLEAN_SELECTOR = BooleanSelector() +TEXT_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT)) +PUBLISH_TOPIC_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT)) +PORT_SELECTOR = vol.All( + NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=1, max=65535)), + vol.Coerce(int), +) +PASSWORD_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.PASSWORD)) +QOS_SELECTOR = vol.All( + NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0, max=2)), + vol.Coerce(int), +) + class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" @@ -282,7 +304,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): # build form fields: OrderedDict[vol.Marker, Any] = OrderedDict() - fields[vol.Optional(CONF_DISCOVERY, default=discovery)] = bool + fields[vol.Optional(CONF_DISCOVERY, default=discovery)] = BOOLEAN_SELECTOR # Birth message is disabled if CONF_BIRTH_MESSAGE = {} fields[ @@ -291,19 +313,21 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): default=CONF_BIRTH_MESSAGE not in current_config or current_config[CONF_BIRTH_MESSAGE] != {}, ) - ] = bool + ] = BOOLEAN_SELECTOR fields[ vol.Optional( "birth_topic", description={"suggested_value": birth[ATTR_TOPIC]} ) - ] = str + ] = PUBLISH_TOPIC_SELECTOR fields[ vol.Optional( "birth_payload", description={"suggested_value": birth[CONF_PAYLOAD]} ) - ] = str - fields[vol.Optional("birth_qos", default=birth[ATTR_QOS])] = vol.In([0, 1, 2]) - fields[vol.Optional("birth_retain", default=birth[ATTR_RETAIN])] = bool + ] = TEXT_SELECTOR + fields[vol.Optional("birth_qos", default=birth[ATTR_QOS])] = QOS_SELECTOR + fields[ + vol.Optional("birth_retain", default=birth[ATTR_RETAIN]) + ] = BOOLEAN_SELECTOR # Will message is disabled if CONF_WILL_MESSAGE = {} fields[ @@ -312,19 +336,21 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): default=CONF_WILL_MESSAGE not in current_config or current_config[CONF_WILL_MESSAGE] != {}, ) - ] = bool + ] = BOOLEAN_SELECTOR fields[ vol.Optional( "will_topic", description={"suggested_value": will[ATTR_TOPIC]} ) - ] = str + ] = PUBLISH_TOPIC_SELECTOR fields[ vol.Optional( "will_payload", description={"suggested_value": will[CONF_PAYLOAD]} ) - ] = str - fields[vol.Optional("will_qos", default=will[ATTR_QOS])] = vol.In([0, 1, 2]) - fields[vol.Optional("will_retain", default=will[ATTR_RETAIN])] = bool + ] = TEXT_SELECTOR + fields[vol.Optional("will_qos", default=will[ATTR_QOS])] = QOS_SELECTOR + fields[ + vol.Optional("will_retain", default=will[ATTR_RETAIN]) + ] = BOOLEAN_SELECTOR return self.async_show_form( step_id="options", @@ -366,20 +392,20 @@ async def async_get_broker_settings( current_pass = current_config.get(CONF_PASSWORD, yaml_config.get(CONF_PASSWORD)) # Build form - fields[vol.Required(CONF_BROKER, default=current_broker)] = str - fields[vol.Required(CONF_PORT, default=current_port)] = vol.Coerce(int) + fields[vol.Required(CONF_BROKER, default=current_broker)] = TEXT_SELECTOR + fields[vol.Required(CONF_PORT, default=current_port)] = PORT_SELECTOR fields[ vol.Optional( CONF_USERNAME, description={"suggested_value": current_user}, ) - ] = str + ] = TEXT_SELECTOR fields[ vol.Optional( CONF_PASSWORD, description={"suggested_value": current_pass}, ) - ] = str + ] = PASSWORD_SELECTOR # Show form return False From 6826f2c291da16a76936f1087b7032210d1824f2 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Tue, 11 Oct 2022 10:54:29 +0200 Subject: [PATCH 344/985] Add reauth flow for devolo_home_network (#71051) * Add reauth flow * Cover cases without existing password * Add test to verify upgrading from older versions * Connect to the device first * Use Mapping for async_step_reauth * Set empty password for user step and remove unneeded update of unique_id --- .../devolo_home_network/__init__.py | 5 +- .../devolo_home_network/config_flow.py | 42 +++++++++++++++- .../devolo_home_network/strings.json | 8 +++- .../devolo_home_network/translations/en.json | 8 +++- .../devolo_home_network/__init__.py | 3 +- .../devolo_home_network/test_config_flow.py | 48 ++++++++++++++++++- .../devolo_home_network/test_init.py | 22 ++++++++- 7 files changed, 128 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index 5cf91325d70..4c54dc721e1 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -10,7 +10,7 @@ from devolo_plc_api.exceptions.device import DeviceNotFound, DeviceUnavailable from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_IP_ADDRESS, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.httpx_client import get_async_client @@ -40,6 +40,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ip=entry.data[CONF_IP_ADDRESS], zeroconf_instance=zeroconf_instance ) await device.async_connect(session_instance=async_client) + device.password = entry.data.get( + CONF_PASSWORD, "" # This key was added in HA Core 2022.6 + ) except DeviceNotFound as err: raise ConfigEntryNotReady( f"Unable to connect to {entry.data[CONF_IP_ADDRESS]}" diff --git a/homeassistant/components/devolo_home_network/config_flow.py b/homeassistant/components/devolo_home_network/config_flow.py index c96126f43e2..0acdc9cfa64 100644 --- a/homeassistant/components/devolo_home_network/config_flow.py +++ b/homeassistant/components/devolo_home_network/config_flow.py @@ -1,6 +1,7 @@ """Config flow for devolo Home Network integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -10,7 +11,7 @@ import voluptuous as vol from homeassistant import config_entries, core from homeassistant.components import zeroconf -from homeassistant.const import CONF_HOST, CONF_IP_ADDRESS, CONF_NAME +from homeassistant.const import CONF_HOST, CONF_IP_ADDRESS, CONF_NAME, CONF_PASSWORD from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.httpx_client import get_async_client @@ -19,6 +20,7 @@ from .const import DOMAIN, PRODUCT, SERIAL_NUMBER, TITLE _LOGGER = logging.getLogger(__name__) STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_IP_ADDRESS): str}) +STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Optional(CONF_PASSWORD): str}) async def validate_input( @@ -68,6 +70,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): else: await self.async_set_unique_id(info[SERIAL_NUMBER], raise_on_progress=False) self._abort_if_unique_id_configured() + user_input[CONF_PASSWORD] = "" return self.async_create_entry(title=info[TITLE], data=user_input) return self.async_show_form( @@ -100,9 +103,46 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: data = { CONF_IP_ADDRESS: self.context[CONF_HOST], + CONF_PASSWORD: "", } return self.async_create_entry(title=title, data=data) return self.async_show_form( step_id="zeroconf_confirm", description_placeholders={"host_name": title}, ) + + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + """Handle reauthentication.""" + self.context[CONF_HOST] = data[CONF_IP_ADDRESS] + self.context["title_placeholders"][PRODUCT] = self.hass.data[DOMAIN][ + self.context["entry_id"] + ]["device"].product + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by reauthentication.""" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=STEP_REAUTH_DATA_SCHEMA, + ) + + reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + assert reauth_entry is not None + + data = { + CONF_IP_ADDRESS: self.context[CONF_HOST], + CONF_PASSWORD: user_input[CONF_PASSWORD], + } + self.hass.config_entries.async_update_entry( + reauth_entry, + data=data, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(reauth_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") diff --git a/homeassistant/components/devolo_home_network/strings.json b/homeassistant/components/devolo_home_network/strings.json index 63be57d9485..6c320710a1b 100644 --- a/homeassistant/components/devolo_home_network/strings.json +++ b/homeassistant/components/devolo_home_network/strings.json @@ -8,6 +8,11 @@ "ip_address": "[%key:common::config_flow::data::ip%]" } }, + "reauth_confirm": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + }, "zeroconf_confirm": { "description": "Do you want to add the devolo home network device with the hostname `{host_name}` to Home Assistant?", "title": "Discovered devolo home network device" @@ -19,7 +24,8 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "home_control": "The devolo Home Control Central Unit does not work with this integration." + "home_control": "The devolo Home Control Central Unit does not work with this integration.", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } diff --git a/homeassistant/components/devolo_home_network/translations/en.json b/homeassistant/components/devolo_home_network/translations/en.json index 39c0b6d331f..e98984738b0 100644 --- a/homeassistant/components/devolo_home_network/translations/en.json +++ b/homeassistant/components/devolo_home_network/translations/en.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Device is already configured", - "home_control": "The devolo Home Control Central Unit does not work with this integration." + "home_control": "The devolo Home Control Central Unit does not work with this integration.", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", @@ -10,6 +11,11 @@ }, "flow_title": "{product} ({name})", "step": { + "reauth_confirm": { + "data": { + "password": "Password" + } + }, "user": { "data": { "ip_address": "IP Address" diff --git a/tests/components/devolo_home_network/__init__.py b/tests/components/devolo_home_network/__init__.py index c8561f485ca..f42abef20ec 100644 --- a/tests/components/devolo_home_network/__init__.py +++ b/tests/components/devolo_home_network/__init__.py @@ -1,6 +1,6 @@ """Tests for the devolo Home Network integration.""" from homeassistant.components.devolo_home_network.const import DOMAIN -from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import HomeAssistant from .const import IP @@ -12,6 +12,7 @@ def configure_integration(hass: HomeAssistant) -> MockConfigEntry: """Configure the integration.""" config = { CONF_IP_ADDRESS: IP, + CONF_PASSWORD: "", } entry = MockConfigEntry(domain=DOMAIN, data=config) entry.add_to_hass(hass) diff --git a/tests/components/devolo_home_network/test_config_flow.py b/tests/components/devolo_home_network/test_config_flow.py index 9f05d0af2fb..0d35630407e 100644 --- a/tests/components/devolo_home_network/test_config_flow.py +++ b/tests/components/devolo_home_network/test_config_flow.py @@ -7,17 +7,18 @@ from unittest.mock import patch from devolo_plc_api.exceptions.device import DeviceNotFound import pytest -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.devolo_home_network import config_flow from homeassistant.components.devolo_home_network.const import ( DOMAIN, SERIAL_NUMBER, TITLE, ) -from homeassistant.const import CONF_BASE, CONF_IP_ADDRESS, CONF_NAME +from homeassistant.const import CONF_BASE, CONF_IP_ADDRESS, CONF_NAME, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from . import configure_integration from .const import DISCOVERY_INFO, DISCOVERY_INFO_WRONG_DEVICE, IP from .mock import MockDevice @@ -47,6 +48,7 @@ async def test_form(hass: HomeAssistant, info: dict[str, Any]): assert result2["title"] == info["title"] assert result2["data"] == { CONF_IP_ADDRESS: IP, + CONF_PASSWORD: "", } assert len(mock_setup_entry.mock_calls) == 1 @@ -112,6 +114,7 @@ async def test_zeroconf(hass: HomeAssistant): assert result2["title"] == "test" assert result2["data"] == { CONF_IP_ADDRESS: IP, + CONF_PASSWORD: "", } @@ -168,6 +171,47 @@ async def test_abort_if_configued(hass: HomeAssistant): assert result3["reason"] == "already_configured" +@pytest.mark.usefixtures("mock_device") +@pytest.mark.usefixtures("mock_zeroconf") +async def test_form_reauth(hass: HomeAssistant): + """Test that the reauth confirmation form is served.""" + entry = configure_integration(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "title_placeholders": { + CONF_NAME: DISCOVERY_INFO.hostname.split(".")[0], + }, + }, + data=entry.data, + ) + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + with patch( + "homeassistant.components.devolo_home_network.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "test-password-new"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + assert len(mock_setup_entry.mock_calls) == 1 + + await hass.config_entries.async_unload(entry.entry_id) + + +@pytest.mark.usefixtures("mock_device") +@pytest.mark.usefixtures("mock_zeroconf") async def test_validate_input(hass: HomeAssistant): """Test input validation.""" with patch( diff --git a/tests/components/devolo_home_network/test_init.py b/tests/components/devolo_home_network/test_init.py index 5d5693c44e3..524590d7ead 100644 --- a/tests/components/devolo_home_network/test_init.py +++ b/tests/components/devolo_home_network/test_init.py @@ -4,13 +4,17 @@ from unittest.mock import patch from devolo_plc_api.exceptions.device import DeviceNotFound import pytest +from homeassistant.components.devolo_home_network.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_IP_ADDRESS, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from . import configure_integration +from .const import IP from .mock import MockDevice +from tests.common import MockConfigEntry + @pytest.mark.usefixtures("mock_device") async def test_setup_entry(hass: HomeAssistant): @@ -24,6 +28,22 @@ async def test_setup_entry(hass: HomeAssistant): assert entry.state is ConfigEntryState.LOADED +@pytest.mark.usefixtures("mock_device") +async def test_setup_without_password(hass: HomeAssistant): + """Test setup entry without a device password set like used before HA Core 2022.06.""" + config = { + CONF_IP_ADDRESS: IP, + } + entry = MockConfigEntry(domain=DOMAIN, data=config) + entry.add_to_hass(hass) + with patch( + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + return_value=True, + ), patch("homeassistant.core.EventBus.async_listen_once"): + assert await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.LOADED + + async def test_setup_device_not_found(hass: HomeAssistant): """Test setup entry.""" entry = configure_integration(hass) From d01f85b6aa21239a918552735f45614ce147d199 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Tue, 11 Oct 2022 11:05:53 +0200 Subject: [PATCH 345/985] Remove old import logic for waze_travel_time (#80079) Remove old import logic --- .../components/waze_travel_time/__init__.py | 15 ++----------- .../components/waze_travel_time/test_init.py | 21 ------------------- 2 files changed, 2 insertions(+), 34 deletions(-) delete mode 100644 tests/components/waze_travel_time/test_init.py diff --git a/homeassistant/components/waze_travel_time/__init__.py b/homeassistant/components/waze_travel_time/__init__.py index 4e82af5119c..806672b3608 100644 --- a/homeassistant/components/waze_travel_time/__init__.py +++ b/homeassistant/components/waze_travel_time/__init__.py @@ -2,24 +2,13 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import ( - async_entries_for_config_entry, - async_get, -) PLATFORMS = [Platform.SENSOR] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Load the saved entities.""" - if entry.unique_id is not None: - hass.config_entries.async_update_entry(entry, unique_id=None) - - ent_reg = async_get(hass) - for entity in async_entries_for_config_entry(ent_reg, entry.entry_id): - ent_reg.async_update_entity(entity.entity_id, new_unique_id=entry.entry_id) - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/tests/components/waze_travel_time/test_init.py b/tests/components/waze_travel_time/test_init.py deleted file mode 100644 index bf8f6a95844..00000000000 --- a/tests/components/waze_travel_time/test_init.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Test Waze Travel Time initialization.""" -from homeassistant.components.waze_travel_time.const import DOMAIN -from homeassistant.helpers.entity_registry import async_get - -from tests.common import MockConfigEntry - - -async def test_migration(hass, bypass_platform_setup): - """Test migration logic for unique id.""" - config_entry = MockConfigEntry( - domain=DOMAIN, version=1, entry_id="test", unique_id="test" - ) - ent_reg = async_get(hass) - ent_entry = ent_reg.async_get_or_create( - "sensor", DOMAIN, unique_id="replaceable_unique_id", config_entry=config_entry - ) - entity_id = ent_entry.entity_id - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.unique_id is None - assert ent_reg.async_get(entity_id).unique_id == config_entry.entry_id From c52b900bfe5cdf61360a3751cdee7f057eaae82b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Oct 2022 12:24:52 +0200 Subject: [PATCH 346/985] Minor cleanup of sensor statistics (#80082) --- homeassistant/components/sensor/recorder.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index c2aa99659cd..ba4924589f4 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -145,7 +145,7 @@ def _normalize_states( old_metadatas: dict[str, tuple[int, StatisticMetaData]], entity_history: Iterable[State], entity_id: str, -) -> tuple[str | None, str | None, list[tuple[float, State]]]: +) -> tuple[str | None, list[tuple[float, State]]]: """Normalize units.""" old_metadata = old_metadatas[entity_id][1] if entity_id in old_metadatas else None state_unit: str | None = None @@ -159,7 +159,7 @@ def _normalize_states( fstates.append((fstate, state)) if not fstates: - return None, None, fstates + return None, fstates state_unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -199,9 +199,9 @@ def _normalize_states( extra, LINK_DEV_STATISTICS, ) - return None, None, [] + return None, [] state_unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) - return state_unit, state_unit, fstates + return state_unit, fstates converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER[statistics_unit] valid_fstates: list[tuple[float, State]] = [] @@ -237,7 +237,7 @@ def _normalize_states( ) ) - return statistics_unit, state_unit, valid_fstates + return statistics_unit, valid_fstates def _suggest_report_issue(hass: HomeAssistant, entity_id: str) -> str: @@ -425,7 +425,7 @@ def _compile_statistics( # noqa: C901 continue entity_history = history_list[entity_id] - statistics_unit, state_unit, fstates = _normalize_states( + statistics_unit, fstates = _normalize_states( hass, session, old_metadatas, @@ -438,9 +438,7 @@ def _compile_statistics( # noqa: C901 state_class = _state.attributes[ATTR_STATE_CLASS] - to_process.append( - (entity_id, statistics_unit, state_unit, state_class, fstates) - ) + to_process.append((entity_id, statistics_unit, state_class, fstates)) if "sum" in wanted_statistics[entity_id]: to_query.append(entity_id) @@ -450,7 +448,6 @@ def _compile_statistics( # noqa: C901 for ( # pylint: disable=too-many-nested-blocks entity_id, statistics_unit, - state_unit, state_class, fstates, ) in to_process: From a391b8dd9d212caeaf597126162d2c42928471c2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Oct 2022 13:51:28 +0200 Subject: [PATCH 347/985] Support correcting sensor volume unit (#80081) --- homeassistant/components/sensor/recorder.py | 9 ++++++++- tests/components/sensor/test_recorder.py | 16 ++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index ba4924589f4..7bb2a998b9e 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -23,7 +23,12 @@ from homeassistant.components.recorder.models import ( StatisticMetaData, StatisticResult, ) -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, REVOLUTIONS_PER_MINUTE +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + REVOLUTIONS_PER_MINUTE, + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, +) from homeassistant.core import HomeAssistant, State, split_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import entity_sources @@ -49,6 +54,8 @@ DEFAULT_STATISTICS = { EQUIVALENT_UNITS = { "RPM": REVOLUTIONS_PER_MINUTE, + "ft3": VOLUME_CUBIC_FEET, + "m3": VOLUME_CUBIC_METERS, } # Keep track of entities for which a warning about decreasing value has been logged diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 73804e83d94..4b0db42c618 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -1912,6 +1912,9 @@ def test_list_statistic_ids_unsupported(hass_recorder, caplog, _attributes): ("battery", "%", "cats", None, 13.050847, -10, 30), ("battery", None, "cats", None, 13.050847, -10, 30), (None, "kW", "Wh", "power", 13.050847, -10, 30), + # Can't downgrade from ft³ to ft3 or from m³ to m3 + (None, "ft³", "ft3", "volume", 13.050847, -10, 30), + (None, "m³", "m3", "volume", 13.050847, -10, 30), ], ) def test_compile_hourly_statistics_changing_units_1( @@ -2193,10 +2196,12 @@ def test_compile_hourly_statistics_changing_units_3( @pytest.mark.parametrize( - "device_class, state_unit, state_unit2, unit_class, mean, mean2, min, max", + "device_class, state_unit, state_unit2, unit_class, unit_class2, mean, mean2, min, max", [ - (None, "RPM", "rpm", None, 13.050847, 13.333333, -10, 30), - (None, "rpm", "RPM", None, 13.050847, 13.333333, -10, 30), + (None, "RPM", "rpm", None, None, 13.050847, 13.333333, -10, 30), + (None, "rpm", "RPM", None, None, 13.050847, 13.333333, -10, 30), + (None, "ft3", "ft³", None, "volume", 13.050847, 13.333333, -10, 30), + (None, "m3", "m³", None, "volume", 13.050847, 13.333333, -10, 30), ], ) def test_compile_hourly_statistics_equivalent_units_1( @@ -2206,6 +2211,7 @@ def test_compile_hourly_statistics_equivalent_units_1( state_unit, state_unit2, unit_class, + unit_class2, mean, mean2, min, @@ -2277,7 +2283,7 @@ def test_compile_hourly_statistics_equivalent_units_1( "name": None, "source": "recorder", "statistics_unit_of_measurement": state_unit2, - "unit_class": unit_class, + "unit_class": unit_class2, }, ] stats = statistics_during_period(hass, zero, period="5minute") @@ -2317,6 +2323,8 @@ def test_compile_hourly_statistics_equivalent_units_1( [ (None, "RPM", "rpm", None, 13.333333, -10, 30), (None, "rpm", "RPM", None, 13.333333, -10, 30), + (None, "ft3", "ft³", None, 13.333333, -10, 30), + (None, "m3", "m³", None, 13.333333, -10, 30), ], ) def test_compile_hourly_statistics_equivalent_units_2( From bcbf99243d126533ca17a04e7bc40028aed756ec Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 11 Oct 2022 13:54:55 +0200 Subject: [PATCH 348/985] Use setup-python check-latest option [ci] (#80078) --- .github/workflows/ci.yaml | 60 +++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7fd4b284191..bb50142c9e0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -23,10 +23,8 @@ env: CACHE_VERSION: 1 PIP_CACHE_VERSION: 1 HA_SHORT_VERSION: 2022.11 - # Pin latest Python patch versions to avoid issues - # with runners using different versions. - DEFAULT_PYTHON: 3.9.14 - ALL_PYTHON_VERSIONS: "['3.9.14', '3.10.7']" + DEFAULT_PYTHON: 3.9 + ALL_PYTHON_VERSIONS: "['3.9', '3.10']" PRE_COMMIT_CACHE: ~/.cache/pre-commit PIP_CACHE: /tmp/pip-cache SQLALCHEMY_WARN_20: 1 @@ -69,7 +67,7 @@ jobs: - name: Generate partial pre-commit restore key id: generate_pre-commit_cache_key run: >- - echo "::set-output name=key::${{ env.CACHE_VERSION }}-${{ env.DEFAULT_PYTHON }}-${{ + echo "::set-output name=key::pre-commit-${{ env.CACHE_VERSION }}-${{ hashFiles('.pre-commit-config.yaml') }}" - name: Filter for core changes uses: dorny/paths-filter@v2.10.2 @@ -175,12 +173,15 @@ jobs: uses: actions/setup-python@v4.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true - name: Restore base Python virtual environment id: cache-venv uses: actions/cache@v3.0.10 with: path: venv - key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ + needs.info.outputs.pre-commit_cache_key }} - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -193,7 +194,9 @@ jobs: uses: actions/cache@v3.0.10 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.pre-commit_cache_key }} - name: Install pre-commit dependencies if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -214,12 +217,15 @@ jobs: id: python with: python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true - name: Restore base Python virtual environment id: cache-venv uses: actions/cache@v3.0.10 with: path: venv - key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ + needs.info.outputs.pre-commit_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -230,7 +236,9 @@ jobs: uses: actions/cache@v3.0.10 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.pre-commit_cache_key }} - name: Fail job if pre-commit cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -263,12 +271,15 @@ jobs: id: python with: python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true - name: Restore base Python virtual environment id: cache-venv uses: actions/cache@v3.0.10 with: path: venv - key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ + needs.info.outputs.pre-commit_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -279,7 +290,9 @@ jobs: uses: actions/cache@v3.0.10 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.pre-commit_cache_key }} - name: Fail job if pre-commit cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -315,12 +328,15 @@ jobs: id: python with: python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true - name: Restore base Python virtual environment id: cache-venv uses: actions/cache@v3.0.10 with: path: venv - key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ + needs.info.outputs.pre-commit_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -331,7 +347,9 @@ jobs: uses: actions/cache@v3.0.10 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.pre-commit_cache_key }} - name: Fail job if pre-commit cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -356,12 +374,15 @@ jobs: id: python with: python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true - name: Restore base Python virtual environment id: cache-venv uses: actions/cache@v3.0.10 with: path: venv - key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ + needs.info.outputs.pre-commit_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -372,7 +393,9 @@ jobs: uses: actions/cache@v3.0.10 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.pre-commit_cache_key }} - name: Fail job if pre-commit cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -478,6 +501,7 @@ jobs: uses: actions/setup-python@v4.3.0 with: python-version: ${{ matrix.python-version }} + check-latest: true - name: Generate partial pip restore key id: generate-pip-key run: >- @@ -541,6 +565,7 @@ jobs: uses: actions/setup-python@v4.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv uses: actions/cache@v3.0.10 @@ -573,6 +598,7 @@ jobs: uses: actions/setup-python@v4.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true - name: Restore base Python virtual environment id: cache-venv uses: actions/cache@v3.0.10 @@ -606,6 +632,7 @@ jobs: uses: actions/setup-python@v4.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv uses: actions/cache@v3.0.10 @@ -650,6 +677,7 @@ jobs: uses: actions/setup-python@v4.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv uses: actions/cache@v3.0.10 @@ -698,6 +726,7 @@ jobs: uses: actions/setup-python@v4.3.0 with: python-version: ${{ matrix.python-version }} + check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v3.0.10 @@ -752,6 +781,7 @@ jobs: uses: actions/setup-python@v4.3.0 with: python-version: ${{ matrix.python-version }} + check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v3.0.10 From 9aa60432558c98496978183683c070b2b9616b69 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Oct 2022 14:01:46 +0200 Subject: [PATCH 349/985] Set character set to utf8mb4 when connecting to MySQL or MariaDB databases (#79755) --- homeassistant/components/recorder/__init__.py | 5 +- homeassistant/components/recorder/const.py | 3 + homeassistant/components/recorder/core.py | 28 ++-- tests/components/recorder/test_init.py | 129 ++++++++++++++++++ 4 files changed, 156 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index f9ed5f59333..0d4bfe8e59b 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -69,7 +69,10 @@ ALLOW_IN_MEMORY_DB = False def validate_db_url(db_url: str) -> Any: """Validate database URL.""" # Don't allow on-memory sqlite databases - if (db_url == SQLITE_URL_PREFIX or ":memory:" in db_url) and not ALLOW_IN_MEMORY_DB: + if ( + db_url == SQLITE_URL_PREFIX + or (db_url.startswith(SQLITE_URL_PREFIX) and ":memory:" in db_url) + ) and not ALLOW_IN_MEMORY_DB: raise vol.Invalid("In-memory SQLite database is not supported") return db_url diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index 532644c7feb..66a9818b4b8 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -8,7 +8,10 @@ from homeassistant.helpers.json import ( # noqa: F401 pylint: disable=unused-im DATA_INSTANCE = "recorder_instance" SQLITE_URL_PREFIX = "sqlite://" +MARIADB_URL_PREFIX = "mariadb://" +MARIADB_PYMYSQL_URL_PREFIX = "mariadb+pymysql://" MYSQLDB_URL_PREFIX = "mysql://" +MYSQLDB_PYMYSQL_URL_PREFIX = "mysql+pymysql://" DOMAIN = "recorder" CONF_DB_INTEGRITY_CHECK = "db_integrity_check" diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 0511b42ebe4..032f1ff1ec2 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -46,7 +46,10 @@ from .const import ( DB_WORKER_PREFIX, DOMAIN, KEEPALIVE_TIME, + MARIADB_PYMYSQL_URL_PREFIX, + MARIADB_URL_PREFIX, MAX_QUEUE_BACKLOG, + MYSQLDB_PYMYSQL_URL_PREFIX, MYSQLDB_URL_PREFIX, SQLITE_URL_PREFIX, SupportedDialect, @@ -1114,14 +1117,23 @@ class Recorder(threading.Thread): kwargs["pool_reset_on_return"] = None elif self.db_url.startswith(SQLITE_URL_PREFIX): kwargs["poolclass"] = RecorderPool - elif self.db_url.startswith(MYSQLDB_URL_PREFIX): - # If they have configured MySQLDB but don't have - # the MySQLDB module installed this will throw - # an ImportError which we suppress here since - # sqlalchemy will give them a better error when - # it tried to import it below. - with contextlib.suppress(ImportError): - kwargs["connect_args"] = {"conv": build_mysqldb_conv()} + elif self.db_url.startswith( + ( + MARIADB_URL_PREFIX, + MARIADB_PYMYSQL_URL_PREFIX, + MYSQLDB_URL_PREFIX, + MYSQLDB_PYMYSQL_URL_PREFIX, + ) + ): + kwargs["connect_args"] = {"charset": "utf8mb4"} + if self.db_url.startswith((MARIADB_URL_PREFIX, MYSQLDB_URL_PREFIX)): + # If they have configured MySQLDB but don't have + # the MySQLDB module installed this will throw + # an ImportError which we suppress here since + # sqlalchemy will give them a better error when + # it tried to import it below. + with contextlib.suppress(ImportError): + kwargs["connect_args"]["conv"] = build_mysqldb_conv() # Disable extended logging for non SQLite databases if not self.db_url.startswith(SQLITE_URL_PREFIX): diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 4a801574ebb..815af89198d 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -17,12 +17,15 @@ from homeassistant.components.recorder import ( CONF_AUTO_PURGE, CONF_AUTO_REPACK, CONF_COMMIT_INTERVAL, + CONF_DB_MAX_RETRIES, + CONF_DB_RETRY_WAIT, CONF_DB_URL, CONFIG_SCHEMA, DOMAIN, SQLITE_URL_PREFIX, Recorder, get_instance, + pool, ) from homeassistant.components.recorder.const import KEEPALIVE_TIME from homeassistant.components.recorder.db_schema import ( @@ -1626,3 +1629,129 @@ async def test_disable_echo(hass, db_url, echo, caplog): await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: db_url}}) create_engine_mock.assert_called_once() assert create_engine_mock.mock_calls[0][2].get("echo") == echo + + +@pytest.mark.parametrize( + "config_url, connect_args", + ( + ( + "mariadb://user:password@SERVER_IP/DB_NAME", + {"charset": "utf8mb4"}, + ), + ( + "mariadb+pymysql://user:password@SERVER_IP/DB_NAME", + {"charset": "utf8mb4"}, + ), + ( + "mysql://user:password@SERVER_IP/DB_NAME", + {"charset": "utf8mb4"}, + ), + ( + "mysql+pymysql://user:password@SERVER_IP/DB_NAME", + {"charset": "utf8mb4"}, + ), + ( + "mysql://user:password@SERVER_IP/DB_NAME?charset=utf8mb4", + {"charset": "utf8mb4"}, + ), + ( + "mysql://user:password@SERVER_IP/DB_NAME?blah=bleh&charset=other", + {"charset": "utf8mb4"}, + ), + ( + "postgresql://blabla", + None, + ), + ( + "sqlite://blabla", + None, + ), + ), +) +async def test_mysql_missing_utf8mb4(hass, config_url, connect_args): + """Test recorder fails to setup if charset=utf8mb4 is missing from db_url.""" + recorder_helper.async_initialize_recorder(hass) + + class MockEvent: + def listen(self, _, _2, callback): + callback(None, None) + + mock_event = MockEvent() + with patch( + "homeassistant.components.recorder.core.create_engine" + ) as create_engine_mock, patch( + "homeassistant.components.recorder.core.sqlalchemy_event", mock_event + ): + await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: config_url}}) + create_engine_mock.assert_called_once() + assert create_engine_mock.mock_calls[0][2].get("connect_args") == connect_args + + +@pytest.mark.parametrize( + "config_url", + ( + "mysql://user:password@SERVER_IP/DB_NAME", + "mysql://user:password@SERVER_IP/DB_NAME?charset=utf8mb4", + "mysql://user:password@SERVER_IP/DB_NAME?blah=bleh&charset=other", + ), +) +async def test_connect_args_priority(hass, config_url): + """Test connect_args has priority over URL query.""" + connect_params = [] + recorder_helper.async_initialize_recorder(hass) + + class MockDialect: + """Non functioning dialect, good enough that SQLAlchemy tries connecting.""" + + __bases__ = [] + _has_events = False + + def __init__(*args, **kwargs): + ... + + def connect(self, *args, **params): + nonlocal connect_params + connect_params.append(params) + return True + + def create_connect_args(self, url): + return ([], {"charset": "invalid"}) + + @classmethod + def dbapi(cls): + ... + + def engine_created(*args): + ... + + def get_dialect_pool_class(self, *args): + return pool.RecorderPool + + def initialize(*args): + ... + + def on_connect_url(self, url): + return False + + class MockEntrypoint: + def engine_created(*_): + ... + + def get_dialect_cls(*_): + return MockDialect + + with patch("sqlalchemy.engine.url.URL._get_entrypoint", MockEntrypoint), patch( + "sqlalchemy.engine.create.util.get_cls_kwargs", return_value=["echo"] + ): + await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DB_URL: config_url, + CONF_DB_MAX_RETRIES: 1, + CONF_DB_RETRY_WAIT: 0, + } + }, + ) + assert connect_params == [{"charset": "utf8mb4"}] From c9130e2892ea6fa6e96590961bed2b6e61870e8e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Oct 2022 14:02:27 +0200 Subject: [PATCH 350/985] Use REVOLUTIONS_PER_MINUTE constant in vallox (#79992) --- homeassistant/components/vallox/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 2e00452fdf2..c349107a3f3 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, + REVOLUTIONS_PER_MINUTE, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -156,7 +157,7 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( metric_key="A_CYC_EXTR_FAN_SPEED", icon="mdi:fan", state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement="RPM", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, entity_type=ValloxFanSpeedSensor, entity_registry_enabled_default=False, ), @@ -166,7 +167,7 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( metric_key="A_CYC_SUPP_FAN_SPEED", icon="mdi:fan", state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement="RPM", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, entity_type=ValloxFanSpeedSensor, entity_registry_enabled_default=False, ), From e7c614a8253ca1cf9e75f34e73d5f78aa3fc4cd7 Mon Sep 17 00:00:00 2001 From: Sean Vig Date: Tue, 11 Oct 2022 08:29:35 -0400 Subject: [PATCH 351/985] Fix audio detection for IP4m-1041 Amcrest camera (#80066) --- homeassistant/components/amcrest/__init__.py | 5 +- .../components/amcrest/binary_sensor.py | 50 +++++++++---------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index e3f48263e8b..8fea717e6bb 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -370,11 +370,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) ) event_codes = { - sensor.event_code + event_code for sensor in BINARY_SENSORS if sensor.key in binary_sensors and not sensor.should_poll - and sensor.event_code is not None + and sensor.event_codes is not None + for event_code in sensor.event_codes } _start_event_monitor(hass, name, api, event_codes) diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index e583aad904b..4d438c7c3bf 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -39,7 +39,7 @@ if TYPE_CHECKING: class AmcrestSensorEntityDescription(BinarySensorEntityDescription): """Describe Amcrest sensor entity.""" - event_code: str | None = None + event_codes: set[str] | None = None should_poll: bool = False @@ -51,7 +51,7 @@ _ONLINE_SCAN_INTERVAL = timedelta(seconds=60 - BINARY_SENSOR_SCAN_INTERVAL_SECS) _AUDIO_DETECTED_KEY = "audio_detected" _AUDIO_DETECTED_POLLED_KEY = "audio_detected_polled" _AUDIO_DETECTED_NAME = "Audio Detected" -_AUDIO_DETECTED_EVENT_CODE = "AudioMutation" +_AUDIO_DETECTED_EVENT_CODES = {"AudioMutation", "AudioIntensity"} _CROSSLINE_DETECTED_KEY = "crossline_detected" _CROSSLINE_DETECTED_POLLED_KEY = "crossline_detected_polled" @@ -70,39 +70,39 @@ BINARY_SENSORS: tuple[AmcrestSensorEntityDescription, ...] = ( key=_AUDIO_DETECTED_KEY, name=_AUDIO_DETECTED_NAME, device_class=BinarySensorDeviceClass.SOUND, - event_code=_AUDIO_DETECTED_EVENT_CODE, + event_codes=_AUDIO_DETECTED_EVENT_CODES, ), AmcrestSensorEntityDescription( key=_AUDIO_DETECTED_POLLED_KEY, name=_AUDIO_DETECTED_NAME, device_class=BinarySensorDeviceClass.SOUND, - event_code=_AUDIO_DETECTED_EVENT_CODE, + event_codes=_AUDIO_DETECTED_EVENT_CODES, should_poll=True, ), AmcrestSensorEntityDescription( key=_CROSSLINE_DETECTED_KEY, name=_CROSSLINE_DETECTED_NAME, device_class=BinarySensorDeviceClass.MOTION, - event_code=_CROSSLINE_DETECTED_EVENT_CODE, + event_codes={_CROSSLINE_DETECTED_EVENT_CODE}, ), AmcrestSensorEntityDescription( key=_CROSSLINE_DETECTED_POLLED_KEY, name=_CROSSLINE_DETECTED_NAME, device_class=BinarySensorDeviceClass.MOTION, - event_code=_CROSSLINE_DETECTED_EVENT_CODE, + event_codes={_CROSSLINE_DETECTED_EVENT_CODE}, should_poll=True, ), AmcrestSensorEntityDescription( key=_MOTION_DETECTED_KEY, name=_MOTION_DETECTED_NAME, device_class=BinarySensorDeviceClass.MOTION, - event_code=_MOTION_DETECTED_EVENT_CODE, + event_codes={_MOTION_DETECTED_EVENT_CODE}, ), AmcrestSensorEntityDescription( key=_MOTION_DETECTED_POLLED_KEY, name=_MOTION_DETECTED_NAME, device_class=BinarySensorDeviceClass.MOTION, - event_code=_MOTION_DETECTED_EVENT_CODE, + event_codes={_MOTION_DETECTED_EVENT_CODE}, should_poll=True, ), AmcrestSensorEntityDescription( @@ -211,13 +211,13 @@ class AmcrestBinarySensor(BinarySensorEntity): log_update_error(_LOGGER, "update", self.name, "binary sensor", error) return - if (event_code := self.entity_description.event_code) is None: - _LOGGER.error("Binary sensor %s event code not set", self.name) - return + if not (event_codes := self.entity_description.event_codes): + raise ValueError(f"Binary sensor {self.name} event codes not set") try: - self._attr_is_on = ( + self._attr_is_on = any( # type: ignore[arg-type] len(await self._api.async_event_channels_happened(event_code)) > 0 + for event_code in event_codes ) except AmcrestError as error: log_update_error(_LOGGER, "update", self.name, "binary sensor", error) @@ -266,17 +266,17 @@ class AmcrestBinarySensor(BinarySensorEntity): ) if ( - self.entity_description.event_code - and not self.entity_description.should_poll - ): - self.async_on_remove( - async_dispatcher_connect( - self.hass, - service_signal( - SERVICE_EVENT, - self._signal_name, - self.entity_description.event_code, - ), - self.async_event_received, + event_codes := self.entity_description.event_codes + ) and not self.entity_description.should_poll: + for event_code in event_codes: + self.async_on_remove( + async_dispatcher_connect( + self.hass, + service_signal( + SERVICE_EVENT, + self._signal_name, + event_code, + ), + self.async_event_received, + ) ) - ) From 2538b9d2697fd1a9837918db33bb98ce38805770 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Oct 2022 14:43:13 +0200 Subject: [PATCH 352/985] Use REVOLUTIONS_PER_MINUTE constant in baf (#79986) --- homeassistant/components/baf/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/baf/sensor.py b/homeassistant/components/baf/sensor.py index 7b93b22fe2f..79ae320969b 100644 --- a/homeassistant/components/baf/sensor.py +++ b/homeassistant/components/baf/sensor.py @@ -14,7 +14,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import PERCENTAGE, REVOLUTIONS_PER_MINUTE, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -65,7 +65,7 @@ FAN_SENSORS = ( BAFSensorDescription( key="current_rpm", name="Current RPM", - native_unit_of_measurement="RPM", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: cast(Optional[int], device.current_rpm), @@ -73,7 +73,7 @@ FAN_SENSORS = ( BAFSensorDescription( key="target_rpm", name="Target RPM", - native_unit_of_measurement="RPM", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: cast(Optional[int], device.target_rpm), From b1dd646ed80285d82570e0fd1ee322ee487ec423 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Oct 2022 14:56:10 +0200 Subject: [PATCH 353/985] Use REVOLUTIONS_PER_MINUTE constant in danfoss_air (#79987) --- homeassistant/components/danfoss_air/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/danfoss_air/sensor.py b/homeassistant/components/danfoss_air/sensor.py index de736b2c599..51eab3e471c 100644 --- a/homeassistant/components/danfoss_air/sensor.py +++ b/homeassistant/components/danfoss_air/sensor.py @@ -10,7 +10,7 @@ from homeassistant.components.sensor import ( SensorEntity, SensorStateClass, ) -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import PERCENTAGE, REVOLUTIONS_PER_MINUTE, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -75,14 +75,14 @@ def setup_platform( ["Danfoss Air Fan Step", PERCENTAGE, ReadCommand.fan_step, None, None], [ "Danfoss Air Exhaust Fan Speed", - "RPM", + REVOLUTIONS_PER_MINUTE, ReadCommand.exhaust_fan_speed, None, None, ], [ "Danfoss Air Supply Fan Speed", - "RPM", + REVOLUTIONS_PER_MINUTE, ReadCommand.supply_fan_speed, None, None, From b81c7d7f8ed01496a6554801333eedacfe97e84b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Oct 2022 14:56:35 +0200 Subject: [PATCH 354/985] Use REVOLUTIONS_PER_MINUTE constant in glances (#79988) --- homeassistant/components/glances/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index f6ae6b6ec17..13f4284acd3 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( DATA_GIBIBYTES, DATA_MEBIBYTES, PERCENTAGE, + REVOLUTIONS_PER_MINUTE, STATE_UNAVAILABLE, TEMP_CELSIUS, Platform, @@ -174,7 +175,7 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( key="fan_speed", type="sensors", name_suffix="Fan speed", - native_unit_of_measurement="RPM", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, icon="mdi:fan", state_class=SensorStateClass.MEASUREMENT, ), From e948f498188c976403ee47ba820f1f03335e6c59 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Oct 2022 14:56:55 +0200 Subject: [PATCH 355/985] Use REVOLUTIONS_PER_MINUTE constant in system_bridge (#79990) --- homeassistant/components/system_bridge/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index f9d6e09f0c6..ab34e6b91b5 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -22,6 +22,7 @@ from homeassistant.const import ( FREQUENCY_MEGAHERTZ, PERCENTAGE, POWER_WATT, + REVOLUTIONS_PER_MINUTE, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -41,7 +42,6 @@ ATTR_TYPE: Final = "type" ATTR_USED: Final = "used" PIXELS: Final = "px" -RPM: Final = "RPM" @dataclass @@ -439,7 +439,7 @@ async def async_setup_entry( name=f"{gpu['name']} Fan Speed", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=RPM, + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, icon="mdi:fan", value=lambda data, k=gpu["key"]: getattr( data.gpu, f"{k}_fan_speed" From 918243b7c8a4a41c2b8cef65bf0444cba6996928 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Oct 2022 14:57:08 +0200 Subject: [PATCH 356/985] Improve some sensor statistics tests (#80087) --- tests/components/sensor/test_recorder.py | 88 +++++++++++++++--------- 1 file changed, 55 insertions(+), 33 deletions(-) diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 4b0db42c618..e7f8139d7d3 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -3327,10 +3327,17 @@ def record_states(hass, zero, entity_id, attributes, seq=None): ), ], ) -async def test_validate_statistics_unit_change_device_class( +async def test_validate_unit_change_convertible( hass, hass_ws_client, recorder_mock, units, attributes, unit, unit2, supported_unit ): - """Test validate_statistics.""" + """Test validate_statistics. + + This tests what happens if a sensor is first recorded in a unit which supports unit + conversion, and the unit is then changed to a unit which can and can not be + converted to the original unit. + + The test also asserts that the sensor's device class is ignored. + """ id = 1 def next_id(): @@ -3400,7 +3407,7 @@ async def test_validate_statistics_unit_change_device_class( await assert_validation_result(client, {}) # Valid state, statistic runs again - empty response - do_adhoc_statistics(hass, start=now) + do_adhoc_statistics(hass, start=now + timedelta(hours=1)) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -3412,11 +3419,11 @@ async def test_validate_statistics_unit_change_device_class( await assert_validation_result(client, {}) # Valid state, statistic runs again - empty response - do_adhoc_statistics(hass, start=now) + do_adhoc_statistics(hass, start=now + timedelta(hours=2)) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) - # Remove the state - empty response + # Remove the state - expect error about missing state hass.states.async_remove("sensor.test") expected = { "sensor.test": [ @@ -3430,15 +3437,18 @@ async def test_validate_statistics_unit_change_device_class( @pytest.mark.parametrize( - "units, attributes, valid_units", + "units, attributes", [ - (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W, kW"), + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES), ], ) -async def test_validate_statistics_unit_change_device_class_2( - hass, hass_ws_client, recorder_mock, units, attributes, valid_units +async def test_validate_statistics_unit_ignore_device_class( + hass, hass_ws_client, recorder_mock, units, attributes ): - """Test validate_statistics.""" + """Test validate_statistics. + + The test asserts that the sensor's device class is ignored. + """ id = 1 def next_id(): @@ -3506,7 +3516,12 @@ async def test_validate_statistics_unit_change_device_class_2( async def test_validate_statistics_unit_change_no_device_class( hass, hass_ws_client, recorder_mock, units, attributes, unit, unit2, supported_unit ): - """Test validate_statistics.""" + """Test validate_statistics. + + This tests what happens if a sensor is first recorded in a unit which supports unit + conversion, and the unit is then changed to a unit which can and can not be + converted to the original unit. + """ id = 1 attributes = dict(attributes) attributes.pop("device_class") @@ -3534,14 +3549,14 @@ async def test_validate_statistics_unit_change_no_device_class( # No statistics, no state - empty response await assert_validation_result(client, {}) - # No statistics, unit in state matching device class - empty response + # No statistics, sensor state set - empty response hass.states.async_set( "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit}} ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) - # No statistics, unit in state not matching device class - empty response + # No statistics, sensor state set to an incompatible unit - empty response hass.states.async_set( "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}} ) @@ -3578,7 +3593,7 @@ async def test_validate_statistics_unit_change_no_device_class( await assert_validation_result(client, {}) # Valid state, statistic runs again - empty response - do_adhoc_statistics(hass, start=now) + do_adhoc_statistics(hass, start=now + timedelta(hours=1)) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -3590,11 +3605,11 @@ async def test_validate_statistics_unit_change_no_device_class( await assert_validation_result(client, {}) # Valid state, statistic runs again - empty response - do_adhoc_statistics(hass, start=now) + do_adhoc_statistics(hass, start=now + timedelta(hours=2)) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) - # Remove the state - empty response + # Remove the state - expect error about missing state hass.states.async_remove("sensor.test") expected = { "sensor.test": [ @@ -3849,11 +3864,14 @@ async def test_validate_statistics_sensor_removed( @pytest.mark.parametrize( - "attributes", - [BATTERY_SENSOR_ATTRIBUTES, NONE_SENSOR_ATTRIBUTES], + "attributes, unit1, unit2", + [ + (BATTERY_SENSOR_ATTRIBUTES, "%", "dogs"), + (NONE_SENSOR_ATTRIBUTES, None, "dogs"), + ], ) async def test_validate_statistics_unit_change_no_conversion( - hass, recorder_mock, hass_ws_client, attributes + hass, recorder_mock, hass_ws_client, attributes, unit1, unit2 ): """Test validate_statistics.""" id = 1 @@ -3892,12 +3910,14 @@ async def test_validate_statistics_unit_change_no_conversion( await assert_validation_result(client, {}) # No statistics, original unit - empty response - hass.states.async_set("sensor.test", 10, attributes=attributes) + hass.states.async_set( + "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit1}} + ) await assert_validation_result(client, {}) # No statistics, changed unit - empty response hass.states.async_set( - "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": unit2}} ) await assert_validation_result(client, {}) @@ -3907,32 +3927,34 @@ async def test_validate_statistics_unit_change_no_conversion( await async_recorder_block_till_done(hass) await assert_statistic_ids([]) - # No statistics, changed unit - empty response + # No statistics, original unit - empty response hass.states.async_set( - "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": unit1}} ) await assert_validation_result(client, {}) - # Run statistics one hour later, only the "dogs" state will be considered + # Run statistics one hour later, only the state with unit1 will be considered await async_recorder_block_till_done(hass) do_adhoc_statistics(hass, start=now + timedelta(hours=1)) await async_recorder_block_till_done(hass) await assert_statistic_ids( - [{"statistic_id": "sensor.test", "unit_of_measurement": "dogs"}] + [{"statistic_id": "sensor.test", "unit_of_measurement": unit1}] ) await assert_validation_result(client, {}) - # Change back to original unit - expect error - hass.states.async_set("sensor.test", 13, attributes=attributes) + # Change unit - expect error + hass.states.async_set( + "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit2}} + ) await async_recorder_block_till_done(hass) expected = { "sensor.test": [ { "data": { - "metadata_unit": "dogs", - "state_unit": attributes.get("unit_of_measurement"), + "metadata_unit": unit1, + "state_unit": unit2, "statistic_id": "sensor.test", - "supported_unit": "dogs", + "supported_unit": unit1, }, "type": "units_changed", } @@ -3940,16 +3962,16 @@ async def test_validate_statistics_unit_change_no_conversion( } await assert_validation_result(client, expected) - # Changed unit - empty response + # Original unit - empty response hass.states.async_set( - "sensor.test", 14, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + "sensor.test", 14, attributes={**attributes, **{"unit_of_measurement": unit1}} ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) # Valid state, statistic runs again - empty response await async_recorder_block_till_done(hass) - do_adhoc_statistics(hass, start=now) + do_adhoc_statistics(hass, start=now + timedelta(hours=2)) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) From 62c4cd3c26d201ffff15876adf4d66a5fb00da34 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 11 Oct 2022 16:56:45 +0200 Subject: [PATCH 357/985] Add name and slug to supervisor discovery info (#80094) --- homeassistant/components/hassio/discovery.py | 21 +++++--- tests/components/adguard/test_config_flow.py | 24 +++++++-- tests/components/almond/test_config_flow.py | 8 ++- tests/components/deconz/test_config_flow.py | 12 +++-- tests/components/hassio/test_discovery.py | 18 ++++--- .../components/motioneye/test_config_flow.py | 30 +++++++++-- tests/components/mqtt/test_config_flow.py | 12 +++-- .../rtsp_to_webrtc/test_config_flow.py | 16 ++++-- .../components/vlc_telnet/test_config_flow.py | 14 ++++-- tests/components/zwave_js/test_config_flow.py | 50 +++++++++++++++---- tests/test_config_entries.py | 5 +- 11 files changed, 160 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index e8cbbfc6bf5..ee680c98ee0 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -17,7 +17,7 @@ from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import discovery_flow from .const import ATTR_ADDON, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_UUID -from .handler import HassioAPIError +from .handler import HassIO, HassioAPIError _LOGGER = logging.getLogger(__name__) @@ -27,6 +27,8 @@ class HassioServiceInfo(BaseServiceInfo): """Prepared info from hassio entries.""" config: dict[str, Any] + name: str + slug: str @callback @@ -62,7 +64,7 @@ class HassIODiscovery(HomeAssistantView): name = "api:hassio_push:discovery" url = "/api/hassio_push/discovery/{uuid}" - def __init__(self, hass: HomeAssistant, hassio): + def __init__(self, hass: HomeAssistant, hassio: HassIO) -> None: """Initialize WebView.""" self.hass = hass self.hassio = hassio @@ -86,25 +88,28 @@ class HassIODiscovery(HomeAssistantView): await self.async_process_del(data) return web.Response() - async def async_process_new(self, data): + async def async_process_new(self, data: dict[str, Any]) -> None: """Process add discovery entry.""" - service = data[ATTR_SERVICE] - config_data = data[ATTR_CONFIG] + service: str = data[ATTR_SERVICE] + config_data: dict[str, Any] = data[ATTR_CONFIG] + slug: str = data[ATTR_ADDON] # Read additional Add-on info try: - addon_info = await self.hassio.get_addon_info(data[ATTR_ADDON]) + addon_info = await self.hassio.get_addon_info(slug) except HassioAPIError as err: _LOGGER.error("Can't read add-on info: %s", err) return - config_data[ATTR_ADDON] = addon_info[ATTR_NAME] + + name: str = addon_info[ATTR_NAME] + config_data[ATTR_ADDON] = name # Use config flow discovery_flow.async_create_flow( self.hass, service, context={"source": config_entries.SOURCE_HASSIO}, - data=HassioServiceInfo(config=config_data), + data=HassioServiceInfo(config=config_data, name=name, slug=slug), ) async def async_process_del(self, data): diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index 4bcfb60e7b6..2fdae7b9d6b 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -127,7 +127,9 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: "addon": "AdGuard Home Addon", "host": "mock-adguard", "port": "3000", - } + }, + name="AdGuard Home Addon", + slug="adguard", ), context={"source": config_entries.SOURCE_HASSIO}, ) @@ -149,7 +151,9 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: "addon": "AdGuard Home Addon", "host": "mock-adguard", "port": "3000", - } + }, + name="AdGuard Home Addon", + slug="adguard", ), context={"source": config_entries.SOURCE_HASSIO}, ) @@ -171,7 +175,13 @@ async def test_hassio_confirm( result = await hass.config_entries.flow.async_init( DOMAIN, data=HassioServiceInfo( - config={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": 3000} + config={ + "addon": "AdGuard Home Addon", + "host": "mock-adguard", + "port": 3000, + }, + name="AdGuard Home Addon", + slug="adguard", ), context={"source": config_entries.SOURCE_HASSIO}, ) @@ -207,7 +217,13 @@ async def test_hassio_connection_error( result = await hass.config_entries.flow.async_init( DOMAIN, data=HassioServiceInfo( - config={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": 3000} + config={ + "addon": "AdGuard Home Addon", + "host": "mock-adguard", + "port": 3000, + }, + name="AdGuard Home Addon", + slug="adguard", ), context={"source": config_entries.SOURCE_HASSIO}, ) diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py index 3bf2db14b95..511a5cf08dc 100644 --- a/tests/components/almond/test_config_flow.py +++ b/tests/components/almond/test_config_flow.py @@ -53,7 +53,9 @@ async def test_hassio(hass): DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, data=HassioServiceInfo( - config={"addon": "Almond add-on", "host": "almond-addon", "port": "1234"} + config={"addon": "Almond add-on", "host": "almond-addon", "port": "1234"}, + name="Almond add-on", + slug="almond", ), ) @@ -90,7 +92,9 @@ async def test_abort_if_existing_entry(hass): assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" - result = await flow.async_step_hassio(HassioServiceInfo(config={})) + result = await flow.async_step_hassio( + HassioServiceInfo(config={}, name="Almond add-on", slug="almond") + ) assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 2f21081a5ae..7a4c73923ec 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -545,7 +545,9 @@ async def test_flow_hassio_discovery(hass): CONF_PORT: 80, CONF_SERIAL: BRIDGEID, CONF_API_KEY: API_KEY, - } + }, + name="Mock Addon", + slug="deconz", ), context={"source": SOURCE_HASSIO}, ) @@ -593,7 +595,9 @@ async def test_hassio_discovery_update_configuration(hass, aioclient_mock): CONF_PORT: 8080, CONF_API_KEY: "updated", CONF_SERIAL: BRIDGEID, - } + }, + name="Mock Addon", + slug="deconz", ), context={"source": SOURCE_HASSIO}, ) @@ -619,7 +623,9 @@ async def test_hassio_discovery_dont_update_configuration(hass, aioclient_mock): CONF_PORT: 80, CONF_API_KEY: API_KEY, CONF_SERIAL: BRIDGEID, - } + }, + name="Mock Addon", + slug="deconz", ), context={"source": SOURCE_HASSIO}, ) diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index 655cc4b23b5..94e989f3c77 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant import config_entries -from homeassistant.components.hassio import HassioServiceInfo +from homeassistant.components.hassio.discovery import HassioServiceInfo from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED @@ -14,8 +14,8 @@ from homeassistant.setup import async_setup_component from tests.common import MockModule, mock_entity_platform, mock_integration -@pytest.fixture -async def mock_mqtt(hass): +@pytest.fixture(name="mock_mqtt") +async def mock_mqtt_fixture(hass): """Mock the MQTT integration's config flow.""" mock_integration(hass, MockModule(MQTT_DOMAIN)) mock_entity_platform(hass, f"config_flow.{MQTT_DOMAIN}", None) @@ -78,7 +78,9 @@ async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client, moc "password": "mock-pass", "protocol": "3.1.1", "addon": "Mosquitto Test", - } + }, + name="Mosquitto Test", + slug="mosquitto", ) ) @@ -140,7 +142,9 @@ async def test_hassio_discovery_startup_done( "password": "mock-pass", "protocol": "3.1.1", "addon": "Mosquitto Test", - } + }, + name="Mosquitto Test", + slug="mosquitto", ) ) @@ -190,6 +194,8 @@ async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client, moc "password": "mock-pass", "protocol": "3.1.1", "addon": "Mosquitto Test", - } + }, + name="Mosquitto Test", + slug="mosquitto", ) ) diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index 6fe38ccf7a1..edb987e5664 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -75,7 +75,11 @@ async def test_hassio_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, - data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), + data=HassioServiceInfo( + config={"addon": "motionEye", "url": TEST_URL}, + name="motionEye", + slug="motioneye", + ), context={"source": config_entries.SOURCE_HASSIO}, ) @@ -351,7 +355,11 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, - data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), + data=HassioServiceInfo( + config={"addon": "motionEye", "url": TEST_URL}, + name="motionEye", + slug="motioneye", + ), context={"source": config_entries.SOURCE_HASSIO}, ) assert result.get("type") == data_entry_flow.FlowResultType.ABORT @@ -366,7 +374,11 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, - data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), + data=HassioServiceInfo( + config={"addon": "motionEye", "url": TEST_URL}, + name="motionEye", + slug="motioneye", + ), context={"source": config_entries.SOURCE_HASSIO}, ) assert result.get("type") == data_entry_flow.FlowResultType.ABORT @@ -382,7 +394,11 @@ async def test_hassio_abort_if_already_in_progress(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_init( DOMAIN, - data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), + data=HassioServiceInfo( + config={"addon": "motionEye", "url": TEST_URL}, + name="motionEye", + slug="motioneye", + ), context={"source": config_entries.SOURCE_HASSIO}, ) assert result2.get("type") == data_entry_flow.FlowResultType.ABORT @@ -394,7 +410,11 @@ async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, - data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), + data=HassioServiceInfo( + config={"addon": "motionEye", "url": TEST_URL}, + name="motionEye", + slug="motioneye", + ), context={"source": config_entries.SOURCE_HASSIO}, ) assert result.get("type") == data_entry_flow.FlowResultType.FORM diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 631f373316b..c60df7089ad 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -237,7 +237,9 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: "host": "mock-mosquitto", "port": "1883", "protocol": "3.1.1", - } + }, + name="Mosquitto", + slug="mosquitto", ), context={"source": config_entries.SOURCE_HASSIO}, ) @@ -261,7 +263,9 @@ async def test_hassio_confirm(hass, mock_try_connection_success, mock_finish_set "password": "mock-pass", "protocol": "3.1.1", # Set by the addon's discovery, ignored by HA "ssl": False, # Set by the addon's discovery, ignored by HA - } + }, + name="Mock Addon", + slug="mosquitto", ), context={"source": config_entries.SOURCE_HASSIO}, ) @@ -305,7 +309,9 @@ async def test_hassio_cannot_connect( "password": "mock-pass", "protocol": "3.1.1", # Set by the addon's discovery, ignored by HA "ssl": False, # Set by the addon's discovery, ignored by HA - } + }, + name="Mock Addon", + slug="mosquitto", ), context={"source": config_entries.SOURCE_HASSIO}, ) diff --git a/tests/components/rtsp_to_webrtc/test_config_flow.py b/tests/components/rtsp_to_webrtc/test_config_flow.py index cca6395c317..6386a942cc4 100644 --- a/tests/components/rtsp_to_webrtc/test_config_flow.py +++ b/tests/components/rtsp_to_webrtc/test_config_flow.py @@ -124,7 +124,9 @@ async def test_hassio_discovery(hass): "addon": "RTSPtoWebRTC", "host": "fake-server", "port": 8083, - } + }, + name="RTSPtoWebRTC", + slug="rtsp-to-webrtc", ), context={"source": config_entries.SOURCE_HASSIO}, ) @@ -161,7 +163,9 @@ async def test_hassio_single_config_entry(hass: HomeAssistant) -> None: "addon": "RTSPtoWebRTC", "host": "fake-server", "port": 8083, - } + }, + name="RTSPtoWebRTC", + slug="rtsp-to-webrtc", ), context={"source": config_entries.SOURCE_HASSIO}, ) @@ -181,7 +185,9 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: "addon": "RTSPtoWebRTC", "host": "fake-server", "port": 8083, - } + }, + name="RTSPtoWebRTC", + slug="rtsp-to-webrtc", ), context={"source": config_entries.SOURCE_HASSIO}, ) @@ -198,7 +204,9 @@ async def test_hassio_discovery_server_failure(hass: HomeAssistant) -> None: "addon": "RTSPtoWebRTC", "host": "fake-server", "port": 8083, - } + }, + name="RTSPtoWebRTC", + slug="rtsp-to-webrtc", ), context={"source": config_entries.SOURCE_HASSIO}, ) diff --git a/tests/components/vlc_telnet/test_config_flow.py b/tests/components/vlc_telnet/test_config_flow.py index 5e712c71b24..f5059517e3e 100644 --- a/tests/components/vlc_telnet/test_config_flow.py +++ b/tests/components/vlc_telnet/test_config_flow.py @@ -246,8 +246,10 @@ async def test_hassio_flow(hass: HomeAssistant) -> None: "host": "1.1.1.1", "port": 8888, "name": "custom name", - "addon": "vlc", - } + "addon": "VLC", + }, + name="VLC", + slug="vlc", ) result = await hass.config_entries.flow.async_init( @@ -284,7 +286,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data=HassioServiceInfo(config=entry_data), + data=HassioServiceInfo(config=entry_data, name="VLC", slug="vlc"), ) await hass.async_block_till_done() @@ -324,8 +326,10 @@ async def test_hassio_errors( "host": "1.1.1.1", "port": 8888, "name": "custom name", - "addon": "vlc", - } + "addon": "VLC", + }, + name="VLC", + slug="vlc", ), ) await hass.async_block_till_done() diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index d4f159f2510..d297b183dd1 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -15,7 +15,7 @@ from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.components.zwave_js.config_flow import SERVER_VERSION_TIMEOUT, TITLE -from homeassistant.components.zwave_js.const import DOMAIN +from homeassistant.components.zwave_js.const import ADDON_SLUG, DOMAIN from tests.common import MockConfigEntry @@ -326,7 +326,11 @@ async def test_supervisor_discovery( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data=HassioServiceInfo(config=ADDON_DISCOVERY_INFO), + data=HassioServiceInfo( + config=ADDON_DISCOVERY_INFO, + name="Z-Wave JS", + slug=ADDON_SLUG, + ), ) with patch( @@ -366,7 +370,11 @@ async def test_supervisor_discovery_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data=HassioServiceInfo(config=ADDON_DISCOVERY_INFO), + data=HassioServiceInfo( + config=ADDON_DISCOVERY_INFO, + name="Z-Wave JS", + slug=ADDON_SLUG, + ), ) assert result["type"] == "abort" @@ -388,7 +396,11 @@ async def test_clean_discovery_on_user_create( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data=HassioServiceInfo(config=ADDON_DISCOVERY_INFO), + data=HassioServiceInfo( + config=ADDON_DISCOVERY_INFO, + name="Z-Wave JS", + slug=ADDON_SLUG, + ), ) assert result["type"] == "form" @@ -454,7 +466,11 @@ async def test_abort_discovery_with_existing_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data=HassioServiceInfo(config=ADDON_DISCOVERY_INFO), + data=HassioServiceInfo( + config=ADDON_DISCOVERY_INFO, + name="Z-Wave JS", + slug=ADDON_SLUG, + ), ) assert result["type"] == "abort" @@ -478,7 +494,11 @@ async def test_abort_hassio_discovery_with_existing_flow( result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data=HassioServiceInfo(config=ADDON_DISCOVERY_INFO), + data=HassioServiceInfo( + config=ADDON_DISCOVERY_INFO, + name="Z-Wave JS", + slug=ADDON_SLUG, + ), ) assert result2["type"] == "abort" @@ -673,7 +693,11 @@ async def test_discovery_addon_not_running( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data=HassioServiceInfo(config=ADDON_DISCOVERY_INFO), + data=HassioServiceInfo( + config=ADDON_DISCOVERY_INFO, + name="Z-Wave JS", + slug=ADDON_SLUG, + ), ) assert result["step_id"] == "hassio_confirm" @@ -753,7 +777,11 @@ async def test_discovery_addon_not_installed( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data=HassioServiceInfo(config=ADDON_DISCOVERY_INFO), + data=HassioServiceInfo( + config=ADDON_DISCOVERY_INFO, + name="Z-Wave JS", + slug=ADDON_SLUG, + ), ) assert result["step_id"] == "hassio_confirm" @@ -834,7 +862,11 @@ async def test_abort_usb_discovery_with_existing_flow(hass, supervisor, addon_op result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, - data=HassioServiceInfo(config=ADDON_DISCOVERY_INFO), + data=HassioServiceInfo( + config=ADDON_DISCOVERY_INFO, + name="Z-Wave JS", + slug=ADDON_SLUG, + ), ) assert result["type"] == "form" diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index d2d4ffe1134..83343146d47 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -2507,7 +2507,10 @@ async def test_async_setup_update_entry(hass): (config_entries.SOURCE_HOMEKIT, BaseServiceInfo()), (config_entries.SOURCE_DHCP, BaseServiceInfo()), (config_entries.SOURCE_ZEROCONF, BaseServiceInfo()), - (config_entries.SOURCE_HASSIO, HassioServiceInfo(config={})), + ( + config_entries.SOURCE_HASSIO, + HassioServiceInfo(config={}, name="Test", slug="test"), + ), ), ) async def test_flow_with_default_discovery(hass, manager, discovery_source): From f2207af1c96b918d0b4bd365b2730c731da3557e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 11 Oct 2022 09:26:44 -0600 Subject: [PATCH 358/985] Use `entry.as_dict()` in Ambient PWS diagnostics (#80111) --- .../components/ambient_station/diagnostics.py | 11 +-- .../ambient_station/test_diagnostics.py | 75 +++++++++++-------- 2 files changed, 48 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/ambient_station/diagnostics.py b/homeassistant/components/ambient_station/diagnostics.py index 6005b206954..d18047fe8e4 100644 --- a/homeassistant/components/ambient_station/diagnostics.py +++ b/homeassistant/components/ambient_station/diagnostics.py @@ -5,7 +5,7 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LOCATION +from homeassistant.const import CONF_API_KEY, CONF_LOCATION, CONF_UNIQUE_ID from homeassistant.core import HomeAssistant from . import AmbientStation @@ -16,6 +16,7 @@ CONF_APP_KEY_CAMEL = "appKey" CONF_DEVICE_ID_CAMEL = "deviceId" CONF_MAC_ADDRESS = "mac_address" CONF_MAC_ADDRESS_CAMEL = "macAddress" +CONF_TITLE = "title" CONF_TZ = "tz" TO_REDACT = { @@ -28,6 +29,9 @@ TO_REDACT = { CONF_MAC_ADDRESS, CONF_MAC_ADDRESS_CAMEL, CONF_TZ, + # Config entry title and unique ID may contain sensitive data: + CONF_TITLE, + CONF_UNIQUE_ID, } @@ -38,9 +42,6 @@ async def async_get_config_entry_diagnostics( ambient: AmbientStation = hass.data[DOMAIN][entry.entry_id] return { - "entry": { - "title": entry.title, - "data": async_redact_data(entry.data, TO_REDACT), - }, + "entry": async_redact_data(entry.as_dict(), TO_REDACT), "stations": async_redact_data(ambient.stations, TO_REDACT), } diff --git a/tests/components/ambient_station/test_diagnostics.py b/tests/components/ambient_station/test_diagnostics.py index 63d5fcff7a1..e6285afa17a 100644 --- a/tests/components/ambient_station/test_diagnostics.py +++ b/tests/components/ambient_station/test_diagnostics.py @@ -13,45 +13,54 @@ async def test_entry_diagnostics( ambient.stations = station_data assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { + "entry_id": config_entry.entry_id, + "version": 2, + "domain": "ambient_station", + "title": REDACTED, "data": {"api_key": REDACTED, "app_key": REDACTED}, - "title": "Mock Title", + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": REDACTED, + "disabled_by": None, }, "stations": { "devices": [ { - "apiKey": REDACTED, - "info": {"location": REDACTED, "name": "Side Yard"}, - "lastData": { - "baromabsin": 25.016, - "baromrelin": 29.953, - "batt_co2": 1, - "dailyrainin": 0, - "date": "2022-01-19T22:38:00.000Z", - "dateutc": 1642631880000, - "deviceId": REDACTED, - "dewPoint": 17.75, - "dewPointin": 37, - "eventrainin": 0, - "feelsLike": 21, - "feelsLikein": 69.1, - "hourlyrainin": 0, - "humidity": 87, - "humidityin": 29, - "lastRain": "2022-01-07T19:45:00.000Z", - "maxdailygust": 9.2, - "monthlyrainin": 0.409, - "solarradiation": 11.62, - "tempf": 21, - "tempinf": 70.9, - "totalrainin": 35.398, - "tz": REDACTED, - "uv": 0, - "weeklyrainin": 0, - "winddir": 25, - "windgustmph": 1.1, - "windspeedmph": 0.2, - }, "macAddress": REDACTED, + "lastData": { + "dateutc": 1642631880000, + "tempinf": 70.9, + "humidityin": 29, + "baromrelin": 29.953, + "baromabsin": 25.016, + "tempf": 21, + "humidity": 87, + "winddir": 25, + "windspeedmph": 0.2, + "windgustmph": 1.1, + "maxdailygust": 9.2, + "hourlyrainin": 0, + "eventrainin": 0, + "dailyrainin": 0, + "weeklyrainin": 0, + "monthlyrainin": 0.409, + "totalrainin": 35.398, + "solarradiation": 11.62, + "uv": 0, + "batt_co2": 1, + "feelsLike": 21, + "dewPoint": 17.75, + "feelsLikein": 69.1, + "dewPointin": 37, + "lastRain": "2022-01-07T19:45:00.000Z", + "deviceId": REDACTED, + "tz": REDACTED, + "date": "2022-01-19T22:38:00.000Z", + }, + "info": {"name": "Side Yard", "location": REDACTED}, + "apiKey": REDACTED, } ], "method": "subscribe", From b41cd57c337d7bad420b5baee1b5dafe596759d2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 11 Oct 2022 09:26:57 -0600 Subject: [PATCH 359/985] Use `entry.as_dict()` in AirVisual diagnostics (#80109) --- .../components/airvisual/diagnostics.py | 18 ++++++++++----- .../components/airvisual/test_diagnostics.py | 23 +++++++++++-------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/airvisual/diagnostics.py b/homeassistant/components/airvisual/diagnostics.py index 94cf5f1899d..c273dbe7a55 100644 --- a/homeassistant/components/airvisual/diagnostics.py +++ b/homeassistant/components/airvisual/diagnostics.py @@ -5,13 +5,20 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_STATE +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_STATE, + CONF_UNIQUE_ID, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import CONF_CITY, CONF_COUNTRY, DOMAIN CONF_COORDINATES = "coordinates" +CONF_TITLE = "title" TO_REDACT = { CONF_API_KEY, @@ -21,6 +28,9 @@ TO_REDACT = { CONF_LATITUDE, CONF_LONGITUDE, CONF_STATE, + # Config entry title and unique ID may contain sensitive data: + CONF_TITLE, + CONF_UNIQUE_ID, } @@ -31,10 +41,6 @@ async def async_get_config_entry_diagnostics( coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] return { - "entry": { - "title": entry.title, - "data": async_redact_data(entry.data, TO_REDACT), - "options": async_redact_data(entry.options, TO_REDACT), - }, + "entry": async_redact_data(entry.as_dict(), TO_REDACT), "data": async_redact_data(coordinator.data["data"], TO_REDACT), } diff --git a/tests/components/airvisual/test_diagnostics.py b/tests/components/airvisual/test_diagnostics.py index 5b68644bb7e..72ed5298f96 100644 --- a/tests/components/airvisual/test_diagnostics.py +++ b/tests/components/airvisual/test_diagnostics.py @@ -8,20 +8,28 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_airvisua """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { - "title": "Mock Title", + "entry_id": config_entry.entry_id, + "version": 2, + "domain": "airvisual", + "title": REDACTED, "data": { - "api_key": REDACTED, "integration_type": "Geographical Location by Latitude/Longitude", + "api_key": REDACTED, "latitude": REDACTED, "longitude": REDACTED, }, - "options": { - "show_on_map": True, - }, + "options": {"show_on_map": True}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": REDACTED, + "disabled_by": None, }, "data": { "city": REDACTED, + "state": REDACTED, "country": REDACTED, + "location": {"type": "Point", "coordinates": REDACTED}, "current": { "weather": { "ts": "2021-09-03T21:00:00.000Z", @@ -40,10 +48,5 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_airvisua "maincn": "p2", }, }, - "location": { - "coordinates": REDACTED, - "type": "Point", - }, - "state": REDACTED, }, } From 030205df8fae4beae3db90a15f8fff1f7deea279 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 11 Oct 2022 17:37:21 +0200 Subject: [PATCH 360/985] Filter out non official zwave_js add-on discovery (#80110) * Filter out non official zwave_js add-on discovery * Add test --- .../components/zwave_js/config_flow.py | 4 ++++ .../components/zwave_js/strings.json | 3 ++- tests/components/zwave_js/test_config_flow.py | 22 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index c114662888f..378580160d1 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -29,6 +29,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import disconnect_client from .addon import AddonError, AddonInfo, AddonManager, AddonState, get_addon_manager from .const import ( + ADDON_SLUG, CONF_ADDON_DEVICE, CONF_ADDON_EMULATE_HARDWARE, CONF_ADDON_LOG_LEVEL, @@ -492,6 +493,9 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): if self._async_in_progress(): return self.async_abort(reason="already_in_progress") + if discovery_info.slug != ADDON_SLUG: + return self.async_abort(reason="not_zwave_js_addon") + self.ws_address = ( f"ws://{discovery_info.config['host']}:{discovery_info.config['port']}" ) diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 19587cf0c0f..7446edb0c5d 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -58,7 +58,8 @@ "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "discovery_requires_supervisor": "Discovery requires the supervisor.", - "not_zwave_device": "Discovered device is not a Z-Wave device." + "not_zwave_device": "Discovered device is not a Z-Wave device.", + "not_zwave_js_addon": "Discovered add-on is not the official Z-Wave JS add-on." }, "progress": { "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.", diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index d297b183dd1..5e58ef25339 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -505,6 +505,28 @@ async def test_abort_hassio_discovery_with_existing_flow( assert result2["reason"] == "already_in_progress" +async def test_abort_hassio_discovery_for_other_addon( + hass, supervisor, addon_installed, addon_options +): + """Test hassio discovery flow is aborted for a non official add-on discovery.""" + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=HassioServiceInfo( + config={ + "addon": "Other Z-Wave JS", + "host": "host1", + "port": 3001, + }, + name="Other Z-Wave JS", + slug="other_addon", + ), + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "not_zwave_js_addon" + + @pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) async def test_usb_discovery( hass, From 5743e9f83d0d7af8c7c6605de1a810d87ba0dcda Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 11 Oct 2022 17:51:42 +0200 Subject: [PATCH 361/985] Use REVOLUTIONS_PER_MINUTE constant in isy994 (#79989) --- homeassistant/components/isy994/const.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 21cc23b01ca..0a0149c376e 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -41,6 +41,7 @@ from homeassistant.const import ( PRESSURE_HPA, PRESSURE_INHG, PRESSURE_MBAR, + REVOLUTIONS_PER_MINUTE, SERVICE_LOCK, SERVICE_UNLOCK, SOUND_PRESSURE_DB, @@ -396,7 +397,7 @@ UOM_FRIENDLY_NAME = { "86": "kΩ", "87": f"{VOLUME_CUBIC_METERS}/{VOLUME_CUBIC_METERS}", "88": "Water activity", - "89": "RPM", + "89": REVOLUTIONS_PER_MINUTE, "90": FREQUENCY_HERTZ, "91": DEGREE, "92": f"{DEGREE} South", From 0f002e704466881b68fe76ddf233746f5313ac2f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 11 Oct 2022 10:12:17 -0600 Subject: [PATCH 362/985] Use `entry.as_dict()` in Guardian diagnostics (#80112) --- homeassistant/components/guardian/diagnostics.py | 10 ++++++---- tests/components/guardian/test_diagnostics.py | 13 +++++++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/guardian/diagnostics.py b/homeassistant/components/guardian/diagnostics.py index d53dcb68fa8..b0317167f79 100644 --- a/homeassistant/components/guardian/diagnostics.py +++ b/homeassistant/components/guardian/diagnostics.py @@ -5,6 +5,7 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_UNIQUE_ID from homeassistant.core import HomeAssistant from . import GuardianData @@ -13,11 +14,15 @@ from .const import CONF_UID, DOMAIN CONF_BSSID = "bssid" CONF_PAIRED_UIDS = "paired_uids" CONF_SSID = "ssid" +CONF_TITLE = "title" TO_REDACT = { CONF_BSSID, CONF_PAIRED_UIDS, CONF_SSID, + # Config entry title and unique ID may contain sensitive data: + CONF_TITLE, + CONF_UNIQUE_ID, CONF_UID, } @@ -29,10 +34,7 @@ async def async_get_config_entry_diagnostics( data: GuardianData = hass.data[DOMAIN][entry.entry_id] return { - "entry": { - "title": entry.title, - "data": async_redact_data(entry.data, TO_REDACT), - }, + "entry": async_redact_data(entry.as_dict(), TO_REDACT), "data": { "valve_controller": { api_category: async_redact_data(coordinator.data, TO_REDACT) diff --git a/tests/components/guardian/test_diagnostics.py b/tests/components/guardian/test_diagnostics.py index 2269d09b1eb..ca6a8c77039 100644 --- a/tests/components/guardian/test_diagnostics.py +++ b/tests/components/guardian/test_diagnostics.py @@ -14,12 +14,21 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_guardian assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { - "title": "Mock Title", + "entry_id": config_entry.entry_id, + "version": 1, + "domain": "guardian", + "title": REDACTED, "data": { + "uid": REDACTED, "ip_address": "192.168.1.100", "port": 7777, - "uid": REDACTED, }, + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": REDACTED, + "disabled_by": None, }, "data": { "valve_controller": { From c05390e09b5e14310c89039e0a2412f6f72b94b2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 11 Oct 2022 10:13:58 -0600 Subject: [PATCH 363/985] Use `entry.as_dict()` in IQVIA diagnostics (#80113) --- homeassistant/components/iqvia/diagnostics.py | 39 ++++++++--- tests/components/iqvia/test_diagnostics.py | 69 +++++++++++-------- 2 files changed, 69 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/iqvia/diagnostics.py b/homeassistant/components/iqvia/diagnostics.py index ee722005874..664467b0702 100644 --- a/homeassistant/components/iqvia/diagnostics.py +++ b/homeassistant/components/iqvia/diagnostics.py @@ -3,11 +3,32 @@ from __future__ import annotations from typing import Any +from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_UNIQUE_ID from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN +from .const import CONF_ZIP_CODE, DOMAIN + +CONF_CITY = "City" +CONF_DISPLAY_LOCATION = "DisplayLocation" +CONF_MARKET = "Market" +CONF_TITLE = "title" +CONF_ZIP_CAP = "ZIP" +CONF_STATE_CAP = "State" + +TO_REDACT = { + CONF_CITY, + CONF_DISPLAY_LOCATION, + CONF_MARKET, + CONF_STATE_CAP, + # Config entry title and unique ID may contain sensitive data: + CONF_TITLE, + CONF_UNIQUE_ID, + CONF_ZIP_CAP, + CONF_ZIP_CODE, +} async def async_get_config_entry_diagnostics( @@ -17,12 +38,12 @@ async def async_get_config_entry_diagnostics( coordinators: dict[str, DataUpdateCoordinator] = hass.data[DOMAIN][entry.entry_id] return { - "entry": { - "title": entry.title, - "data": dict(entry.data), - }, - "data": { - data_type: coordinator.data - for data_type, coordinator in coordinators.items() - }, + "entry": async_redact_data(entry.as_dict(), TO_REDACT), + "data": async_redact_data( + { + data_type: coordinator.data + for data_type, coordinator in coordinators.items() + }, + TO_REDACT, + ), } diff --git a/tests/components/iqvia/test_diagnostics.py b/tests/components/iqvia/test_diagnostics.py index 4c5f4bcac75..ee3e6817fc1 100644 --- a/tests/components/iqvia/test_diagnostics.py +++ b/tests/components/iqvia/test_diagnostics.py @@ -1,4 +1,6 @@ """Test IQVIA diagnostics.""" +from homeassistant.components.diagnostics import REDACTED + from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -6,19 +8,26 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_iqvia): """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { - "title": "Mock Title", - "data": { - "zip_code": "12345", - }, + "entry_id": config_entry.entry_id, + "version": 1, + "domain": "iqvia", + "title": REDACTED, + "data": {"zip_code": REDACTED}, + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": REDACTED, + "disabled_by": None, }, "data": { "allergy_average_forecasted": { "Type": "pollen", "ForecastDate": "2018-06-12T00:00:00-04:00", "Location": { - "ZIP": "12345", - "City": "SCHENECTADY", - "State": "NY", + "ZIP": REDACTED, + "City": REDACTED, + "State": REDACTED, "periods": [ {"Period": "2018-06-12T13:47:12.897", "Index": 6.6}, {"Period": "2018-06-13T13:47:12.897", "Index": 6.3}, @@ -26,16 +35,16 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_iqvia): {"Period": "2018-06-15T13:47:12.897", "Index": 7.6}, {"Period": "2018-06-16T13:47:12.897", "Index": 7.3}, ], - "DisplayLocation": "Schenectady, NY", + "DisplayLocation": REDACTED, }, }, "allergy_index": { "Type": "pollen", "ForecastDate": "2018-06-12T00:00:00-04:00", "Location": { - "ZIP": "12345", - "City": "SCHENECTADY", - "State": "NY", + "ZIP": REDACTED, + "City": REDACTED, + "State": REDACTED, "periods": [ { "Triggers": [ @@ -113,12 +122,12 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_iqvia): "Index": 6.3, }, ], - "DisplayLocation": "Schenectady, NY", + "DisplayLocation": REDACTED, }, }, "allergy_outlook": { - "Market": "SCHENECTADY, CO", - "ZIP": "12345", + "Market": REDACTED, + "ZIP": REDACTED, "TrendID": 4, "Trend": "subsiding", "Outlook": "The amount of pollen in the air for Wednesday...", @@ -128,9 +137,9 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_iqvia): "Type": "asthma", "ForecastDate": "2018-10-28T00:00:00-04:00", "Location": { - "ZIP": "12345", - "City": "SCHENECTADY", - "State": "NY", + "ZIP": REDACTED, + "City": REDACTED, + "State": REDACTED, "periods": [ { "Period": "2018-10-28T05:45:01.45", @@ -154,16 +163,16 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_iqvia): "Idx": "5.5", }, ], - "DisplayLocation": "Schenectady, NY", + "DisplayLocation": REDACTED, }, }, "asthma_index": { "Type": "asthma", "ForecastDate": "2018-10-29T00:00:00-04:00", "Location": { - "ZIP": "12345", - "City": "SCHENECTADY", - "State": "NY", + "ZIP": REDACTED, + "City": REDACTED, + "State": REDACTED, "periods": [ { "Triggers": [ @@ -225,32 +234,32 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_iqvia): "Idx": "4.6", }, ], - "DisplayLocation": "Schenectady, NY", + "DisplayLocation": REDACTED, }, }, "disease_average_forecasted": { "Type": "cold", "ForecastDate": "2018-06-12T00:00:00-04:00", "Location": { - "ZIP": "12345", - "City": "SCHENECTADY", - "State": "NY", + "ZIP": REDACTED, + "City": REDACTED, + "State": REDACTED, "periods": [ {"Period": "2018-06-12T05:13:51.817", "Index": 2.4}, {"Period": "2018-06-13T05:13:51.817", "Index": 2.5}, {"Period": "2018-06-14T05:13:51.817", "Index": 2.5}, {"Period": "2018-06-15T05:13:51.817", "Index": 2.5}, ], - "DisplayLocation": "Schenectady, NY", + "DisplayLocation": REDACTED, }, }, "disease_index": { "ForecastDate": "2019-04-07T00:00:00-04:00", "Location": { - "City": "SCHENECTADY", - "DisplayLocation": "Schenectady, NY", - "State": "NY", - "ZIP": "12345", + "City": REDACTED, + "DisplayLocation": REDACTED, + "State": REDACTED, + "ZIP": REDACTED, "periods": [ { "Idx": "6.8", From 020b7e97629e150d175446862b94ff78c2b0d54c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 11 Oct 2022 10:14:35 -0600 Subject: [PATCH 364/985] Use `entry.as_dict()` in Notion diagnostics (#80114) --- .../components/notion/diagnostics.py | 15 +- tests/components/notion/test_diagnostics.py | 207 ++++++++++-------- 2 files changed, 124 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/notion/diagnostics.py b/homeassistant/components/notion/diagnostics.py index 9e1d6d3b7a4..9b0a070897c 100644 --- a/homeassistant/components/notion/diagnostics.py +++ b/homeassistant/components/notion/diagnostics.py @@ -5,18 +5,26 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN CONF_DEVICE_KEY = "device_key" +CONF_HARDWARE_ID = "hardware_id" +CONF_LAST_BRIDGE_HARDWARE_ID = "last_bridge_hardware_id" +CONF_TITLE = "title" TO_REDACT = { CONF_DEVICE_KEY, CONF_EMAIL, + CONF_HARDWARE_ID, + CONF_LAST_BRIDGE_HARDWARE_ID, CONF_PASSWORD, + # Config entry title and unique ID may contain sensitive data: + CONF_TITLE, + CONF_UNIQUE_ID, CONF_USERNAME, } @@ -27,4 +35,7 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - return async_redact_data(coordinator.data, TO_REDACT) + return { + "entry": async_redact_data(entry.as_dict(), TO_REDACT), + "data": async_redact_data(coordinator.data, TO_REDACT), + } diff --git a/tests/components/notion/test_diagnostics.py b/tests/components/notion/test_diagnostics.py index 39d2777462f..d8b5abcc781 100644 --- a/tests/components/notion/test_diagnostics.py +++ b/tests/components/notion/test_diagnostics.py @@ -7,105 +7,120 @@ from tests.components.diagnostics import get_diagnostics_for_config_entry async def test_entry_diagnostics(hass, config_entry, hass_client, setup_notion): """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "bridges": { - "12345": { - "id": 12345, - "name": None, - "mode": "home", - "hardware_id": "0x1234567890abcdef", - "hardware_revision": 4, - "firmware_version": { - "wifi": "0.121.0", - "wifi_app": "3.3.0", - "silabs": "1.0.1", - }, - "missing_at": None, - "created_at": "2019-04-30T01:43:50.497Z", - "updated_at": "2019-04-30T01:44:43.749Z", - "system_id": 12345, - "firmware": { - "wifi": "0.121.0", - "wifi_app": "3.3.0", - "silabs": "1.0.1", - }, - "links": {"system": 12345}, - } + "entry": { + "entry_id": config_entry.entry_id, + "version": 1, + "domain": "notion", + "title": REDACTED, + "data": {"username": REDACTED, "password": REDACTED}, + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": REDACTED, + "disabled_by": None, }, - "sensors": { - "123456": { - "id": 123456, - "uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "user": {"id": 12345, "email": REDACTED}, - "bridge": {"id": 12345, "hardware_id": "0x1234567890abcdef"}, - "last_bridge_hardware_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "name": "Bathroom Sensor", - "location_id": 123456, - "system_id": 12345, - "hardware_id": "0x1234567890abcdef", - "firmware_version": "1.1.2", - "hardware_revision": 5, - "device_key": REDACTED, - "encryption_key": True, - "installed_at": "2019-04-30T01:57:34.443Z", - "calibrated_at": "2019-04-30T01:57:35.651Z", - "last_reported_at": "2019-04-30T02:20:04.821Z", - "missing_at": None, - "updated_at": "2019-04-30T01:57:36.129Z", - "created_at": "2019-04-30T01:56:45.932Z", - "signal_strength": 5, - "links": {"location": 123456}, - "lqi": 0, - "rssi": -46, - "surface_type": None, + "data": { + "bridges": { + "12345": { + "id": 12345, + "name": None, + "mode": "home", + "hardware_id": REDACTED, + "hardware_revision": 4, + "firmware_version": { + "wifi": "0.121.0", + "wifi_app": "3.3.0", + "silabs": "1.0.1", + }, + "missing_at": None, + "created_at": "2019-04-30T01:43:50.497Z", + "updated_at": "2019-04-30T01:44:43.749Z", + "system_id": 12345, + "firmware": { + "wifi": "0.121.0", + "wifi_app": "3.3.0", + "silabs": "1.0.1", + }, + "links": {"system": 12345}, + } }, - "132462": { - "id": 132462, - "uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "user": {"id": 12345, "email": REDACTED}, - "bridge": {"id": 12345, "hardware_id": "0x1234567890abcdef"}, - "last_bridge_hardware_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "name": "Living Room Sensor", - "location_id": 123456, - "system_id": 12345, - "hardware_id": "0x1234567890abcdef", - "firmware_version": "1.1.2", - "hardware_revision": 5, - "device_key": REDACTED, - "encryption_key": True, - "installed_at": "2019-04-30T01:45:56.169Z", - "calibrated_at": "2019-04-30T01:46:06.256Z", - "last_reported_at": "2019-04-30T02:20:04.829Z", - "missing_at": None, - "updated_at": "2019-04-30T01:46:07.717Z", - "created_at": "2019-04-30T01:45:14.148Z", - "signal_strength": 5, - "links": {"location": 123456}, - "lqi": 0, - "rssi": -30, - "surface_type": None, + "sensors": { + "123456": { + "id": 123456, + "uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "user": {"id": 12345, "email": REDACTED}, + "bridge": {"id": 12345, "hardware_id": REDACTED}, + "last_bridge_hardware_id": REDACTED, + "name": "Bathroom Sensor", + "location_id": 123456, + "system_id": 12345, + "hardware_id": REDACTED, + "firmware_version": "1.1.2", + "hardware_revision": 5, + "device_key": REDACTED, + "encryption_key": True, + "installed_at": "2019-04-30T01:57:34.443Z", + "calibrated_at": "2019-04-30T01:57:35.651Z", + "last_reported_at": "2019-04-30T02:20:04.821Z", + "missing_at": None, + "updated_at": "2019-04-30T01:57:36.129Z", + "created_at": "2019-04-30T01:56:45.932Z", + "signal_strength": 5, + "links": {"location": 123456}, + "lqi": 0, + "rssi": -46, + "surface_type": None, + }, + "132462": { + "id": 132462, + "uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "user": {"id": 12345, "email": REDACTED}, + "bridge": {"id": 12345, "hardware_id": REDACTED}, + "last_bridge_hardware_id": REDACTED, + "name": "Living Room Sensor", + "location_id": 123456, + "system_id": 12345, + "hardware_id": REDACTED, + "firmware_version": "1.1.2", + "hardware_revision": 5, + "device_key": REDACTED, + "encryption_key": True, + "installed_at": "2019-04-30T01:45:56.169Z", + "calibrated_at": "2019-04-30T01:46:06.256Z", + "last_reported_at": "2019-04-30T02:20:04.829Z", + "missing_at": None, + "updated_at": "2019-04-30T01:46:07.717Z", + "created_at": "2019-04-30T01:45:14.148Z", + "signal_strength": 5, + "links": {"location": 123456}, + "lqi": 0, + "rssi": -30, + "surface_type": None, + }, }, - }, - "tasks": { - "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx": { - "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "task_type": "low_battery", - "sensor_data": [], - "status": { - "insights": { - "primary": { - "from_state": None, - "to_state": "high", - "data_received_at": "2020-11-17T18:40:27.024Z", - "origin": {}, + "tasks": { + "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx": { + "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "task_type": "low_battery", + "sensor_data": [], + "status": { + "insights": { + "primary": { + "from_state": None, + "to_state": "high", + "data_received_at": "2020-11-17T18:40:27.024Z", + "origin": {}, + } } - } - }, - "created_at": "2020-11-17T18:40:27.024Z", - "updated_at": "2020-11-17T18:40:27.033Z", - "sensor_id": 525993, - "model_version": "4.1", - "configuration": {}, - "links": {"sensor": 525993}, - } + }, + "created_at": "2020-11-17T18:40:27.024Z", + "updated_at": "2020-11-17T18:40:27.033Z", + "sensor_id": 525993, + "model_version": "4.1", + "configuration": {}, + "links": {"sensor": 525993}, + } + }, }, } From 4ea46bf8418f329074bedac54635be2bc0076bbe Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 11 Oct 2022 10:17:03 -0600 Subject: [PATCH 365/985] Use `entry.as_dict()` in OpenUV diagnostics (#80115) --- .../components/openuv/diagnostics.py | 16 ++++++++---- tests/components/openuv/test_diagnostics.py | 26 ++++++++++++------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/openuv/diagnostics.py b/homeassistant/components/openuv/diagnostics.py index 02b56ce0e90..30443dd90fc 100644 --- a/homeassistant/components/openuv/diagnostics.py +++ b/homeassistant/components/openuv/diagnostics.py @@ -5,18 +5,27 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_UNIQUE_ID, +) from homeassistant.core import HomeAssistant from . import OpenUV from .const import DOMAIN CONF_COORDINATES = "coordinates" +CONF_TITLE = "title" TO_REDACT = { CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, + # Config entry title and unique ID may contain sensitive data: + CONF_TITLE, + CONF_UNIQUE_ID, } @@ -27,9 +36,6 @@ async def async_get_config_entry_diagnostics( openuv: OpenUV = hass.data[DOMAIN][entry.entry_id] return { - "entry": { - "data": async_redact_data(entry.data, TO_REDACT), - "options": async_redact_data(entry.options, TO_REDACT), - }, + "entry": async_redact_data(entry.as_dict(), TO_REDACT), "data": async_redact_data(openuv.data, TO_REDACT), } diff --git a/tests/components/openuv/test_diagnostics.py b/tests/components/openuv/test_diagnostics.py index 1196045300b..b64c48d153b 100644 --- a/tests/components/openuv/test_diagnostics.py +++ b/tests/components/openuv/test_diagnostics.py @@ -12,18 +12,30 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_openuv): ) assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { + "entry_id": config_entry.entry_id, + "version": 2, + "domain": "openuv", + "title": REDACTED, "data": { "api_key": REDACTED, "elevation": 0, "latitude": REDACTED, "longitude": REDACTED, }, - "options": { - "from_window": 3.5, - "to_window": 3.5, - }, + "options": {"from_window": 3.5, "to_window": 3.5}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": REDACTED, + "disabled_by": None, }, "data": { + "protection_window": { + "from_time": "2018-07-30T15:17:49.750Z", + "from_uv": 3.2509, + "to_time": "2018-07-30T22:47:49.750Z", + "to_uv": 3.6483, + }, "uv": { "uv": 8.2342, "uv_time": "2018-07-30T20:53:06.302Z", @@ -62,11 +74,5 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_openuv): }, }, }, - "protection_window": { - "from_time": "2018-07-30T15:17:49.750Z", - "from_uv": 3.2509, - "to_time": "2018-07-30T22:47:49.750Z", - "to_uv": 3.6483, - }, }, } From 2f6e55b3bb42c647367b7d8f1c83b5be5cff1664 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 11 Oct 2022 10:19:13 -0600 Subject: [PATCH 366/985] Fix uncaught error in OpenUV diagnostics test (#80116) --- tests/components/openuv/test_diagnostics.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/components/openuv/test_diagnostics.py b/tests/components/openuv/test_diagnostics.py index b64c48d153b..0fb88d9cda4 100644 --- a/tests/components/openuv/test_diagnostics.py +++ b/tests/components/openuv/test_diagnostics.py @@ -1,14 +1,19 @@ """Test OpenUV diagnostics.""" from homeassistant.components.diagnostics import REDACTED -from homeassistant.components.openuv import CONF_ENTRY_ID +from homeassistant.const import CONF_ENTITY_ID +from homeassistant.setup import async_setup_component from tests.components.diagnostics import get_diagnostics_for_config_entry async def test_entry_diagnostics(hass, config_entry, hass_client, setup_openuv): """Test config entry diagnostics.""" + await async_setup_component(hass, "homeassistant", {}) await hass.services.async_call( - "openuv", "update_data", service_data={CONF_ENTRY_ID: "test_entry_id"} + "homeassistant", + "update_entity", + {CONF_ENTITY_ID: ["sensor.current_uv_index"]}, + blocking=True, ) assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { From 8bc9aa9ea4e2be57c00a86de74ed21b8e2f0967d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Oct 2022 19:49:58 +0200 Subject: [PATCH 367/985] Update mutagen to 1.46.0 (#80004) * Update mutagen to 1.46.0 * Ignore untyped call --- homeassistant/components/tts/__init__.py | 6 +++--- homeassistant/components/tts/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 757c33e2653..0e0c41e5e30 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -618,9 +618,9 @@ class SpeechManager: if not tts_file.tags: tts_file.add_tags() if isinstance(tts_file.tags, ID3): - 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["artist"] = ID3Text(encoding=3, text=artist) # type: ignore[no-untyped-call] + tts_file["album"] = ID3Text(encoding=3, text=album) # type: ignore[no-untyped-call] + tts_file["title"] = ID3Text(encoding=3, text=message) # type: ignore[no-untyped-call] else: tts_file["artist"] = artist tts_file["album"] = album diff --git a/homeassistant/components/tts/manifest.json b/homeassistant/components/tts/manifest.json index f3b16cafac5..2957369ff6a 100644 --- a/homeassistant/components/tts/manifest.json +++ b/homeassistant/components/tts/manifest.json @@ -2,7 +2,7 @@ "domain": "tts", "name": "Text-to-Speech (TTS)", "documentation": "https://www.home-assistant.io/integrations/tts", - "requirements": ["mutagen==1.45.1"], + "requirements": ["mutagen==1.46.0"], "dependencies": ["http"], "after_dependencies": ["media_player"], "codeowners": ["@pvizeli"], diff --git a/requirements_all.txt b/requirements_all.txt index 47769d5425c..e07b76c077f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1105,7 +1105,7 @@ motioneye-client==0.3.12 mullvad-api==1.0.0 # homeassistant.components.tts -mutagen==1.45.1 +mutagen==1.46.0 # homeassistant.components.mutesync mutesync==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91dd1495317..e31e0e2e99d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -807,7 +807,7 @@ motioneye-client==0.3.12 mullvad-api==1.0.0 # homeassistant.components.tts -mutagen==1.45.1 +mutagen==1.46.0 # homeassistant.components.mutesync mutesync==0.0.1 From 687987f05b9a456f2ca0799c1b4c256b462a7252 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 11 Oct 2022 12:15:07 -0600 Subject: [PATCH 368/985] Use `entry.as_dict()` in RainMachine diagnostics (#80118) * Use `entry.as_dict()` in RainMachine diagnostics * One call --- .../components/rainmachine/diagnostics.py | 25 ++++++++--------- .../rainmachine/test_diagnostics.py | 28 ++++++++++++------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/rainmachine/diagnostics.py b/homeassistant/components/rainmachine/diagnostics.py index 47ded7990c6..e4835d514e6 100644 --- a/homeassistant/components/rainmachine/diagnostics.py +++ b/homeassistant/components/rainmachine/diagnostics.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_PASSWORD, + CONF_UNIQUE_ID, ) from homeassistant.core import HomeAssistant @@ -32,6 +33,8 @@ TO_REDACT = { CONF_STATION_NAME, CONF_STATION_SOURCE, CONF_TIMEZONE, + # Config entry unique ID may contain sensitive data: + CONF_UNIQUE_ID, } @@ -47,20 +50,16 @@ async def async_get_config_entry_diagnostics( LOGGER.warning("Unable to download controller-specific diagnostics") controller_diagnostics = None - return { - "entry": { - "title": entry.title, - "data": async_redact_data(entry.data, TO_REDACT), - "options": dict(entry.options), - }, - "data": { - "coordinator": async_redact_data( - { + return async_redact_data( + { + "entry": entry.as_dict(), + "data": { + "coordinator": { api_category: controller.data for api_category, controller in data.coordinators.items() }, - TO_REDACT, - ), - "controller_diagnostics": controller_diagnostics, + "controller_diagnostics": controller_diagnostics, + }, }, - } + TO_REDACT, + ) diff --git a/tests/components/rainmachine/test_diagnostics.py b/tests/components/rainmachine/test_diagnostics.py index 7cf8406d2ae..a3c03c956a4 100644 --- a/tests/components/rainmachine/test_diagnostics.py +++ b/tests/components/rainmachine/test_diagnostics.py @@ -10,6 +10,9 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_rainmach """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { + "entry_id": config_entry.entry_id, + "version": 2, + "domain": "rainmachine", "title": "Mock Title", "data": { "ip_address": "192.168.1.100", @@ -18,14 +21,15 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_rainmach "ssl": True, }, "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": REDACTED, + "disabled_by": None, }, "data": { "coordinator": { - "api.versions": { - "apiVer": "4.6.1", - "hwVer": "3", - "swVer": "4.0.1144", - }, + "api.versions": {"apiVer": "4.6.1", "hwVer": "3", "swVer": "4.0.1144"}, "machine.firmware_update_status": { "lastUpdateCheckTimestamp": 1657825288, "packageDetails": [], @@ -628,6 +632,9 @@ async def test_entry_diagnostics_failed_controller_diagnostics( controller.diagnostics.current.side_effect = RainMachineError assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { + "entry_id": config_entry.entry_id, + "version": 2, + "domain": "rainmachine", "title": "Mock Title", "data": { "ip_address": "192.168.1.100", @@ -636,14 +643,15 @@ async def test_entry_diagnostics_failed_controller_diagnostics( "ssl": True, }, "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": REDACTED, + "disabled_by": None, }, "data": { "coordinator": { - "api.versions": { - "apiVer": "4.6.1", - "hwVer": "3", - "swVer": "4.0.1144", - }, + "api.versions": {"apiVer": "4.6.1", "hwVer": "3", "swVer": "4.0.1144"}, "machine.firmware_update_status": { "lastUpdateCheckTimestamp": 1657825288, "packageDetails": [], From d4465e4e69eab00536f4776b5367dfcea9f9c87a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 11 Oct 2022 12:15:21 -0600 Subject: [PATCH 369/985] Use `entry.as_dict()` in WattTime diagnostics (#80122) --- homeassistant/components/watttime/diagnostics.py | 15 ++++++++++----- tests/components/watttime/test_diagnostics.py | 13 +++++++++++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/watttime/diagnostics.py b/homeassistant/components/watttime/diagnostics.py index 080c7c37b07..2808e8e3c35 100644 --- a/homeassistant/components/watttime/diagnostics.py +++ b/homeassistant/components/watttime/diagnostics.py @@ -9,17 +9,25 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_PASSWORD, + CONF_UNIQUE_ID, CONF_USERNAME, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN +from .const import CONF_BALANCING_AUTHORITY, CONF_BALANCING_AUTHORITY_ABBREV, DOMAIN + +CONF_TITLE = "title" TO_REDACT = { + CONF_BALANCING_AUTHORITY, + CONF_BALANCING_AUTHORITY_ABBREV, CONF_LATITUDE, CONF_LONGITUDE, CONF_PASSWORD, + # Config entry title and unique ID may contain sensitive data: + CONF_TITLE, + CONF_UNIQUE_ID, CONF_USERNAME, } @@ -32,10 +40,7 @@ async def async_get_config_entry_diagnostics( return async_redact_data( { - "entry": { - "data": dict(entry.data), - "options": dict(entry.options), - }, + "entry": entry.as_dict(), "data": coordinator.data, }, TO_REDACT, diff --git a/tests/components/watttime/test_diagnostics.py b/tests/components/watttime/test_diagnostics.py index 0d8d87203bb..e5aaf65e920 100644 --- a/tests/components/watttime/test_diagnostics.py +++ b/tests/components/watttime/test_diagnostics.py @@ -8,15 +8,24 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_watttime """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { + "entry_id": config_entry.entry_id, + "version": 1, + "domain": "watttime", + "title": REDACTED, "data": { "username": REDACTED, "password": REDACTED, "latitude": REDACTED, "longitude": REDACTED, - "balancing_authority": "PJM New Jersey", - "balancing_authority_abbreviation": "PJM_NJ", + "balancing_authority": REDACTED, + "balancing_authority_abbreviation": REDACTED, }, "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": REDACTED, + "disabled_by": None, }, "data": { "freq": "300", From 1262c0e221f9fe9a8b4dd07367b9b6f00ea69b8f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 11 Oct 2022 12:15:32 -0600 Subject: [PATCH 370/985] Use `entry.as_dict()` in SimpliSafe diagnostics (#80121) --- .../components/simplisafe/diagnostics.py | 20 +++++++++++++++---- .../components/simplisafe/test_diagnostics.py | 14 ++++++++++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/simplisafe/diagnostics.py b/homeassistant/components/simplisafe/diagnostics.py index cd6e4ca52be..cb983f74202 100644 --- a/homeassistant/components/simplisafe/diagnostics.py +++ b/homeassistant/components/simplisafe/diagnostics.py @@ -5,7 +5,14 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ADDRESS, CONF_CODE, CONF_LOCATION +from homeassistant.const import ( + CONF_ADDRESS, + CONF_CODE, + CONF_LOCATION, + CONF_TOKEN, + CONF_UNIQUE_ID, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from . import SimpliSafe @@ -18,6 +25,7 @@ CONF_PAYMENT_PROFILE_ID = "paymentProfileId" CONF_SERIAL = "serial" CONF_SID = "sid" CONF_SYSTEM_ID = "system_id" +CONF_TITLE = "title" CONF_UID = "uid" CONF_WIFI_SSID = "wifi_ssid" @@ -32,7 +40,13 @@ TO_REDACT = { CONF_SERIAL, CONF_SID, CONF_SYSTEM_ID, + # Config entry title may contain sensitive data: + CONF_TITLE, + CONF_TOKEN, CONF_UID, + # Config entry unique ID may contain sensitive data: + CONF_UNIQUE_ID, + CONF_USERNAME, CONF_WIFI_SSID, } @@ -45,9 +59,7 @@ async def async_get_config_entry_diagnostics( return async_redact_data( { - "entry": { - "options": dict(entry.options), - }, + "entry": entry.as_dict(), "subscription_data": simplisafe.subscription_data, "systems": [system.as_dict() for system in simplisafe.systems.values()], }, diff --git a/tests/components/simplisafe/test_diagnostics.py b/tests/components/simplisafe/test_diagnostics.py index 446d9d5e9e3..f7a88fe0d06 100644 --- a/tests/components/simplisafe/test_diagnostics.py +++ b/tests/components/simplisafe/test_diagnostics.py @@ -8,9 +8,17 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_simplisa """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { - "options": { - "code": REDACTED, - }, + "entry_id": config_entry.entry_id, + "version": 1, + "domain": "simplisafe", + "title": REDACTED, + "data": {"token": REDACTED, "username": REDACTED}, + "options": {"code": REDACTED}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": REDACTED, + "disabled_by": None, }, "subscription_data": { "system_123": { From cc13641f29385d58c425ca4afa10f5623c484dab Mon Sep 17 00:00:00 2001 From: kingy444 Date: Wed, 12 Oct 2022 05:21:54 +1100 Subject: [PATCH 371/985] Powerview Implement remaining types (#80097) --- .../hunterdouglas_powerview/button.py | 7 + .../hunterdouglas_powerview/cover.py | 398 +++++++++++++++++- 2 files changed, 404 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hunterdouglas_powerview/button.py b/homeassistant/components/hunterdouglas_powerview/button.py index 483e2ca2784..ca9a72a7b99 100644 --- a/homeassistant/components/hunterdouglas_powerview/button.py +++ b/homeassistant/components/hunterdouglas_powerview/button.py @@ -48,6 +48,13 @@ BUTTONS: Final = [ entity_category=EntityCategory.DIAGNOSTIC, press_action=lambda shade: shade.jog(), ), + PowerviewButtonDescription( + key="favorite", + name="Favorite", + icon="mdi:heart", + entity_category=EntityCategory.DIAGNOSTIC, + press_action=lambda shade: shade.favorite(), + ), ] diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 1f04c8ddbd1..0082c68e26e 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -6,6 +6,7 @@ from collections.abc import Iterable from contextlib import suppress from datetime import timedelta import logging +from math import ceil from typing import Any from aiopvapi.helpers.constants import ( @@ -541,6 +542,58 @@ class PowerViewShadeWithTiltAnywhere(PowerViewShadeWithTiltBase): ) +class PowerViewShadeTiltOnly(PowerViewShadeWithTiltBase): + """Representation of a shade with tilt only capability, no move. + + API Class: ShadeTiltOnly + + Type 5 - Tilt Only 180° + """ + + def __init__( + self, + coordinator: PowerviewShadeUpdateCoordinator, + device_info: PowerviewDeviceInfo, + room_name: str, + shade: BaseShade, + name: str, + ) -> None: + """Initialize the shade.""" + super().__init__(coordinator, device_info, room_name, shade, name) + self._attr_supported_features = ( + CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.SET_TILT_POSITION + ) + if self._device_info.model != LEGACY_DEVICE_MODEL: + self._attr_supported_features |= CoverEntityFeature.STOP_TILT + self._max_tilt = self._shade.shade_limits.tilt_max + + +class PowerViewShadeTopDown(PowerViewShade): + """Representation of a shade that lowers from the roof to the floor. + + These shades are inverted where MAX_POSITION equates to closed and MIN_POSITION is open + API Class: ShadeTopDown + + Type 6 - Top Down + """ + + @property + def current_cover_position(self) -> int: + """Return the current position of cover.""" + return hd_position_to_hass(MAX_POSITION - self.positions.primary, MAX_POSITION) + + @property + def is_closed(self) -> bool: + """Return if the cover is closed.""" + return (MAX_POSITION - self.positions.primary) <= CLOSED_POSITION + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Move the shade to a specific position.""" + await self._async_set_cover_position(100 - kwargs[ATTR_POSITION]) + + class PowerViewShadeDualRailBase(PowerViewShade): """Representation of a shade with top/down bottom/up capabilities. @@ -677,11 +730,354 @@ class PowerViewShadeTDBUTop(PowerViewShadeDualRailBase): ) +class PowerViewShadeDualOverlappedBase(PowerViewShade): + """Represent a shade that has a front sheer and rear blackout panel. + + This equates to two shades being controlled by one motor + """ + + @property + def transition_steps(self) -> int: + """Return the steps to make a move.""" + # poskind 1 represents the second half of the shade in hass + # front must be fully closed before rear can move + # 51 - 100 is equiv to 1-100 on other shades - one motor, two shades + primary = (hd_position_to_hass(self.positions.primary, MAX_POSITION) / 2) + 50 + # poskind 2 represents the shade first half of the shade in hass + # rear (blackout) must be fully open before front can move + # 51 - 100 is equiv to 1-100 on other shades - one motor, two shades + secondary = hd_position_to_hass(self.positions.secondary, MAX_POSITION) / 2 + return ceil(primary + secondary) + + @property + def open_position(self) -> PowerviewShadeMove: + """Return the open position and required additional positions.""" + return PowerviewShadeMove( + { + ATTR_POSITION1: MAX_POSITION, + ATTR_POSKIND1: POS_KIND_PRIMARY, + }, + {POS_KIND_SECONDARY: MIN_POSITION, POS_KIND_VANE: MIN_POSITION}, + ) + + @property + def close_position(self) -> PowerviewShadeMove: + """Return the open position and required additional positions.""" + return PowerviewShadeMove( + { + ATTR_POSITION1: MIN_POSITION, + ATTR_POSKIND1: POS_KIND_SECONDARY, + }, + {POS_KIND_PRIMARY: MIN_POSITION, POS_KIND_VANE: MIN_POSITION}, + ) + + +class PowerViewShadeDualOverlappedCombined(PowerViewShadeDualOverlappedBase): + """Represent a shade that has a front sheer and rear blackout panel. + + This equates to two shades being controlled by one motor. + The front shade must be completely down before the rear shade will move. + Sibling Class: PowerViewShadeDualOverlappedFront, PowerViewShadeDualOverlappedRear + API Class: ShadeDualOverlapped + + Type 8 - Duolite (front and rear shades) + """ + + # type + def __init__( + self, + coordinator: PowerviewShadeUpdateCoordinator, + device_info: PowerviewDeviceInfo, + room_name: str, + shade: BaseShade, + name: str, + ) -> None: + """Initialize the shade.""" + super().__init__(coordinator, device_info, room_name, shade, name) + self._attr_unique_id = f"{self._shade.id}_combined" + self._attr_name = f"{self._shade_name} Combined" + + @property + def is_closed(self) -> bool: + """Return if the cover is closed.""" + # if rear shade is down it is closed + return self.positions.secondary <= CLOSED_POSITION + + @property + def current_cover_position(self) -> int: + """Return the current position of cover.""" + # if front is open return that (other positions are impossible) + # if front shade is closed get position of rear + position = (hd_position_to_hass(self.positions.primary, MAX_POSITION) / 2) + 50 + if self.positions.primary == MIN_POSITION: + position = hd_position_to_hass(self.positions.secondary, MAX_POSITION) / 2 + + return ceil(position) + + @callback + def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove: + position_shade = hass_position_to_hd(target_hass_position, MAX_POSITION) + # note we set POS_KIND_VANE: MIN_POSITION here even with shades without tilt so no additional + # override is required for differences between type 8/9/10 + # this just stores the value in the coordinator for future reference + if target_hass_position <= 50: + target_hass_position = target_hass_position * 2 + return PowerviewShadeMove( + { + ATTR_POSITION1: position_shade, + ATTR_POSKIND1: POS_KIND_SECONDARY, + }, + {POS_KIND_PRIMARY: MIN_POSITION, POS_KIND_VANE: MIN_POSITION}, + ) + + # 51 <= target_hass_position <= 100 (51-100 represents front sheer shade) + target_hass_position = (target_hass_position - 50) * 2 + return PowerviewShadeMove( + { + ATTR_POSITION1: position_shade, + ATTR_POSKIND1: POS_KIND_PRIMARY, + }, + {POS_KIND_SECONDARY: MAX_POSITION, POS_KIND_VANE: MIN_POSITION}, + ) + + +class PowerViewShadeDualOverlappedFront(PowerViewShadeDualOverlappedBase): + """Represent the shade front panel - These have a blackout panel too. + + This equates to two shades being controlled by one motor. + The front shade must be completely down before the rear shade will move. + Sibling Class: PowerViewShadeDualOverlappedCombined, PowerViewShadeDualOverlappedRear + API Class: ShadeDualOverlapped + ShadeDualOverlappedTilt90 + ShadeDualOverlappedTilt180 + + Type 8 - Duolite (front and rear shades) + Type 9 - Duolite with 90° Tilt (front bottom up shade that also tilts plus a rear blackout (non-tilting) shade) + Type 10 - Duolite with 180° Tilt + """ + + def __init__( + self, + coordinator: PowerviewShadeUpdateCoordinator, + device_info: PowerviewDeviceInfo, + room_name: str, + shade: BaseShade, + name: str, + ) -> None: + """Initialize the shade.""" + super().__init__(coordinator, device_info, room_name, shade, name) + self._attr_unique_id = f"{self._shade.id}_front" + self._attr_name = f"{self._shade_name} Front" + + @property + def should_poll(self) -> bool: + """Certain shades create multiple entities. + + Do not poll shade multiple times. Combined shade will return data + and multiple polling will cause timeouts. + """ + return False + + @callback + def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove: + position_shade = hass_position_to_hd(target_hass_position, MAX_POSITION) + # note we set POS_KIND_VANE: MIN_POSITION here even with shades without tilt so no additional + # override is required for differences between type 8/9/10 + # this just stores the value in the coordinator for future reference + return PowerviewShadeMove( + { + ATTR_POSITION1: position_shade, + ATTR_POSKIND1: POS_KIND_PRIMARY, + }, + {POS_KIND_SECONDARY: MAX_POSITION, POS_KIND_VANE: MIN_POSITION}, + ) + + @property + def close_position(self) -> PowerviewShadeMove: + """Return the close position and required additional positions.""" + return PowerviewShadeMove( + { + ATTR_POSITION1: MIN_POSITION, + ATTR_POSKIND1: POS_KIND_PRIMARY, + }, + {POS_KIND_SECONDARY: MAX_POSITION, POS_KIND_VANE: MIN_POSITION}, + ) + + +class PowerViewShadeDualOverlappedRear(PowerViewShadeDualOverlappedBase): + """Represent the shade front panel - These have a blackout panel too. + + This equates to two shades being controlled by one motor. + The front shade must be completely down before the rear shade will move. + Sibling Class: PowerViewShadeDualOverlappedCombined, PowerViewShadeDualOverlappedFront + API Class: ShadeDualOverlapped + ShadeDualOverlappedTilt90 + ShadeDualOverlappedTilt180 + + Type 8 - Duolite (front and rear shades) + Type 9 - Duolite with 90° Tilt (front bottom up shade that also tilts plus a rear blackout (non-tilting) shade) + Type 10 - Duolite with 180° Tilt + """ + + def __init__( + self, + coordinator: PowerviewShadeUpdateCoordinator, + device_info: PowerviewDeviceInfo, + room_name: str, + shade: BaseShade, + name: str, + ) -> None: + """Initialize the shade.""" + super().__init__(coordinator, device_info, room_name, shade, name) + self._attr_unique_id = f"{self._shade.id}_rear" + self._attr_name = f"{self._shade_name} Rear" + + @property + def should_poll(self) -> bool: + """Certain shades create multiple entities. + + Do not poll shade multiple times. Combined shade will return data + and multiple polling will cause timeouts. + """ + return False + + @property + def is_closed(self) -> bool: + """Return if the cover is closed.""" + # if rear shade is down it is closed + return self.positions.secondary <= CLOSED_POSITION + + @property + def current_cover_position(self) -> int: + """Return the current position of cover.""" + return hd_position_to_hass(self.positions.secondary, MAX_POSITION) + + @callback + def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove: + position_shade = hass_position_to_hd(target_hass_position, MAX_POSITION) + # note we set POS_KIND_VANE: MIN_POSITION here even with shades without tilt so no additional + # override is required for differences between type 8/9/10 + # this just stores the value in the coordinator for future reference + return PowerviewShadeMove( + { + ATTR_POSITION1: position_shade, + ATTR_POSKIND1: POS_KIND_SECONDARY, + }, + {POS_KIND_PRIMARY: MIN_POSITION, POS_KIND_VANE: MIN_POSITION}, + ) + + @property + def open_position(self) -> PowerviewShadeMove: + """Return the open position and required additional positions.""" + return PowerviewShadeMove( + { + ATTR_POSITION1: MAX_POSITION, + ATTR_POSKIND1: POS_KIND_SECONDARY, + }, + {POS_KIND_PRIMARY: MIN_POSITION, POS_KIND_VANE: MIN_POSITION}, + ) + + +class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombined): + """Represent a shade that has a front sheer and rear blackout panel. + + This equates to two shades being controlled by one motor. + The front shade must be completely down before the rear shade will move. + Tilting this shade will also force positional change of the main roller. + + Sibling Class: PowerViewShadeDualOverlappedFront, PowerViewShadeDualOverlappedRear + API Class: ShadeDualOverlappedTilt90 + ShadeDualOverlappedTilt180 + + Type 9 - Duolite with 90° Tilt (front bottom up shade that also tilts plus a rear blackout (non-tilting) shade) + Type 10 - Duolite with 180° Tilt + """ + + # type + def __init__( + self, + coordinator: PowerviewShadeUpdateCoordinator, + device_info: PowerviewDeviceInfo, + room_name: str, + shade: BaseShade, + name: str, + ) -> None: + """Initialize the shade.""" + super().__init__(coordinator, device_info, room_name, shade, name) + self._attr_supported_features |= ( + CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.SET_TILT_POSITION + ) + if self._device_info.model != LEGACY_DEVICE_MODEL: + self._attr_supported_features |= CoverEntityFeature.STOP_TILT + self._max_tilt = self._shade.shade_limits.tilt_max + + @property + def transition_steps(self) -> int: + """Return the steps to make a move.""" + # poskind 1 represents the second half of the shade in hass + # front must be fully closed before rear can move + # 51 - 100 is equiv to 1-100 on other shades - one motor, two shades + primary = (hd_position_to_hass(self.positions.primary, MAX_POSITION) / 2) + 50 + # poskind 2 represents the shade first half of the shade in hass + # rear (blackout) must be fully open before front can move + # 51 - 100 is equiv to 1-100 on other shades - one motor, two shades + secondary = hd_position_to_hass(self.positions.secondary, MAX_POSITION) / 2 + vane = hd_position_to_hass(self.positions.vane, self._max_tilt) + return ceil(primary + secondary + vane) + + @callback + def _get_shade_tilt(self, target_hass_tilt_position: int) -> PowerviewShadeMove: + """Return a PowerviewShadeMove.""" + position_vane = hass_position_to_hd(target_hass_tilt_position, self._max_tilt) + return PowerviewShadeMove( + { + ATTR_POSITION1: position_vane, + ATTR_POSKIND1: POS_KIND_VANE, + }, + {POS_KIND_PRIMARY: MIN_POSITION, POS_KIND_SECONDARY: MAX_POSITION}, + ) + + @property + def open_tilt_position(self) -> PowerviewShadeMove: + """Return the open tilt position and required additional positions.""" + return PowerviewShadeMove( + self._shade.open_position_tilt, + {POS_KIND_PRIMARY: MIN_POSITION, POS_KIND_SECONDARY: MAX_POSITION}, + ) + + @property + def close_tilt_position(self) -> PowerviewShadeMove: + """Return the open tilt position and required additional positions.""" + return PowerviewShadeMove( + self._shade.open_position_tilt, + {POS_KIND_PRIMARY: MIN_POSITION, POS_KIND_SECONDARY: MAX_POSITION}, + ) + + TYPE_TO_CLASSES = { + 0: (PowerViewShade,), 1: (PowerViewShadeWithTiltOnClosed,), 2: (PowerViewShadeWithTiltAnywhere,), + 3: (PowerViewShade,), 4: (PowerViewShadeWithTiltAnywhere,), - 7: (PowerViewShadeTDBUTop, PowerViewShadeTDBUBottom), + 5: (PowerViewShadeTiltOnly,), + 6: (PowerViewShadeTopDown,), + 7: ( + PowerViewShadeTDBUTop, + PowerViewShadeTDBUBottom, + ), + 8: ( + PowerViewShadeDualOverlappedCombined, + PowerViewShadeDualOverlappedFront, + PowerViewShadeDualOverlappedRear, + ), + 9: ( + PowerViewShadeDualOverlappedCombinedTilt, + PowerViewShadeDualOverlappedFront, + PowerViewShadeDualOverlappedRear, + ), + 10: ( + PowerViewShadeDualOverlappedCombinedTilt, + PowerViewShadeDualOverlappedFront, + PowerViewShadeDualOverlappedRear, + ), } From f92da26c042bc85290e3bcd9eb1737c338676d08 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 11 Oct 2022 13:03:48 -0600 Subject: [PATCH 372/985] Add config entry to Ridwell diagnostics (#80120) --- .../components/ridwell/diagnostics.py | 24 ++++++++++++++++--- tests/components/ridwell/test_diagnostics.py | 17 ++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ridwell/diagnostics.py b/homeassistant/components/ridwell/diagnostics.py index 3f29165842f..b4832770409 100644 --- a/homeassistant/components/ridwell/diagnostics.py +++ b/homeassistant/components/ridwell/diagnostics.py @@ -4,12 +4,24 @@ from __future__ import annotations import dataclasses from typing import Any +from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME from homeassistant.core import HomeAssistant from . import RidwellData from .const import DOMAIN +CONF_TITLE = "title" + +TO_REDACT = { + CONF_PASSWORD, + # Config entry title and unique ID may contain sensitive data: + CONF_TITLE, + CONF_UNIQUE_ID, + CONF_USERNAME, +} + async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry @@ -17,6 +29,12 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" data: RidwellData = hass.data[DOMAIN][entry.entry_id] - return { - "data": [dataclasses.asdict(event) for event in data.coordinator.data.values()] - } + return async_redact_data( + { + "entry": entry.as_dict(), + "data": [ + dataclasses.asdict(event) for event in data.coordinator.data.values() + ], + }, + TO_REDACT, + ) diff --git a/tests/components/ridwell/test_diagnostics.py b/tests/components/ridwell/test_diagnostics.py index 8427fa13e11..96d1531ac84 100644 --- a/tests/components/ridwell/test_diagnostics.py +++ b/tests/components/ridwell/test_diagnostics.py @@ -1,10 +1,25 @@ """Test Ridwell diagnostics.""" +from homeassistant.components.diagnostics import REDACTED + from tests.components.diagnostics import get_diagnostics_for_config_entry async def test_entry_diagnostics(hass, config_entry, hass_client, setup_ridwell): """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { + "entry": { + "entry_id": config_entry.entry_id, + "version": 2, + "domain": "ridwell", + "title": REDACTED, + "data": {"username": REDACTED, "password": REDACTED}, + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": REDACTED, + "disabled_by": None, + }, "data": [ { "_async_request": None, @@ -31,5 +46,5 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_ridwell) "repr": "", }, } - ] + ], } From b446cab03b28d80bd62e1ad68ff87e5b3189829a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 11 Oct 2022 13:14:07 -0600 Subject: [PATCH 373/985] Redact additional sensitive fields in ReCollect Waste diagnostics (#80119) * Redact additional sensitive fields in ReCollect Waste diagnostics * One call --- .../components/recollect_waste/diagnostics.py | 26 +++++++++++++++---- .../recollect_waste/test_diagnostics.py | 18 +++++++++++-- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recollect_waste/diagnostics.py b/homeassistant/components/recollect_waste/diagnostics.py index fb19b1790f5..d410eb40085 100644 --- a/homeassistant/components/recollect_waste/diagnostics.py +++ b/homeassistant/components/recollect_waste/diagnostics.py @@ -4,11 +4,24 @@ from __future__ import annotations import dataclasses from typing import Any +from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_UNIQUE_ID from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN +from .const import CONF_PLACE_ID, DOMAIN + +CONF_AREA_NAME = "area_name" +CONF_TITLE = "title" + +TO_REDACT = { + CONF_AREA_NAME, + CONF_PLACE_ID, + # Config entry title and unique ID may contain sensitive data: + CONF_TITLE, + CONF_UNIQUE_ID, +} async def async_get_config_entry_diagnostics( @@ -17,7 +30,10 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - return { - "entry": entry.as_dict(), - "data": [dataclasses.asdict(event) for event in coordinator.data], - } + return async_redact_data( + { + "entry": entry.as_dict(), + "data": [dataclasses.asdict(event) for event in coordinator.data], + }, + TO_REDACT, + ) diff --git a/tests/components/recollect_waste/test_diagnostics.py b/tests/components/recollect_waste/test_diagnostics.py index c9c9ba5a93f..93978135681 100644 --- a/tests/components/recollect_waste/test_diagnostics.py +++ b/tests/components/recollect_waste/test_diagnostics.py @@ -1,4 +1,6 @@ """Test ReCollect Waste diagnostics.""" +from homeassistant.components.diagnostics import REDACTED + from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -7,7 +9,19 @@ async def test_entry_diagnostics( ): """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "entry": config_entry.as_dict(), + "entry": { + "entry_id": config_entry.entry_id, + "version": 2, + "domain": "recollect_waste", + "title": REDACTED, + "data": {"place_id": REDACTED, "service_id": "12345"}, + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": REDACTED, + "disabled_by": None, + }, "data": [ { "date": { @@ -17,7 +31,7 @@ async def test_entry_diagnostics( "pickup_types": [ {"name": "garbage", "friendly_name": "Trash Collection"} ], - "area_name": "The Sun", + "area_name": REDACTED, } ], } From 54fb0d7cc2061ac6928c3af035c82fdb36f803fc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Oct 2022 21:21:59 +0200 Subject: [PATCH 374/985] Fix set humidity in Tuya (#80132) --- homeassistant/components/tuya/humidifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/humidifier.py b/homeassistant/components/tuya/humidifier.py index 5fd33ba80d0..9891c81a456 100644 --- a/homeassistant/components/tuya/humidifier.py +++ b/homeassistant/components/tuya/humidifier.py @@ -104,7 +104,7 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity): if int_type := self.find_dpcode( description.humidity, dptype=DPType.INTEGER, prefer_function=True ): - self._set_humiditye = int_type + self._set_humidity = int_type self._attr_min_humidity = int(int_type.min_scaled) self._attr_max_humidity = int(int_type.max_scaled) From 9dd894a36e7dafac0bec15602c40dec6569a8277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 11 Oct 2022 22:49:02 +0300 Subject: [PATCH 375/985] Huawei LTE logging related tweaks (#79854) * Remove no longer needed dicttoxml logging config huawei-lte-api 1.5+ no longer uses dicttoxml. * Fix `loggers` entry --- homeassistant/components/huawei_lte/__init__.py | 4 ---- homeassistant/components/huawei_lte/manifest.json | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index b51d01f0fd7..17646fa3ed6 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -490,10 +490,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Huawei LTE component.""" - # dicttoxml (used by huawei-lte-api) has uselessly verbose INFO level. - # https://github.com/quandyfactory/dicttoxml/issues/60 - logging.getLogger("dicttoxml").setLevel(logging.WARNING) - if DOMAIN not in hass.data: hass.data[DOMAIN] = HuaweiLteData(hass_config=config, routers={}) diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 473d8df3124..c658fff1b0f 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -16,5 +16,5 @@ ], "codeowners": ["@scop", "@fphammerle"], "iot_class": "local_polling", - "loggers": ["huawei_lte_api"] + "loggers": ["huawei_lte_api.Session"] } From e3a3f934415beb195e26317347cfee9af4e95633 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Oct 2022 22:54:49 +0200 Subject: [PATCH 376/985] Add full test coverage to LaMetric (#80134) --- .coveragerc | 1 - tests/components/lametric/test_notify.py | 124 +++++++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 tests/components/lametric/test_notify.py diff --git a/.coveragerc b/.coveragerc index 939a6e092b0..9b5167eb68f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -660,7 +660,6 @@ omit = homeassistant/components/kostal_plenticore/switch.py homeassistant/components/kwb/sensor.py homeassistant/components/lacrosse/sensor.py - homeassistant/components/lametric/notify.py homeassistant/components/lannouncer/notify.py homeassistant/components/lastfm/sensor.py homeassistant/components/launch_library/__init__.py diff --git a/tests/components/lametric/test_notify.py b/tests/components/lametric/test_notify.py new file mode 100644 index 00000000000..3b581c81e75 --- /dev/null +++ b/tests/components/lametric/test_notify.py @@ -0,0 +1,124 @@ +"""Tests for the LaMetric notify platform.""" +from unittest.mock import MagicMock + +from demetriek import ( + LaMetricError, + Notification, + NotificationIconType, + NotificationPriority, + NotificationSound, + NotificationSoundCategory, + Simple, +) +import pytest + +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_MESSAGE, + DOMAIN as NOTIFY_DOMAIN, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from tests.common import MockConfigEntry + +NOTIFY_SERVICE = "frenck_s_lametric" + + +async def test_notification_defaults( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_lametric: MagicMock, +) -> None: + """Test the LaMetric notification defaults.""" + await hass.services.async_call( + NOTIFY_DOMAIN, + NOTIFY_SERVICE, + { + ATTR_MESSAGE: "Try not to become a man of success. Rather become a man of value", + }, + blocking=True, + ) + + assert len(mock_lametric.notify.mock_calls) == 1 + + notification: Notification = mock_lametric.notify.mock_calls[0][2]["notification"] + assert notification.icon_type is NotificationIconType.NONE + assert notification.life_time is None + assert notification.model.cycles == 1 + assert notification.model.sound is None + assert notification.notification_id is None + assert notification.notification_type is None + assert notification.priority is NotificationPriority.INFO + + assert len(notification.model.frames) == 1 + frame = notification.model.frames[0] + assert type(frame) is Simple + assert frame.icon == "a7956" + assert ( + frame.text == "Try not to become a man of success. Rather become a man of value" + ) + + +async def test_notification_options( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_lametric: MagicMock, +) -> None: + """Test the LaMetric notification options.""" + await hass.services.async_call( + NOTIFY_DOMAIN, + NOTIFY_SERVICE, + { + ATTR_MESSAGE: "The secret of getting ahead is getting started", + ATTR_DATA: { + "icon": "1234", + "sound": "positive1", + "cycles": 3, + "icon_type": "alert", + "priority": "critical", + }, + }, + blocking=True, + ) + + assert len(mock_lametric.notify.mock_calls) == 1 + + notification: Notification = mock_lametric.notify.mock_calls[0][2]["notification"] + assert notification.icon_type is NotificationIconType.ALERT + assert notification.life_time is None + assert notification.model.cycles == 3 + assert notification.model.sound is not None + assert notification.model.sound.category is NotificationSoundCategory.NOTIFICATIONS + assert notification.model.sound.sound is NotificationSound.POSITIVE1 + assert notification.model.sound.repeat == 1 + assert notification.notification_id is None + assert notification.notification_type is None + assert notification.priority is NotificationPriority.CRITICAL + + assert len(notification.model.frames) == 1 + frame = notification.model.frames[0] + assert type(frame) is Simple + assert frame.icon == 1234 + assert frame.text == "The secret of getting ahead is getting started" + + +async def test_notification_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_lametric: MagicMock, +) -> None: + """Test the LaMetric notification error.""" + mock_lametric.notify.side_effect = LaMetricError + + with pytest.raises( + HomeAssistantError, match="Could not send LaMetric notification" + ): + await hass.services.async_call( + NOTIFY_DOMAIN, + NOTIFY_SERVICE, + { + ATTR_MESSAGE: "It's failure that gives you the proper perspective on success", + }, + blocking=True, + ) From f23b1750e85f07091eb896a0b12b8f95e5646338 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Oct 2022 11:26:03 -1000 Subject: [PATCH 377/985] Migrate HomeKit Controller to use stable identifiers (#80064) --- .../homekit_controller/alarm_control_panel.py | 12 ++- .../homekit_controller/binary_sensor.py | 9 +- .../components/homekit_controller/button.py | 12 ++- .../components/homekit_controller/camera.py | 12 ++- .../components/homekit_controller/climate.py | 13 ++- .../homekit_controller/connection.py | 102 ++++++++++++++---- .../components/homekit_controller/cover.py | 19 +++- .../components/homekit_controller/entity.py | 27 +++-- .../components/homekit_controller/fan.py | 12 ++- .../homekit_controller/humidifier.py | 24 +++-- .../components/homekit_controller/light.py | 12 ++- .../components/homekit_controller/lock.py | 12 ++- .../homekit_controller/media_player.py | 12 ++- .../components/homekit_controller/number.py | 10 +- .../components/homekit_controller/select.py | 12 ++- .../components/homekit_controller/sensor.py | 28 ++++- .../components/homekit_controller/switch.py | 19 ++-- tests/components/homekit_controller/common.py | 22 ++-- .../specific_devices/test_anker_eufycam.py | 2 +- .../specific_devices/test_aqara_gateway.py | 14 +-- .../specific_devices/test_aqara_switch.py | 2 +- .../specific_devices/test_arlo_baby.py | 14 +-- .../specific_devices/test_connectsense.py | 16 +-- .../specific_devices/test_ecobee3.py | 36 +++---- .../specific_devices/test_ecobee_501.py | 4 +- .../specific_devices/test_ecobee_occupancy.py | 2 +- .../specific_devices/test_eve_degree.py | 10 +- .../specific_devices/test_eve_energy.py | 16 +-- .../specific_devices/test_haa_fan.py | 8 +- .../test_homeassistant_bridge.py | 2 +- .../specific_devices/test_hue_bridge.py | 2 +- .../specific_devices/test_koogeek_ls1.py | 4 +- .../specific_devices/test_koogeek_p1eu.py | 4 +- .../specific_devices/test_koogeek_sw2.py | 6 +- .../specific_devices/test_lennox_e30.py | 2 +- .../specific_devices/test_lg_tv.py | 2 +- .../test_lutron_caseta_bridge.py | 2 +- .../specific_devices/test_mss425f.py | 12 +-- .../specific_devices/test_mss565.py | 2 +- .../specific_devices/test_mysa_living.py | 8 +- .../test_nanoleaf_strip_nl55.py | 8 +- .../specific_devices/test_netamo_doorbell.py | 2 +- .../test_netamo_smart_co_alarm.py | 4 +- .../test_rainmachine_pro_8.py | 16 +-- .../test_ryse_smart_bridge.py | 24 ++--- .../specific_devices/test_schlage_sense.py | 2 +- .../test_simpleconnect_fan.py | 2 +- .../specific_devices/test_velux_gateway.py | 8 +- .../test_vocolinc_flowerbud.py | 8 +- .../specific_devices/test_vocolinc_vp3.py | 33 +++++- .../test_alarm_control_panel.py | 21 +++- .../homekit_controller/test_binary_sensor.py | 20 +++- .../homekit_controller/test_button.py | 20 +++- .../homekit_controller/test_camera.py | 19 +++- .../homekit_controller/test_climate.py | 19 +++- .../homekit_controller/test_connection.py | 6 -- .../homekit_controller/test_cover.py | 21 +++- .../components/homekit_controller/test_fan.py | 21 +++- .../homekit_controller/test_humidifier.py | 20 +++- .../homekit_controller/test_light.py | 47 +++++++- .../homekit_controller/test_lock.py | 21 +++- .../homekit_controller/test_media_player.py | 21 +++- .../homekit_controller/test_number.py | 22 +++- .../homekit_controller/test_select.py | 23 +++- .../homekit_controller/test_sensor.py | 37 ++++++- .../homekit_controller/test_switch.py | 31 +++++- 66 files changed, 781 insertions(+), 234 deletions(-) diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index 204fa1bb3f8..a466d15db58 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -18,11 +18,13 @@ from homeassistant.const import ( STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, + Platform, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import KNOWN_DEVICES +from .connection import HKDevice from .entity import HomeKitEntity ICON = "mdi:security" @@ -49,15 +51,19 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Homekit alarm control panel.""" - hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + hkid: str = config_entry.data["AccessoryPairingID"] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_service(service: Service) -> bool: if service.type != ServicesTypes.SECURITY_SYSTEM: return False info = {"aid": service.accessory.aid, "iid": service.iid} - async_add_entities([HomeKitAlarmControlPanelEntity(conn, info)], True) + entity = HomeKitAlarmControlPanelEntity(conn, info) + conn.async_migrate_unique_id( + entity.old_unique_id, entity.unique_id, Platform.ALARM_CONTROL_PANEL + ) + async_add_entities([entity]) return True conn.add_listener(async_add_service) diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index c980e31b50c..8115023fa52 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -158,7 +159,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Homekit lighting.""" - hkid = config_entry.data["AccessoryPairingID"] + hkid: str = config_entry.data["AccessoryPairingID"] conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback @@ -174,7 +175,11 @@ async def async_setup_entry( ): return False info = {"aid": service.accessory.aid, "iid": service.iid} - async_add_entities([entity_class(conn, info)], True) + entity: HomeKitEntity = entity_class(conn, info) + conn.async_migrate_unique_id( + entity.old_unique_id, entity.unique_id, Platform.BINARY_SENSOR + ) + async_add_entities([entity]) return True conn.add_listener(async_add_service) diff --git a/homeassistant/components/homekit_controller/button.py b/homeassistant/components/homekit_controller/button.py index d5a8bc733ad..4ce2b425a5e 100644 --- a/homeassistant/components/homekit_controller/button.py +++ b/homeassistant/components/homekit_controller/button.py @@ -16,6 +16,7 @@ from homeassistant.components.button import ( ButtonEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -63,12 +64,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Homekit buttons.""" - hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + hkid: str = config_entry.data["AccessoryPairingID"] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_characteristic(char: Characteristic) -> bool: - entities = [] + entities: list[HomeKitButton | HomeKitEcobeeClearHoldButton] = [] info = {"aid": char.service.accessory.aid, "iid": char.service.iid} if description := BUTTON_ENTITIES.get(char.type): @@ -78,6 +79,11 @@ async def async_setup_entry( else: return False + for entity in entities: + conn.async_migrate_unique_id( + entity.old_unique_id, entity.unique_id, Platform.BUTTON + ) + async_add_entities(entities, True) return True diff --git a/homeassistant/components/homekit_controller/camera.py b/homeassistant/components/homekit_controller/camera.py index 510c0c2f522..35a4b089641 100644 --- a/homeassistant/components/homekit_controller/camera.py +++ b/homeassistant/components/homekit_controller/camera.py @@ -6,10 +6,12 @@ from aiohomekit.model.services import ServicesTypes from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import KNOWN_DEVICES +from .connection import HKDevice from .entity import AccessoryEntity @@ -39,8 +41,8 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Homekit sensors.""" - hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + hkid: str = config_entry.data["AccessoryPairingID"] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_accessory(accessory: Accessory) -> bool: @@ -51,7 +53,11 @@ async def async_setup_entry( return False info = {"aid": accessory.aid, "iid": stream_mgmt.iid} - async_add_entities([HomeKitCamera(conn, info)], True) + entity = HomeKitCamera(conn, info) + conn.async_migrate_unique_id( + entity.old_unique_id, entity.unique_id, Platform.CAMERA + ) + async_add_entities([entity]) return True conn.add_accessory_factory(async_add_accessory) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 41788eb4cb2..de42243a6bb 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -32,11 +32,12 @@ from homeassistant.components.climate import ( HVACMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import KNOWN_DEVICES +from .connection import HKDevice from .entity import HomeKitEntity _LOGGER = logging.getLogger(__name__) @@ -92,15 +93,19 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Homekit climate.""" - hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + hkid: str = config_entry.data["AccessoryPairingID"] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_service(service: Service) -> bool: if not (entity_class := ENTITY_TYPES.get(service.type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} - async_add_entities([entity_class(conn, info)], True) + entity: HomeKitEntity = entity_class(conn, info) + conn.async_migrate_unique_id( + entity.old_unique_id, entity.unique_id, Platform.CLIMATE + ) + async_add_entities([entity]) return True conn.add_listener(async_add_service) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 05a0a589bf1..e2ab68f8c63 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -21,7 +21,7 @@ from aiohomekit.model.services import Service from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_VIA_DEVICE from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_time_interval @@ -79,9 +79,7 @@ class HKDevice: connection: Controller = hass.data[CONTROLLER] - self.pairing = connection.load_pairing( - self.pairing_data["AccessoryPairingID"], self.pairing_data - ) + self.pairing = connection.load_pairing(self.unique_id, self.pairing_data) # A list of callbacks that turn HK accessories into entities self.accessory_factories: list[AddAccessoryCb] = [] @@ -253,7 +251,12 @@ class HKDevice: identifiers.add((IDENTIFIER_SERIAL_NUMBER, accessory.serial_number)) device_info = DeviceInfo( - identifiers=identifiers, + identifiers={ + ( + IDENTIFIER_ACCESSORY_ID, + f"{self.unique_id}:aid:{accessory.aid}", + ) + }, name=accessory.name, manufacturer=accessory.manufacturer, model=accessory.model, @@ -317,27 +320,87 @@ class HKDevice: self.unique_id, accessory.aid, ) + device_registry.async_update_device( + device.id, + new_identifiers={ + ( + IDENTIFIER_ACCESSORY_ID, + f"{self.unique_id}:aid:{accessory.aid}", + ) + }, + ) - new_identifiers = { + @callback + def async_migrate_unique_id( + self, old_unique_id: str, new_unique_id: str, platform: str + ) -> None: + """Migrate legacy unique IDs to new format.""" + _LOGGER.debug( + "Checking if unique ID %s on %s needs to be migrated", + old_unique_id, + platform, + ) + entity_registry = er.async_get(self.hass) + # async_get_entity_id wants the "homekit_controller" domain + # in the platform field and the actual platform in the domain + # field for historical reasons since everything used to be + # PLATFORM.INTEGRATION instead of INTEGRATION.PLATFORM + if ( + entity_id := entity_registry.async_get_entity_id( + platform, DOMAIN, old_unique_id + ) + ) is None: + _LOGGER.debug("Unique ID %s does not need to be migrated", old_unique_id) + return + if new_entity_id := entity_registry.async_get_entity_id( + platform, DOMAIN, new_unique_id + ): + _LOGGER.debug( + "Unique ID %s is already in use by %s (system may have been downgraded)", + new_unique_id, + new_entity_id, + ) + return + _LOGGER.debug( + "Migrating unique ID for entity %s (%s -> %s)", + entity_id, + old_unique_id, + new_unique_id, + ) + entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id) + + @callback + def async_remove_legacy_device_serial_numbers(self) -> None: + """Migrate remove legacy serial numbers from devices. + + We no longer use serial numbers as device identifiers + since they are not reliable, and the HomeKit spec + does not require them to be stable. + """ + _LOGGER.debug( + "Removing legacy serial numbers from device registry entries for pairing %s", + self.unique_id, + ) + + device_registry = dr.async_get(self.hass) + for accessory in self.entity_map.accessories: + identifiers = { ( IDENTIFIER_ACCESSORY_ID, f"{self.unique_id}:aid:{accessory.aid}", ) } - - if not self.unreliable_serial_numbers: - new_identifiers.add((IDENTIFIER_SERIAL_NUMBER, accessory.serial_number)) - else: - _LOGGER.debug( - "Not migrating serial number identifier for %s:aid:%s (it is wrong, not unique or unreliable)", - self.unique_id, - accessory.aid, - ) - - device_registry.async_update_device( - device.id, new_identifiers=new_identifiers + legacy_serial_identifier = ( + IDENTIFIER_SERIAL_NUMBER, + accessory.serial_number, ) + device = device_registry.async_get_device(identifiers=identifiers) + if not device or legacy_serial_identifier not in device.identifiers: + continue + + device_registry.async_update_device(device.id, new_identifiers=identifiers) + @callback def async_create_devices(self) -> None: """ @@ -416,6 +479,9 @@ class HKDevice: # Migrate to new device ids self.async_migrate_devices() + # Remove any of the legacy serial numbers from the device registry + self.async_remove_legacy_device_serial_numbers() + self.async_create_devices() # Load any triggers for this config entry diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index 6cbc623596e..d4feeccc77a 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -14,11 +14,18 @@ from homeassistant.components.cover import ( CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING +from homeassistant.const import ( + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, + Platform, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import KNOWN_DEVICES +from .connection import HKDevice from .entity import HomeKitEntity STATE_STOPPED = "stopped" @@ -42,15 +49,19 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Homekit covers.""" - hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + hkid: str = config_entry.data["AccessoryPairingID"] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_service(service: Service) -> bool: if not (entity_class := ENTITY_TYPES.get(service.type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} - async_add_entities([entity_class(conn, info)], True) + entity: HomeKitEntity = entity_class(conn, info) + conn.async_migrate_unique_id( + entity.old_unique_id, entity.unique_id, Platform.COVER + ) + async_add_entities([entity]) return True conn.add_listener(async_add_service) diff --git a/homeassistant/components/homekit_controller/entity.py b/homeassistant/components/homekit_controller/entity.py index ad99e65f2d8..a4e1b2b41b3 100644 --- a/homeassistant/components/homekit_controller/entity.py +++ b/homeassistant/components/homekit_controller/entity.py @@ -121,8 +121,8 @@ class HomeKitEntity(Entity): self._char_name = char.service.value(CharacteristicsTypes.NAME) @property - def unique_id(self) -> str: - """Return the ID of this device.""" + def old_unique_id(self) -> str: + """Return the OLD ID of this device.""" info = self.accessory_info serial = info.value(CharacteristicsTypes.SERIAL_NUMBER) if valid_serial_number(serial): @@ -130,6 +130,11 @@ class HomeKitEntity(Entity): # Some accessories do not have a serial number return f"homekit-{self._accessory.unique_id}-{self._aid}-{self._iid}" + @property + def unique_id(self) -> str: + """Return the ID of this device.""" + return f"{self._accessory.unique_id}_{self._aid}_{self._iid}" + @property def default_name(self) -> str | None: """Return the default name of the device.""" @@ -175,11 +180,16 @@ class AccessoryEntity(HomeKitEntity): """A HomeKit entity that is related to an entire accessory rather than a specific service or characteristic.""" @property - def unique_id(self) -> str: - """Return the ID of this device.""" + def old_unique_id(self) -> str: + """Return the old ID of this device.""" serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) return f"homekit-{serial}-aid:{self._aid}" + @property + def unique_id(self) -> str: + """Return the ID of this device.""" + return f"{self._accessory.unique_id}_{self._aid}" + class CharacteristicEntity(HomeKitEntity): """ @@ -197,7 +207,12 @@ class CharacteristicEntity(HomeKitEntity): super().__init__(accessory, devinfo) @property - def unique_id(self) -> str: - """Return the ID of this device.""" + def old_unique_id(self) -> str: + """Return the old ID of this device.""" serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) return f"homekit-{serial}-aid:{self._aid}-sid:{self._char.service.iid}-cid:{self._char.iid}" + + @property + def unique_id(self) -> str: + """Return the ID of this device.""" + return f"{self._accessory.unique_id}_{self._aid}_{self._char.service.iid}_{self._char.iid}" diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index 03f4dade674..cdd9c3e803c 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -13,6 +13,7 @@ from homeassistant.components.fan import ( FanEntityFeature, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( @@ -21,6 +22,7 @@ from homeassistant.util.percentage import ( ) from . import KNOWN_DEVICES +from .connection import HKDevice from .entity import HomeKitEntity # 0 is clockwise, 1 is counter-clockwise. The match to forward and reverse is so that @@ -193,15 +195,19 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Homekit fans.""" - hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + hkid: str = config_entry.data["AccessoryPairingID"] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_service(service: Service) -> bool: if not (entity_class := ENTITY_TYPES.get(service.type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} - async_add_entities([entity_class(conn, info)], True) + entity: HomeKitEntity = entity_class(conn, info) + conn.async_migrate_unique_id( + entity.old_unique_id, entity.unique_id, Platform.FAN + ) + async_add_entities([entity]) return True conn.add_listener(async_add_service) diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index adc1b1c7935..e396b3c9c97 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -16,10 +16,12 @@ from homeassistant.components.humidifier import ( HumidifierEntityFeature, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import KNOWN_DEVICES +from .connection import HKDevice from .entity import HomeKitEntity HK_MODE_TO_HA = { @@ -243,11 +245,16 @@ class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity): ) @property - def unique_id(self) -> str: - """Return the ID of this device.""" + def old_unique_id(self) -> str: + """Return the old ID of this device.""" serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) return f"homekit-{serial}-{self._iid}-{self.device_class}" + @property + def unique_id(self) -> str: + """Return the ID of this device.""" + return f"{self._accessory.unique_id}_{self._iid}_{self.device_class}" + async def async_setup_entry( hass: HomeAssistant, @@ -255,8 +262,8 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Homekit humidifer.""" - hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + hkid: str = config_entry.data["AccessoryPairingID"] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_service(service: Service) -> bool: @@ -265,7 +272,7 @@ async def async_setup_entry( info = {"aid": service.accessory.aid, "iid": service.iid} - entities: list[HumidifierEntity] = [] + entities: list[HomeKitHumidifier | HomeKitDehumidifier] = [] if service.has(CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD): entities.append(HomeKitHumidifier(conn, info)) @@ -273,7 +280,12 @@ async def async_setup_entry( if service.has(CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD): entities.append(HomeKitDehumidifier(conn, info)) - async_add_entities(entities, True) + for entity in entities: + conn.async_migrate_unique_id( + entity.old_unique_id, entity.unique_id, Platform.HUMIDIFIER + ) + + async_add_entities(entities) return True diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index 010411c60d0..5bf810a89db 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -14,10 +14,12 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import KNOWN_DEVICES +from .connection import HKDevice from .entity import HomeKitEntity @@ -27,15 +29,19 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Homekit lightbulb.""" - hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + hkid: str = config_entry.data["AccessoryPairingID"] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_service(service: Service) -> bool: if service.type != ServicesTypes.LIGHTBULB: return False info = {"aid": service.accessory.aid, "iid": service.iid} - async_add_entities([HomeKitLight(conn, info)], True) + entity = HomeKitLight(conn, info) + conn.async_migrate_unique_id( + entity.old_unique_id, entity.unique_id, Platform.LIGHT + ) + async_add_entities([entity]) return True conn.add_listener(async_add_service) diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index 8e8919ae4f8..a6c8a3672a3 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -13,11 +13,13 @@ from homeassistant.const import ( STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED, + Platform, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import KNOWN_DEVICES +from .connection import HKDevice from .entity import HomeKitEntity CURRENT_STATE_MAP = { @@ -38,15 +40,19 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Homekit lock.""" - hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + hkid: str = config_entry.data["AccessoryPairingID"] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_service(service: Service) -> bool: if service.type != ServicesTypes.LOCK_MECHANISM: return False info = {"aid": service.accessory.aid, "iid": service.iid} - async_add_entities([HomeKitLock(conn, info)], True) + entity = HomeKitLock(conn, info) + conn.async_migrate_unique_id( + entity.old_unique_id, entity.unique_id, Platform.LOCK + ) + async_add_entities([entity]) return True conn.add_listener(async_add_service) diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py index 5c791f165e2..4efa7dbce1c 100644 --- a/homeassistant/components/homekit_controller/media_player.py +++ b/homeassistant/components/homekit_controller/media_player.py @@ -19,10 +19,12 @@ from homeassistant.components.media_player import ( MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import KNOWN_DEVICES +from .connection import HKDevice from .entity import HomeKitEntity _LOGGER = logging.getLogger(__name__) @@ -41,15 +43,19 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Homekit television.""" - hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + hkid: str = config_entry.data["AccessoryPairingID"] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_service(service: Service) -> bool: if service.type != ServicesTypes.TELEVISION: return False info = {"aid": service.accessory.aid, "iid": service.iid} - async_add_entities([HomeKitTelevision(conn, info)], True) + entity = HomeKitTelevision(conn, info) + conn.async_migrate_unique_id( + entity.old_unique_id, entity.unique_id, Platform.MEDIA_PLAYER + ) + async_add_entities([entity]) return True conn.add_listener(async_add_service) diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index 6347ccb2a56..a20ba83e80a 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -16,6 +16,7 @@ from homeassistant.components.number import ( NumberEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -59,12 +60,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Homekit numbers.""" - hkid = config_entry.data["AccessoryPairingID"] + hkid: str = config_entry.data["AccessoryPairingID"] conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_characteristic(char: Characteristic) -> bool: - entities = [] + entities: list[HomeKitNumber] = [] info = {"aid": char.service.accessory.aid, "iid": char.service.iid} if description := NUMBER_ENTITIES.get(char.type): @@ -72,6 +73,11 @@ async def async_setup_entry( else: return False + for entity in entities: + conn.async_migrate_unique_id( + entity.old_unique_id, entity.unique_id, Platform.NUMBER + ) + async_add_entities(entities, True) return True diff --git a/homeassistant/components/homekit_controller/select.py b/homeassistant/components/homekit_controller/select.py index a22f79d675b..ca5eaec4dc5 100644 --- a/homeassistant/components/homekit_controller/select.py +++ b/homeassistant/components/homekit_controller/select.py @@ -5,10 +5,12 @@ from aiohomekit.model.characteristics import Characteristic, CharacteristicsType from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import KNOWN_DEVICES +from .connection import HKDevice from .const import DEVICE_CLASS_ECOBEE_MODE from .entity import CharacteristicEntity @@ -58,14 +60,18 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Homekit select entities.""" - hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + hkid: str = config_entry.data["AccessoryPairingID"] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_characteristic(char: Characteristic) -> bool: if char.type == CharacteristicsTypes.VENDOR_ECOBEE_CURRENT_MODE: info = {"aid": char.service.accessory.aid, "iid": char.service.iid} - async_add_entities([EcobeeModeSelect(conn, info, char)]) + entity = EcobeeModeSelect(conn, info, char) + conn.async_migrate_unique_id( + entity.old_unique_id, entity.unique_id, Platform.SELECT + ) + async_add_entities([entity]) return True return False diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 564eb5ba9c6..e9f928dd571 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -30,6 +30,7 @@ from homeassistant.const import ( PRESSURE_HPA, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, + Platform, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory @@ -556,11 +557,16 @@ class RSSISensor(HomeKitEntity, SensorEntity): return "Signal strength" @property - def unique_id(self) -> str: - """Return the ID of this device.""" + def old_unique_id(self) -> str: + """Return the old ID of this device.""" serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) return f"homekit-{serial}-rssi" + @property + def unique_id(self) -> str: + """Return the ID of this device.""" + return f"{self._accessory.unique_id}_rssi" + @property def native_value(self) -> int | None: """Return the current rssi value.""" @@ -587,7 +593,11 @@ async def async_setup_entry( ) and not service.has(required_char): return False info = {"aid": service.accessory.aid, "iid": service.iid} - async_add_entities([entity_class(conn, info)]) + entity: HomeKitSensor = entity_class(conn, info) + conn.async_migrate_unique_id( + entity.old_unique_id, entity.unique_id, Platform.SENSOR + ) + async_add_entities([entity]) return True conn.add_listener(async_add_service) @@ -599,7 +609,11 @@ async def async_setup_entry( if description.probe and not description.probe(char): return False info = {"aid": char.service.accessory.aid, "iid": char.service.iid} - async_add_entities([SimpleSensor(conn, info, char, description)]) + entity = SimpleSensor(conn, info, char, description) + conn.async_migrate_unique_id( + entity.old_unique_id, entity.unique_id, Platform.SENSOR + ) + async_add_entities([entity]) return True @@ -614,7 +628,11 @@ async def async_setup_entry( service_type=ServicesTypes.ACCESSORY_INFORMATION ) info = {"aid": accessory.aid, "iid": accessory_info.iid} - async_add_entities([RSSISensor(conn, info)]) + entity = RSSISensor(conn, info) + conn.async_migrate_unique_id( + entity.old_unique_id, entity.unique_id, Platform.SENSOR + ) + async_add_entities([entity]) return True conn.add_accessory_factory(async_add_accessory) diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index c537233de7e..d1e06e585b0 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -14,6 +14,7 @@ from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -182,7 +183,7 @@ class DeclarativeCharacteristicSwitch(CharacteristicEntity, SwitchEntity): ) -ENTITY_TYPES = { +ENTITY_TYPES: dict[str, type[HomeKitSwitch] | type[HomeKitValve]] = { ServicesTypes.SWITCH: HomeKitSwitch, ServicesTypes.OUTLET: HomeKitSwitch, ServicesTypes.VALVE: HomeKitValve, @@ -195,15 +196,19 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Homekit switches.""" - hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + hkid: str = config_entry.data["AccessoryPairingID"] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_service(service: Service) -> bool: if not (entity_class := ENTITY_TYPES.get(service.type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} - async_add_entities([entity_class(conn, info)], True) + entity: HomeKitSwitch | HomeKitValve = entity_class(conn, info) + conn.async_migrate_unique_id( + entity.old_unique_id, entity.unique_id, Platform.SWITCH + ) + async_add_entities([entity]) return True conn.add_listener(async_add_service) @@ -214,9 +219,11 @@ async def async_setup_entry( return False info = {"aid": char.service.accessory.aid, "iid": char.service.iid} - async_add_entities( - [DeclarativeCharacteristicSwitch(conn, info, char, description)], True + entity = DeclarativeCharacteristicSwitch(conn, info, char, description) + conn.async_migrate_unique_id( + entity.old_unique_id, entity.unique_id, Platform.SWITCH ) + async_add_entities([entity]) return True conn.add_char_factory(async_add_characteristic) diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 07cc2b5cae7..b30ba6236a9 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -9,7 +9,12 @@ import os from typing import Any, Final from unittest import mock -from aiohomekit.model import Accessories, AccessoriesState, Accessory +from aiohomekit.model import ( + Accessories, + AccessoriesState, + Accessory, + mixin as model_mixin, +) from aiohomekit.testing import FakeController, FakePairing from aiohomekit.zeroconf import HomeKitService @@ -19,7 +24,6 @@ from homeassistant.components.homekit_controller.const import ( DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, IDENTIFIER_ACCESSORY_ID, - IDENTIFIER_SERIAL_NUMBER, ) from homeassistant.components.homekit_controller.utils import async_get_controller from homeassistant.config_entries import ConfigEntry @@ -320,7 +324,6 @@ async def assert_devices_and_entities_created( device = device_registry.async_get_device( { - (IDENTIFIER_SERIAL_NUMBER, expected.serial_number), (IDENTIFIER_ACCESSORY_ID, expected.unique_id), } ) @@ -336,21 +339,15 @@ async def assert_devices_and_entities_created( # We might have matched the device by one identifier only # Lets check that the other one is correct. Otherwise the test might silently be wrong. - serial_number_set = False accessory_id_set = False for key, value in device.identifiers: - if key == IDENTIFIER_SERIAL_NUMBER: - assert value == expected.serial_number - serial_number_set = True - - elif key == IDENTIFIER_ACCESSORY_ID: + if key == IDENTIFIER_ACCESSORY_ID: assert value == expected.unique_id accessory_id_set = True # If unique_id or serial is provided it MUST actually appear in the device registry entry. assert (not expected.unique_id) ^ accessory_id_set - assert (not expected.serial_number) ^ serial_number_set for entity_info in expected.entities: entity = entity_registry.async_get(entity_info.entity_id) @@ -410,3 +407,8 @@ async def remove_device(ws_client, device_id, config_entry_id): ) response = await ws_client.receive_json() return response["success"] + + +def get_next_aid(): + """Get next aid.""" + return model_mixin.id_counter + 1 diff --git a/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py b/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py index 644abb8a3a6..f2e209a9fdb 100644 --- a/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py +++ b/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py @@ -39,7 +39,7 @@ async def test_eufycam_setup(hass): EntityTestInfo( entity_id="camera.eufycam2_0000", friendly_name="eufyCam2-0000", - unique_id="homekit-A0000A000000000D-aid:4", + unique_id="00:00:00:00:00:00_4", state="idle", ), ], diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py index 75423f3373e..7df51316ceb 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py @@ -37,7 +37,7 @@ async def test_aqara_gateway_setup(hass): EntityTestInfo( "alarm_control_panel.aqara_hub_1563_security_system", friendly_name="Aqara Hub-1563 Security System", - unique_id="homekit-0000000123456789-66304", + unique_id="00:00:00:00:00:00_1_66304", supported_features=AlarmControlPanelEntityFeature.ARM_NIGHT | AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY, @@ -46,7 +46,7 @@ async def test_aqara_gateway_setup(hass): EntityTestInfo( "light.aqara_hub_1563_lightbulb_1563", friendly_name="Aqara Hub-1563 Lightbulb-1563", - unique_id="homekit-0000000123456789-65792", + unique_id="00:00:00:00:00:00_1_65792", supported_features=0, capabilities={"supported_color_modes": ["hs"]}, state="off", @@ -54,7 +54,7 @@ async def test_aqara_gateway_setup(hass): EntityTestInfo( "number.aqara_hub_1563_volume", friendly_name="Aqara Hub-1563 Volume", - unique_id="homekit-0000000123456789-aid:1-sid:65536-cid:65541", + unique_id="00:00:00:00:00:00_1_65536_65541", capabilities={ "max": 100, "min": 0, @@ -67,7 +67,7 @@ async def test_aqara_gateway_setup(hass): EntityTestInfo( "switch.aqara_hub_1563_pairing_mode", friendly_name="Aqara Hub-1563 Pairing Mode", - unique_id="homekit-0000000123456789-aid:1-sid:65536-cid:65538", + unique_id="00:00:00:00:00:00_1_65536_65538", entity_category=EntityCategory.CONFIG, state="off", ), @@ -96,7 +96,7 @@ async def test_aqara_gateway_e1_setup(hass): EntityTestInfo( "alarm_control_panel.aqara_hub_e1_00a0_security_system", friendly_name="Aqara-Hub-E1-00A0 Security System", - unique_id="homekit-00aa00000a0-16", + unique_id="00:00:00:00:00:00_1_16", supported_features=AlarmControlPanelEntityFeature.ARM_NIGHT | AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY, @@ -105,7 +105,7 @@ async def test_aqara_gateway_e1_setup(hass): EntityTestInfo( "number.aqara_hub_e1_00a0_volume", friendly_name="Aqara-Hub-E1-00A0 Volume", - unique_id="homekit-00aa00000a0-aid:1-sid:17-cid:1114116", + unique_id="00:00:00:00:00:00_1_17_1114116", capabilities={ "max": 100, "min": 0, @@ -118,7 +118,7 @@ async def test_aqara_gateway_e1_setup(hass): EntityTestInfo( "switch.aqara_hub_e1_00a0_pairing_mode", friendly_name="Aqara-Hub-E1-00A0 Pairing Mode", - unique_id="homekit-00aa00000a0-aid:1-sid:17-cid:1114117", + unique_id="00:00:00:00:00:00_1_17_1114117", entity_category=EntityCategory.CONFIG, state="off", ), diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py index 793fb49af5b..6472d993974 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py @@ -42,7 +42,7 @@ async def test_aqara_switch_setup(hass): EntityTestInfo( entity_id="sensor.programmable_switch_battery_sensor", friendly_name="Programmable Switch Battery Sensor", - unique_id="homekit-111a1111a1a111-5", + unique_id="00:00:00:00:00:00_1_5", capabilities={"state_class": SensorStateClass.MEASUREMENT}, entity_category=EntityCategory.DIAGNOSTIC, unit_of_measurement=PERCENTAGE, diff --git a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py index 1b2b4bda3d6..26c0c87e3b3 100644 --- a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py +++ b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py @@ -33,19 +33,19 @@ async def test_arlo_baby_setup(hass): entities=[ EntityTestInfo( entity_id="camera.arlobabya0", - unique_id="homekit-00A0000000000-aid:1", + unique_id="00:00:00:00:00:00_1", friendly_name="ArloBabyA0", state="idle", ), EntityTestInfo( entity_id="binary_sensor.arlobabya0_motion", - unique_id="homekit-00A0000000000-500", + unique_id="00:00:00:00:00:00_1_500", friendly_name="ArloBabyA0 Motion", state="off", ), EntityTestInfo( entity_id="sensor.arlobabya0_battery", - unique_id="homekit-00A0000000000-700", + unique_id="00:00:00:00:00:00_1_700", friendly_name="ArloBabyA0 Battery", entity_category=EntityCategory.DIAGNOSTIC, capabilities={"state_class": SensorStateClass.MEASUREMENT}, @@ -54,7 +54,7 @@ async def test_arlo_baby_setup(hass): ), EntityTestInfo( entity_id="sensor.arlobabya0_humidity", - unique_id="homekit-00A0000000000-900", + unique_id="00:00:00:00:00:00_1_900", friendly_name="ArloBabyA0 Humidity", capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, @@ -62,7 +62,7 @@ async def test_arlo_baby_setup(hass): ), EntityTestInfo( entity_id="sensor.arlobabya0_temperature", - unique_id="homekit-00A0000000000-1000", + unique_id="00:00:00:00:00:00_1_1000", friendly_name="ArloBabyA0 Temperature", capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=TEMP_CELSIUS, @@ -70,14 +70,14 @@ async def test_arlo_baby_setup(hass): ), EntityTestInfo( entity_id="sensor.arlobabya0_air_quality", - unique_id="homekit-00A0000000000-aid:1-sid:800-cid:802", + unique_id="00:00:00:00:00:00_1_800_802", capabilities={"state_class": SensorStateClass.MEASUREMENT}, friendly_name="ArloBabyA0 Air Quality", state="1", ), EntityTestInfo( entity_id="light.arlobabya0_nightlight", - unique_id="homekit-00A0000000000-1100", + unique_id="00:00:00:00:00:00_1_1100", friendly_name="ArloBabyA0 Nightlight", supported_features=0, capabilities={"supported_color_modes": ["hs"]}, diff --git a/tests/components/homekit_controller/specific_devices/test_connectsense.py b/tests/components/homekit_controller/specific_devices/test_connectsense.py index 9e233ebdc10..096ed39a336 100644 --- a/tests/components/homekit_controller/specific_devices/test_connectsense.py +++ b/tests/components/homekit_controller/specific_devices/test_connectsense.py @@ -37,7 +37,7 @@ async def test_connectsense_setup(hass): EntityTestInfo( entity_id="sensor.inwall_outlet_0394de_current", friendly_name="InWall Outlet-0394DE Current", - unique_id="homekit-1020301376-aid:1-sid:13-cid:18", + unique_id="00:00:00:00:00:00_1_13_18", capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state="0.03", @@ -45,7 +45,7 @@ async def test_connectsense_setup(hass): EntityTestInfo( entity_id="sensor.inwall_outlet_0394de_power", friendly_name="InWall Outlet-0394DE Power", - unique_id="homekit-1020301376-aid:1-sid:13-cid:19", + unique_id="00:00:00:00:00:00_1_13_19", capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=POWER_WATT, state="0.8", @@ -53,7 +53,7 @@ async def test_connectsense_setup(hass): EntityTestInfo( entity_id="sensor.inwall_outlet_0394de_energy_kwh", friendly_name="InWall Outlet-0394DE Energy kWh", - unique_id="homekit-1020301376-aid:1-sid:13-cid:20", + unique_id="00:00:00:00:00:00_1_13_20", capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=ENERGY_KILO_WATT_HOUR, state="379.69299", @@ -61,13 +61,13 @@ async def test_connectsense_setup(hass): EntityTestInfo( entity_id="switch.inwall_outlet_0394de_outlet_a", friendly_name="InWall Outlet-0394DE Outlet A", - unique_id="homekit-1020301376-13", + unique_id="00:00:00:00:00:00_1_13", state="on", ), EntityTestInfo( entity_id="sensor.inwall_outlet_0394de_current_2", friendly_name="InWall Outlet-0394DE Current", - unique_id="homekit-1020301376-aid:1-sid:25-cid:30", + unique_id="00:00:00:00:00:00_1_25_30", capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state="0.05", @@ -75,7 +75,7 @@ async def test_connectsense_setup(hass): EntityTestInfo( entity_id="sensor.inwall_outlet_0394de_power_2", friendly_name="InWall Outlet-0394DE Power", - unique_id="homekit-1020301376-aid:1-sid:25-cid:31", + unique_id="00:00:00:00:00:00_1_25_31", capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=POWER_WATT, state="0.8", @@ -83,7 +83,7 @@ async def test_connectsense_setup(hass): EntityTestInfo( entity_id="sensor.inwall_outlet_0394de_energy_kwh_2", friendly_name="InWall Outlet-0394DE Energy kWh", - unique_id="homekit-1020301376-aid:1-sid:25-cid:32", + unique_id="00:00:00:00:00:00_1_25_32", capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=ENERGY_KILO_WATT_HOUR, state="175.85001", @@ -91,7 +91,7 @@ async def test_connectsense_setup(hass): EntityTestInfo( entity_id="switch.inwall_outlet_0394de_outlet_b", friendly_name="InWall Outlet-0394DE Outlet B", - unique_id="homekit-1020301376-25", + unique_id="00:00:00:00:00:00_1_25", state="on", ), ], diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 69a7d4f809c..299b8d24a9b 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -60,7 +60,7 @@ async def test_ecobee3_setup(hass): EntityTestInfo( entity_id="binary_sensor.kitchen", friendly_name="Kitchen", - unique_id="homekit-AB1C-56", + unique_id="00:00:00:00:00:00_2_56", state="off", ), ], @@ -78,7 +78,7 @@ async def test_ecobee3_setup(hass): EntityTestInfo( entity_id="binary_sensor.porch", friendly_name="Porch", - unique_id="homekit-AB2C-56", + unique_id="00:00:00:00:00:00_3_56", state="off", ), ], @@ -96,7 +96,7 @@ async def test_ecobee3_setup(hass): EntityTestInfo( entity_id="binary_sensor.basement", friendly_name="Basement", - unique_id="homekit-AB3C-56", + unique_id="00:00:00:00:00:00_4_56", state="off", ), ], @@ -106,7 +106,7 @@ async def test_ecobee3_setup(hass): EntityTestInfo( entity_id="climate.homew", friendly_name="HomeW", - unique_id="homekit-123456789012-16", + unique_id="00:00:00:00:00:00_1_16", supported_features=( SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE @@ -124,7 +124,7 @@ async def test_ecobee3_setup(hass): EntityTestInfo( entity_id="sensor.homew_current_temperature", friendly_name="HomeW Current Temperature", - unique_id="homekit-123456789012-aid:1-sid:16-cid:19", + unique_id="00:00:00:00:00:00_1_16_19", capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=TEMP_CELSIUS, state="21.8", @@ -132,7 +132,7 @@ async def test_ecobee3_setup(hass): EntityTestInfo( entity_id="select.homew_current_mode", friendly_name="HomeW Current Mode", - unique_id="homekit-123456789012-aid:1-sid:16-cid:33", + unique_id="00:00:00:00:00:00_1_16_33", capabilities={"options": ["home", "sleep", "away"]}, state="home", ), @@ -164,16 +164,16 @@ async def test_ecobee3_setup_from_cache(hass, hass_storage): entity_registry = er.async_get(hass) climate = entity_registry.async_get("climate.homew") - assert climate.unique_id == "homekit-123456789012-16" + assert climate.unique_id == "00:00:00:00:00:00_1_16" occ1 = entity_registry.async_get("binary_sensor.kitchen") - assert occ1.unique_id == "homekit-AB1C-56" + assert occ1.unique_id == "00:00:00:00:00:00_2_56" occ2 = entity_registry.async_get("binary_sensor.porch") - assert occ2.unique_id == "homekit-AB2C-56" + assert occ2.unique_id == "00:00:00:00:00:00_3_56" occ3 = entity_registry.async_get("binary_sensor.basement") - assert occ3.unique_id == "homekit-AB3C-56" + assert occ3.unique_id == "00:00:00:00:00:00_4_56" async def test_ecobee3_setup_connection_failure(hass): @@ -204,16 +204,16 @@ async def test_ecobee3_setup_connection_failure(hass): await time_changed(hass, 5 * 60) climate = entity_registry.async_get("climate.homew") - assert climate.unique_id == "homekit-123456789012-16" + assert climate.unique_id == "00:00:00:00:00:00_1_16" occ1 = entity_registry.async_get("binary_sensor.kitchen") - assert occ1.unique_id == "homekit-AB1C-56" + assert occ1.unique_id == "00:00:00:00:00:00_2_56" occ2 = entity_registry.async_get("binary_sensor.porch") - assert occ2.unique_id == "homekit-AB2C-56" + assert occ2.unique_id == "00:00:00:00:00:00_3_56" occ3 = entity_registry.async_get("binary_sensor.basement") - assert occ3.unique_id == "homekit-AB3C-56" + assert occ3.unique_id == "00:00:00:00:00:00_4_56" async def test_ecobee3_add_sensors_at_runtime(hass): @@ -226,7 +226,7 @@ async def test_ecobee3_add_sensors_at_runtime(hass): await setup_test_accessories(hass, accessories) climate = entity_registry.async_get("climate.homew") - assert climate.unique_id == "homekit-123456789012-16" + assert climate.unique_id == "00:00:00:00:00:00_1_16" occ1 = entity_registry.async_get("binary_sensor.kitchen") assert occ1 is None @@ -243,10 +243,10 @@ async def test_ecobee3_add_sensors_at_runtime(hass): await device_config_changed(hass, accessories) occ1 = entity_registry.async_get("binary_sensor.kitchen") - assert occ1.unique_id == "homekit-AB1C-56" + assert occ1.unique_id == "00:00:00:00:00:00_2_56" occ2 = entity_registry.async_get("binary_sensor.porch") - assert occ2.unique_id == "homekit-AB2C-56" + assert occ2.unique_id == "00:00:00:00:00:00_3_56" occ3 = entity_registry.async_get("binary_sensor.basement") - assert occ3.unique_id == "homekit-AB3C-56" + assert occ3.unique_id == "00:00:00:00:00:00_4_56" diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee_501.py b/tests/components/homekit_controller/specific_devices/test_ecobee_501.py index cf498a61e81..3d508df3a9e 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee_501.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee_501.py @@ -39,7 +39,7 @@ async def test_ecobee501_setup(hass): EntityTestInfo( entity_id="climate.my_ecobee", friendly_name="My ecobee", - unique_id="homekit-123456789016-16", + unique_id="00:00:00:00:00:00_1_16", supported_features=( SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE @@ -59,7 +59,7 @@ async def test_ecobee501_setup(hass): EntityTestInfo( entity_id="binary_sensor.my_ecobee_occupancy", friendly_name="My ecobee Occupancy", - unique_id="homekit-123456789016-57", + unique_id="00:00:00:00:00:00_1_57", unit_of_measurement=None, state=STATE_ON, ), diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py b/tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py index 20dae666c69..88220279b0c 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee_occupancy.py @@ -34,7 +34,7 @@ async def test_ecobee_occupancy_setup(hass): EntityTestInfo( entity_id="binary_sensor.master_fan", friendly_name="Master Fan", - unique_id="homekit-111111111111-56", + unique_id="00:00:00:00:00:00_1_56", state="off", ), ], diff --git a/tests/components/homekit_controller/specific_devices/test_eve_degree.py b/tests/components/homekit_controller/specific_devices/test_eve_degree.py index eab2de030db..c1a73dc37fa 100644 --- a/tests/components/homekit_controller/specific_devices/test_eve_degree.py +++ b/tests/components/homekit_controller/specific_devices/test_eve_degree.py @@ -34,7 +34,7 @@ async def test_eve_degree_setup(hass): entities=[ EntityTestInfo( entity_id="sensor.eve_degree_aa11_temperature", - unique_id="homekit-AA00A0A00000-22", + unique_id="00:00:00:00:00:00_1_22", friendly_name="Eve Degree AA11 Temperature", capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=TEMP_CELSIUS, @@ -42,7 +42,7 @@ async def test_eve_degree_setup(hass): ), EntityTestInfo( entity_id="sensor.eve_degree_aa11_humidity", - unique_id="homekit-AA00A0A00000-27", + unique_id="00:00:00:00:00:00_1_27", friendly_name="Eve Degree AA11 Humidity", capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, @@ -50,7 +50,7 @@ async def test_eve_degree_setup(hass): ), EntityTestInfo( entity_id="sensor.eve_degree_aa11_air_pressure", - unique_id="homekit-AA00A0A00000-aid:1-sid:30-cid:32", + unique_id="00:00:00:00:00:00_1_30_32", friendly_name="Eve Degree AA11 Air Pressure", unit_of_measurement=PRESSURE_HPA, capabilities={"state_class": SensorStateClass.MEASUREMENT}, @@ -58,7 +58,7 @@ async def test_eve_degree_setup(hass): ), EntityTestInfo( entity_id="sensor.eve_degree_aa11_battery", - unique_id="homekit-AA00A0A00000-17", + unique_id="00:00:00:00:00:00_1_17", friendly_name="Eve Degree AA11 Battery", entity_category=EntityCategory.DIAGNOSTIC, capabilities={"state_class": SensorStateClass.MEASUREMENT}, @@ -67,7 +67,7 @@ async def test_eve_degree_setup(hass): ), EntityTestInfo( entity_id="number.eve_degree_aa11_elevation", - unique_id="homekit-AA00A0A00000-aid:1-sid:30-cid:33", + unique_id="00:00:00:00:00:00_1_30_33", friendly_name="Eve Degree AA11 Elevation", capabilities={ "max": 9000, diff --git a/tests/components/homekit_controller/specific_devices/test_eve_energy.py b/tests/components/homekit_controller/specific_devices/test_eve_energy.py index 292ab9c66ac..65e5c16179f 100644 --- a/tests/components/homekit_controller/specific_devices/test_eve_energy.py +++ b/tests/components/homekit_controller/specific_devices/test_eve_energy.py @@ -19,7 +19,7 @@ from ..common import ( ) -async def test_eve_degree_setup(hass): +async def test_eve_energy_setup(hass): """Test that the accessory can be correctly setup in HA.""" accessories = await setup_accessories_from_file(hass, "eve_energy.json") await setup_test_accessories(hass, accessories) @@ -38,13 +38,13 @@ async def test_eve_degree_setup(hass): entities=[ EntityTestInfo( entity_id="switch.eve_energy_50ff", - unique_id="homekit-AA00A0A00000-28", + unique_id="00:00:00:00:00:00_1_28", friendly_name="Eve Energy 50FF", state="off", ), EntityTestInfo( entity_id="sensor.eve_energy_50ff_amps", - unique_id="homekit-AA00A0A00000-aid:1-sid:28-cid:33", + unique_id="00:00:00:00:00:00_1_28_33", friendly_name="Eve Energy 50FF Amps", unit_of_measurement=ELECTRIC_CURRENT_AMPERE, capabilities={"state_class": SensorStateClass.MEASUREMENT}, @@ -52,7 +52,7 @@ async def test_eve_degree_setup(hass): ), EntityTestInfo( entity_id="sensor.eve_energy_50ff_volts", - unique_id="homekit-AA00A0A00000-aid:1-sid:28-cid:32", + unique_id="00:00:00:00:00:00_1_28_32", friendly_name="Eve Energy 50FF Volts", unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, capabilities={"state_class": SensorStateClass.MEASUREMENT}, @@ -60,7 +60,7 @@ async def test_eve_degree_setup(hass): ), EntityTestInfo( entity_id="sensor.eve_energy_50ff_power", - unique_id="homekit-AA00A0A00000-aid:1-sid:28-cid:34", + unique_id="00:00:00:00:00:00_1_28_34", friendly_name="Eve Energy 50FF Power", unit_of_measurement=POWER_WATT, capabilities={"state_class": SensorStateClass.MEASUREMENT}, @@ -68,7 +68,7 @@ async def test_eve_degree_setup(hass): ), EntityTestInfo( entity_id="sensor.eve_energy_50ff_energy_kwh", - unique_id="homekit-AA00A0A00000-aid:1-sid:28-cid:35", + unique_id="00:00:00:00:00:00_1_28_35", friendly_name="Eve Energy 50FF Energy kWh", capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=ENERGY_KILO_WATT_HOUR, @@ -76,14 +76,14 @@ async def test_eve_degree_setup(hass): ), EntityTestInfo( entity_id="switch.eve_energy_50ff_lock_physical_controls", - unique_id="homekit-AA00A0A00000-aid:1-sid:28-cid:36", + unique_id="00:00:00:00:00:00_1_28_36", friendly_name="Eve Energy 50FF Lock Physical Controls", entity_category=EntityCategory.CONFIG, state="off", ), EntityTestInfo( entity_id="button.eve_energy_50ff_identify", - unique_id="homekit-AA00A0A00000-aid:1-sid:1-cid:3", + unique_id="00:00:00:00:00:00_1_1_3", friendly_name="Eve Energy 50FF Identify", entity_category=EntityCategory.DIAGNOSTIC, state="unknown", diff --git a/tests/components/homekit_controller/specific_devices/test_haa_fan.py b/tests/components/homekit_controller/specific_devices/test_haa_fan.py index 2f01a2c404e..33eb5e24979 100644 --- a/tests/components/homekit_controller/specific_devices/test_haa_fan.py +++ b/tests/components/homekit_controller/specific_devices/test_haa_fan.py @@ -46,7 +46,7 @@ async def test_haa_fan_setup(hass): EntityTestInfo( entity_id="switch.haa_c718b3", friendly_name="HAA-C718B3", - unique_id="homekit-C718B3-2-8", + unique_id="00:00:00:00:00:00_2_8", state="off", ) ], @@ -56,7 +56,7 @@ async def test_haa_fan_setup(hass): EntityTestInfo( entity_id="fan.haa_c718b3", friendly_name="HAA-C718B3", - unique_id="homekit-C718B3-1-8", + unique_id="00:00:00:00:00:00_1_8", state="on", supported_features=FanEntityFeature.SET_SPEED, capabilities={ @@ -66,14 +66,14 @@ async def test_haa_fan_setup(hass): EntityTestInfo( entity_id="button.haa_c718b3_setup", friendly_name="HAA-C718B3 Setup", - unique_id="homekit-C718B3-1-aid:1-sid:1010-cid:1012", + unique_id="00:00:00:00:00:00_1_1010_1012", entity_category=EntityCategory.CONFIG, state="unknown", ), EntityTestInfo( entity_id="button.haa_c718b3_update", friendly_name="HAA-C718B3 Update", - unique_id="homekit-C718B3-1-aid:1-sid:1010-cid:1011", + unique_id="00:00:00:00:00:00_1_1010_1011", entity_category=EntityCategory.CONFIG, state="unknown", ), diff --git a/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py b/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py index 175e534f639..6848f4079b0 100644 --- a/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py @@ -43,7 +43,7 @@ async def test_homeassistant_bridge_fan_setup(hass): EntityTestInfo( entity_id="fan.living_room_fan", friendly_name="Living Room Fan", - unique_id="homekit-fan.living_room_fan-8", + unique_id="00:00:00:00:00:00_1256851357_8", supported_features=( FanEntityFeature.DIRECTION | FanEntityFeature.SET_SPEED 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 1092bb4f82c..361bfbfe178 100644 --- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py @@ -46,7 +46,7 @@ async def test_hue_bridge_setup(hass): capabilities={"state_class": SensorStateClass.MEASUREMENT}, friendly_name="Hue dimmer switch battery", entity_category=EntityCategory.DIAGNOSTIC, - unique_id="homekit-6623462389072572-644245094400", + unique_id="00:00:00:00:00:00_6623462389072572_644245094400", unit_of_measurement=PERCENTAGE, state="100", ) diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py index 99f34491e86..2e3102d8f13 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py @@ -46,7 +46,7 @@ async def test_koogeek_ls1_setup(hass): EntityTestInfo( entity_id="light.koogeek_ls1_20833f_light_strip", friendly_name="Koogeek-LS1-20833F Light Strip", - unique_id="homekit-AAAA011111111111-7", + unique_id="00:00:00:00:00:00_1_7", supported_features=0, capabilities={"supported_color_modes": ["hs"]}, state="off", @@ -54,7 +54,7 @@ async def test_koogeek_ls1_setup(hass): EntityTestInfo( entity_id="button.koogeek_ls1_20833f_identify", friendly_name="Koogeek-LS1-20833F Identify", - unique_id="homekit-AAAA011111111111-aid:1-sid:1-cid:6", + unique_id="00:00:00:00:00:00_1_1_6", entity_category=EntityCategory.DIAGNOSTIC, state="unknown", ), diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py index 7df1cee54d5..ee8c273904c 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py @@ -33,13 +33,13 @@ async def test_koogeek_p1eu_setup(hass): EntityTestInfo( entity_id="switch.koogeek_p1_a00aa0_outlet", friendly_name="Koogeek-P1-A00AA0 outlet", - unique_id="homekit-EUCP03190xxxxx48-7", + unique_id="00:00:00:00:00:00_1_7", state="off", ), EntityTestInfo( entity_id="sensor.koogeek_p1_a00aa0_power", friendly_name="Koogeek-P1-A00AA0 Power", - unique_id="homekit-EUCP03190xxxxx48-aid:1-sid:21-cid:22", + unique_id="00:00:00:00:00:00_1_21_22", unit_of_measurement=POWER_WATT, capabilities={"state_class": SensorStateClass.MEASUREMENT}, state="5", diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py index 210fec0aafc..91edf91156a 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py @@ -39,19 +39,19 @@ async def test_koogeek_sw2_setup(hass): EntityTestInfo( entity_id="switch.koogeek_sw2_187a91_switch_1", friendly_name="Koogeek-SW2-187A91 Switch 1", - unique_id="homekit-CNNT061751001372-8", + unique_id="00:00:00:00:00:00_1_8", state="off", ), EntityTestInfo( entity_id="switch.koogeek_sw2_187a91_switch_2", friendly_name="Koogeek-SW2-187A91 Switch 2", - unique_id="homekit-CNNT061751001372-11", + unique_id="00:00:00:00:00:00_1_11", state="off", ), EntityTestInfo( entity_id="sensor.koogeek_sw2_187a91_power", friendly_name="Koogeek-SW2-187A91 Power", - unique_id="homekit-CNNT061751001372-aid:1-sid:14-cid:18", + unique_id="00:00:00:00:00:00_1_14_18", unit_of_measurement=POWER_WATT, capabilities={"state_class": SensorStateClass.MEASUREMENT}, state="0", diff --git a/tests/components/homekit_controller/specific_devices/test_lennox_e30.py b/tests/components/homekit_controller/specific_devices/test_lennox_e30.py index 1bb31241023..fb1c0d183d3 100644 --- a/tests/components/homekit_controller/specific_devices/test_lennox_e30.py +++ b/tests/components/homekit_controller/specific_devices/test_lennox_e30.py @@ -39,7 +39,7 @@ async def test_lennox_e30_setup(hass): EntityTestInfo( entity_id="climate.lennox", friendly_name="Lennox", - unique_id="homekit-XXXXXXXX-100", + unique_id="00:00:00:00:00:00_1_100", supported_features=( SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE ), 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 22d29f7500d..4af74e0cd86 100644 --- a/tests/components/homekit_controller/specific_devices/test_lg_tv.py +++ b/tests/components/homekit_controller/specific_devices/test_lg_tv.py @@ -36,7 +36,7 @@ async def test_lg_tv(hass): EntityTestInfo( entity_id="media_player.lg_webos_tv_af80", friendly_name="LG webOS TV AF80", - unique_id="homekit-999AAAAAA999-48", + unique_id="00:00:00:00:00:00_1_48", supported_features=( SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_SELECT_SOURCE ), diff --git a/tests/components/homekit_controller/specific_devices/test_lutron_caseta_bridge.py b/tests/components/homekit_controller/specific_devices/test_lutron_caseta_bridge.py index 9df8cf4e5ae..76c5bc70bff 100644 --- a/tests/components/homekit_controller/specific_devices/test_lutron_caseta_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_lutron_caseta_bridge.py @@ -41,7 +41,7 @@ async def test_lutron_caseta_bridge_setup(hass): EntityTestInfo( entity_id="fan.caseta_r_wireless_fan_speed_control", friendly_name="Caséta® Wireless Fan Speed Control", - unique_id="homekit-39024290-2", + unique_id="00:00:00:00:00:00_21474836482_2", unit_of_measurement=None, supported_features=1, state=STATE_OFF, diff --git a/tests/components/homekit_controller/specific_devices/test_mss425f.py b/tests/components/homekit_controller/specific_devices/test_mss425f.py index 6db4140bd75..86d8ebeca71 100644 --- a/tests/components/homekit_controller/specific_devices/test_mss425f.py +++ b/tests/components/homekit_controller/specific_devices/test_mss425f.py @@ -34,38 +34,38 @@ async def test_meross_mss425f_setup(hass): EntityTestInfo( entity_id="button.mss425f_15cc_identify", friendly_name="MSS425F-15cc Identify", - unique_id="homekit-HH41234-aid:1-sid:1-cid:2", + unique_id="00:00:00:00:00:00_1_1_2", entity_category=EntityCategory.DIAGNOSTIC, state=STATE_UNKNOWN, ), EntityTestInfo( entity_id="switch.mss425f_15cc_outlet_1", friendly_name="MSS425F-15cc Outlet-1", - unique_id="homekit-HH41234-12", + unique_id="00:00:00:00:00:00_1_12", state=STATE_ON, ), EntityTestInfo( entity_id="switch.mss425f_15cc_outlet_2", friendly_name="MSS425F-15cc Outlet-2", - unique_id="homekit-HH41234-15", + unique_id="00:00:00:00:00:00_1_15", state=STATE_ON, ), EntityTestInfo( entity_id="switch.mss425f_15cc_outlet_3", friendly_name="MSS425F-15cc Outlet-3", - unique_id="homekit-HH41234-18", + unique_id="00:00:00:00:00:00_1_18", state=STATE_ON, ), EntityTestInfo( entity_id="switch.mss425f_15cc_outlet_4", friendly_name="MSS425F-15cc Outlet-4", - unique_id="homekit-HH41234-21", + unique_id="00:00:00:00:00:00_1_21", state=STATE_ON, ), EntityTestInfo( entity_id="switch.mss425f_15cc_usb", friendly_name="MSS425F-15cc USB", - unique_id="homekit-HH41234-24", + unique_id="00:00:00:00:00:00_1_24", state=STATE_ON, ), ], diff --git a/tests/components/homekit_controller/specific_devices/test_mss565.py b/tests/components/homekit_controller/specific_devices/test_mss565.py index 1a9c5bbbf6f..5140b563a9a 100644 --- a/tests/components/homekit_controller/specific_devices/test_mss565.py +++ b/tests/components/homekit_controller/specific_devices/test_mss565.py @@ -33,7 +33,7 @@ async def test_meross_mss565_setup(hass): EntityTestInfo( entity_id="light.mss565_28da_dimmer_switch", friendly_name="MSS565-28da Dimmer Switch", - unique_id="homekit-BB1121-12", + unique_id="00:00:00:00:00:00_1_12", capabilities={"supported_color_modes": ["brightness"]}, state=STATE_ON, ), diff --git a/tests/components/homekit_controller/specific_devices/test_mysa_living.py b/tests/components/homekit_controller/specific_devices/test_mysa_living.py index a5abe4ad2e7..83404d9dd99 100644 --- a/tests/components/homekit_controller/specific_devices/test_mysa_living.py +++ b/tests/components/homekit_controller/specific_devices/test_mysa_living.py @@ -34,7 +34,7 @@ async def test_mysa_living_setup(hass): EntityTestInfo( entity_id="climate.mysa_85dda9_thermostat", friendly_name="Mysa-85dda9 Thermostat", - unique_id="homekit-AAAAAAA000-20", + unique_id="00:00:00:00:00:00_1_20", supported_features=ClimateEntityFeature.TARGET_TEMPERATURE, capabilities={ "hvac_modes": ["off", "heat", "cool", "heat_cool"], @@ -46,7 +46,7 @@ async def test_mysa_living_setup(hass): EntityTestInfo( entity_id="sensor.mysa_85dda9_current_humidity", friendly_name="Mysa-85dda9 Current Humidity", - unique_id="homekit-AAAAAAA000-aid:1-sid:20-cid:27", + unique_id="00:00:00:00:00:00_1_20_27", unit_of_measurement=PERCENTAGE, capabilities={"state_class": SensorStateClass.MEASUREMENT}, state="40", @@ -54,7 +54,7 @@ async def test_mysa_living_setup(hass): EntityTestInfo( entity_id="sensor.mysa_85dda9_current_temperature", friendly_name="Mysa-85dda9 Current Temperature", - unique_id="homekit-AAAAAAA000-aid:1-sid:20-cid:25", + unique_id="00:00:00:00:00:00_1_20_25", unit_of_measurement=TEMP_CELSIUS, capabilities={"state_class": SensorStateClass.MEASUREMENT}, state="24.1", @@ -62,7 +62,7 @@ async def test_mysa_living_setup(hass): EntityTestInfo( entity_id="light.mysa_85dda9_display", friendly_name="Mysa-85dda9 Display", - unique_id="homekit-AAAAAAA000-40", + unique_id="00:00:00:00:00:00_1_40", supported_features=0, capabilities={"supported_color_modes": ["brightness"]}, state="off", diff --git a/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py index 61d872ccd2a..4afb61b19f3 100644 --- a/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py +++ b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py @@ -34,7 +34,7 @@ async def test_nanoleaf_nl55_setup(hass): EntityTestInfo( entity_id="light.nanoleaf_strip_3b32_nanoleaf_light_strip", friendly_name="Nanoleaf Strip 3B32 Nanoleaf Light Strip", - unique_id="homekit-AAAA011111111111-19", + unique_id="00:00:00:00:00:00_1_19", supported_features=0, capabilities={ "max_color_temp_kelvin": 6535, @@ -48,21 +48,21 @@ async def test_nanoleaf_nl55_setup(hass): EntityTestInfo( entity_id="button.nanoleaf_strip_3b32_identify", friendly_name="Nanoleaf Strip 3B32 Identify", - unique_id="homekit-AAAA011111111111-aid:1-sid:1-cid:2", + unique_id="00:00:00:00:00:00_1_1_2", entity_category=EntityCategory.DIAGNOSTIC, state="unknown", ), EntityTestInfo( entity_id="sensor.nanoleaf_strip_3b32_thread_capabilities", friendly_name="Nanoleaf Strip 3B32 Thread Capabilities", - unique_id="homekit-AAAA011111111111-aid:1-sid:31-cid:115", + unique_id="00:00:00:00:00:00_1_31_115", entity_category=EntityCategory.DIAGNOSTIC, state="border_router_capable", ), EntityTestInfo( entity_id="sensor.nanoleaf_strip_3b32_thread_status", friendly_name="Nanoleaf Strip 3B32 Thread Status", - unique_id="homekit-AAAA011111111111-aid:1-sid:31-cid:117", + unique_id="00:00:00:00:00:00_1_31_117", entity_category=EntityCategory.DIAGNOSTIC, state="border_router", ), diff --git a/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py b/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py index 188bbaffedd..9ff84c45701 100644 --- a/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py +++ b/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py @@ -35,7 +35,7 @@ async def test_netamo_doorbell_setup(hass): EntityTestInfo( entity_id="camera.netatmo_doorbell_g738658", friendly_name="Netatmo-Doorbell-g738658", - unique_id="homekit-g738658-aid:1", + unique_id="00:00:00:00:00:00_1", state="idle", ), ], diff --git a/tests/components/homekit_controller/specific_devices/test_netamo_smart_co_alarm.py b/tests/components/homekit_controller/specific_devices/test_netamo_smart_co_alarm.py index b2c83a005f8..3f46ffdc9fa 100644 --- a/tests/components/homekit_controller/specific_devices/test_netamo_smart_co_alarm.py +++ b/tests/components/homekit_controller/specific_devices/test_netamo_smart_co_alarm.py @@ -35,14 +35,14 @@ async def test_netamo_smart_co_alarm_setup(hass): EntityTestInfo( entity_id="binary_sensor.smart_co_alarm_carbon_monoxide_sensor", friendly_name="Smart CO Alarm Carbon Monoxide Sensor", - unique_id="homekit-1234-22", + unique_id="00:00:00:00:00:00_1_22", state="off", ), EntityTestInfo( entity_id="binary_sensor.smart_co_alarm_low_battery", friendly_name="Smart CO Alarm Low Battery", entity_category=EntityCategory.DIAGNOSTIC, - unique_id="homekit-1234-36", + unique_id="00:00:00:00:00:00_1_36", state="off", ), ], diff --git a/tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py b/tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py index ecea2cdafbb..c93493f38b5 100644 --- a/tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py +++ b/tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py @@ -34,49 +34,49 @@ async def test_rainmachine_pro_8_setup(hass): EntityTestInfo( entity_id="switch.rainmachine_00ce4a", friendly_name="RainMachine-00ce4a", - unique_id="homekit-00aa0000aa0a-512", + unique_id="00:00:00:00:00:00_1_512", state="off", ), EntityTestInfo( entity_id="switch.rainmachine_00ce4a_2", friendly_name="RainMachine-00ce4a", - unique_id="homekit-00aa0000aa0a-768", + unique_id="00:00:00:00:00:00_1_768", state="off", ), EntityTestInfo( entity_id="switch.rainmachine_00ce4a_3", friendly_name="RainMachine-00ce4a", - unique_id="homekit-00aa0000aa0a-1024", + unique_id="00:00:00:00:00:00_1_1024", state="off", ), EntityTestInfo( entity_id="switch.rainmachine_00ce4a_4", friendly_name="RainMachine-00ce4a", - unique_id="homekit-00aa0000aa0a-1280", + unique_id="00:00:00:00:00:00_1_1280", state="off", ), EntityTestInfo( entity_id="switch.rainmachine_00ce4a_5", friendly_name="RainMachine-00ce4a", - unique_id="homekit-00aa0000aa0a-1536", + unique_id="00:00:00:00:00:00_1_1536", state="off", ), EntityTestInfo( entity_id="switch.rainmachine_00ce4a_6", friendly_name="RainMachine-00ce4a", - unique_id="homekit-00aa0000aa0a-1792", + unique_id="00:00:00:00:00:00_1_1792", state="off", ), EntityTestInfo( entity_id="switch.rainmachine_00ce4a_7", friendly_name="RainMachine-00ce4a", - unique_id="homekit-00aa0000aa0a-2048", + unique_id="00:00:00:00:00:00_1_2048", state="off", ), EntityTestInfo( entity_id="switch.rainmachine_00ce4a_8", friendly_name="RainMachine-00ce4a", - unique_id="homekit-00aa0000aa0a-2304", + unique_id="00:00:00:00:00:00_1_2304", state="off", ), ], diff --git a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py index a0c84472429..1e572683ce3 100644 --- a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py @@ -47,7 +47,7 @@ async def test_ryse_smart_bridge_setup(hass): EntityTestInfo( entity_id="cover.master_bath_south_ryse_shade", friendly_name="Master Bath South RYSE Shade", - unique_id="homekit-00:00:00:00:00:00-2-48", + unique_id="00:00:00:00:00:00_2_48", supported_features=RYSE_SUPPORTED_FEATURES, state="closed", ), @@ -56,7 +56,7 @@ async def test_ryse_smart_bridge_setup(hass): friendly_name="Master Bath South RYSE Shade Battery", entity_category=EntityCategory.DIAGNOSTIC, capabilities={"state_class": SensorStateClass.MEASUREMENT}, - unique_id="homekit-00:00:00:00:00:00-2-64", + unique_id="00:00:00:00:00:00_2_64", unit_of_measurement=PERCENTAGE, state="100", ), @@ -75,7 +75,7 @@ async def test_ryse_smart_bridge_setup(hass): EntityTestInfo( entity_id="cover.ryse_smartshade_ryse_shade", friendly_name="RYSE SmartShade RYSE Shade", - unique_id="homekit-00:00:00:00:00:00-3-48", + unique_id="00:00:00:00:00:00_3_48", supported_features=RYSE_SUPPORTED_FEATURES, state="open", ), @@ -84,7 +84,7 @@ async def test_ryse_smart_bridge_setup(hass): friendly_name="RYSE SmartShade RYSE Shade Battery", entity_category=EntityCategory.DIAGNOSTIC, capabilities={"state_class": SensorStateClass.MEASUREMENT}, - unique_id="homekit-00:00:00:00:00:00-3-64", + unique_id="00:00:00:00:00:00_3_64", unit_of_measurement=PERCENTAGE, state="100", ), @@ -126,7 +126,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): EntityTestInfo( entity_id="cover.lr_left_ryse_shade", friendly_name="LR Left RYSE Shade", - unique_id="homekit-00:00:00:00:00:00-2-48", + unique_id="00:00:00:00:00:00_2_48", supported_features=RYSE_SUPPORTED_FEATURES, state="closed", ), @@ -135,7 +135,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): friendly_name="LR Left RYSE Shade Battery", entity_category=EntityCategory.DIAGNOSTIC, capabilities={"state_class": SensorStateClass.MEASUREMENT}, - unique_id="homekit-00:00:00:00:00:00-2-64", + unique_id="00:00:00:00:00:00_2_64", unit_of_measurement=PERCENTAGE, state="89", ), @@ -154,7 +154,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): EntityTestInfo( entity_id="cover.lr_right_ryse_shade", friendly_name="LR Right RYSE Shade", - unique_id="homekit-00:00:00:00:00:00-3-48", + unique_id="00:00:00:00:00:00_3_48", supported_features=RYSE_SUPPORTED_FEATURES, state="closed", ), @@ -163,7 +163,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): friendly_name="LR Right RYSE Shade Battery", entity_category=EntityCategory.DIAGNOSTIC, capabilities={"state_class": SensorStateClass.MEASUREMENT}, - unique_id="homekit-00:00:00:00:00:00-3-64", + unique_id="00:00:00:00:00:00_3_64", unit_of_measurement=PERCENTAGE, state="100", ), @@ -182,7 +182,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): EntityTestInfo( entity_id="cover.br_left_ryse_shade", friendly_name="BR Left RYSE Shade", - unique_id="homekit-00:00:00:00:00:00-4-48", + unique_id="00:00:00:00:00:00_4_48", supported_features=RYSE_SUPPORTED_FEATURES, state="open", ), @@ -191,7 +191,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): friendly_name="BR Left RYSE Shade Battery", entity_category=EntityCategory.DIAGNOSTIC, capabilities={"state_class": SensorStateClass.MEASUREMENT}, - unique_id="homekit-00:00:00:00:00:00-4-64", + unique_id="00:00:00:00:00:00_4_64", unit_of_measurement=PERCENTAGE, state="100", ), @@ -210,7 +210,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): EntityTestInfo( entity_id="cover.rzss_ryse_shade", friendly_name="RZSS RYSE Shade", - unique_id="homekit-00:00:00:00:00:00-5-48", + unique_id="00:00:00:00:00:00_5_48", supported_features=RYSE_SUPPORTED_FEATURES, state="open", ), @@ -219,7 +219,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): entity_category=EntityCategory.DIAGNOSTIC, capabilities={"state_class": SensorStateClass.MEASUREMENT}, friendly_name="RZSS RYSE Shade Battery", - unique_id="homekit-00:00:00:00:00:00-5-64", + unique_id="00:00:00:00:00:00_5_64", unit_of_measurement=PERCENTAGE, state="0", ), diff --git a/tests/components/homekit_controller/specific_devices/test_schlage_sense.py b/tests/components/homekit_controller/specific_devices/test_schlage_sense.py index 0a59ec6f70a..e1b55f6bd88 100644 --- a/tests/components/homekit_controller/specific_devices/test_schlage_sense.py +++ b/tests/components/homekit_controller/specific_devices/test_schlage_sense.py @@ -31,7 +31,7 @@ async def test_schlage_sense_setup(hass): EntityTestInfo( entity_id="lock.sense_lock_mechanism", friendly_name="SENSE Lock Mechanism", - unique_id="homekit-AAAAAAA000-30", + unique_id="00:00:00:00:00:00_1_30", supported_features=0, state="unknown", ), diff --git a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py index ba24bdeef96..9a5edeb45b2 100644 --- a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py +++ b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py @@ -36,7 +36,7 @@ async def test_simpleconnect_fan_setup(hass): EntityTestInfo( entity_id="fan.simpleconnect_fan_06f674_hunter_fan", friendly_name="SIMPLEconnect Fan-06F674 Hunter Fan", - unique_id="homekit-1234567890abcd-8", + unique_id="00:00:00:00:00:00_1_8", supported_features=FanEntityFeature.DIRECTION | FanEntityFeature.SET_SPEED, capabilities={ diff --git a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py index 8a5102e0a87..a82d995d4d1 100644 --- a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py @@ -51,7 +51,7 @@ async def test_velux_cover_setup(hass): EntityTestInfo( entity_id="cover.velux_window_roof_window", friendly_name="VELUX Window Roof Window", - unique_id="homekit-1111111a114a111a-8", + unique_id="00:00:00:00:00:00_3_8", supported_features=CoverEntityFeature.CLOSE | CoverEntityFeature.SET_POSITION | CoverEntityFeature.OPEN, @@ -73,7 +73,7 @@ async def test_velux_cover_setup(hass): entity_id="sensor.velux_sensor_temperature_sensor", friendly_name="VELUX Sensor Temperature sensor", capabilities={"state_class": SensorStateClass.MEASUREMENT}, - unique_id="homekit-a11b111-8", + unique_id="00:00:00:00:00:00_2_8", unit_of_measurement=TEMP_CELSIUS, state="18.9", ), @@ -81,7 +81,7 @@ async def test_velux_cover_setup(hass): entity_id="sensor.velux_sensor_humidity_sensor", friendly_name="VELUX Sensor Humidity sensor", capabilities={"state_class": SensorStateClass.MEASUREMENT}, - unique_id="homekit-a11b111-11", + unique_id="00:00:00:00:00:00_2_11", unit_of_measurement=PERCENTAGE, state="58", ), @@ -89,7 +89,7 @@ async def test_velux_cover_setup(hass): entity_id="sensor.velux_sensor_carbon_dioxide_sensor", friendly_name="VELUX Sensor Carbon Dioxide sensor", capabilities={"state_class": SensorStateClass.MEASUREMENT}, - unique_id="homekit-a11b111-14", + unique_id="00:00:00:00:00:00_2_14", unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state="400", ), diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py index 7c3262f3098..b07a10cf17d 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py @@ -36,7 +36,7 @@ async def test_vocolinc_flowerbud_setup(hass): EntityTestInfo( entity_id="humidifier.vocolinc_flowerbud_0d324b", friendly_name="VOCOlinc-Flowerbud-0d324b", - unique_id="homekit-AM01121849000327-30", + unique_id="00:00:00:00:00:00_1_30", supported_features=HumidifierEntityFeature.MODES, capabilities={ "available_modes": ["normal", "auto"], @@ -48,7 +48,7 @@ async def test_vocolinc_flowerbud_setup(hass): EntityTestInfo( entity_id="light.vocolinc_flowerbud_0d324b_mood_light", friendly_name="VOCOlinc-Flowerbud-0d324b Mood Light", - unique_id="homekit-AM01121849000327-9", + unique_id="00:00:00:00:00:00_1_9", supported_features=0, capabilities={"supported_color_modes": ["hs"]}, state="on", @@ -56,7 +56,7 @@ async def test_vocolinc_flowerbud_setup(hass): EntityTestInfo( entity_id="number.vocolinc_flowerbud_0d324b_spray_quantity", friendly_name="VOCOlinc-Flowerbud-0d324b Spray Quantity", - unique_id="homekit-AM01121849000327-aid:1-sid:30-cid:38", + unique_id="00:00:00:00:00:00_1_30_38", capabilities={ "max": 5, "min": 1, @@ -69,7 +69,7 @@ async def test_vocolinc_flowerbud_setup(hass): EntityTestInfo( entity_id="sensor.vocolinc_flowerbud_0d324b_current_humidity", friendly_name="VOCOlinc-Flowerbud-0d324b Current Humidity", - unique_id="homekit-AM01121849000327-aid:1-sid:30-cid:33", + unique_id="00:00:00:00:00:00_1_30_33", capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, state="45.0", diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py index 4037a44898e..7ced8979c8a 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py @@ -2,6 +2,7 @@ from homeassistant.components.sensor import SensorStateClass from homeassistant.const import POWER_WATT +from homeassistant.helpers import entity_registry as er from ..common import ( HUB_TEST_ACCESSORY_ID, @@ -15,6 +16,21 @@ from ..common import ( async def test_vocolinc_vp3_setup(hass): """Test that a VOCOlinc VP3 can be correctly setup in HA.""" + + entity_registry = er.async_get(hass) + outlet = entity_registry.async_get_or_create( + "switch", + "homekit_controller", + "homekit-EU0121203xxxxx07-48", + suggested_object_id="original_vocolinc_vp3_outlet", + ) + sensor = entity_registry.async_get_or_create( + "sensor", + "homekit_controller", + "homekit-EU0121203xxxxx07-aid:1-sid:48-cid:97", + suggested_object_id="original_vocolinc_vp3_power", + ) + accessories = await setup_accessories_from_file(hass, "vocolinc_vp3.json") await setup_test_accessories(hass, accessories) @@ -31,15 +47,15 @@ async def test_vocolinc_vp3_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="switch.vocolinc_vp3_123456_outlet", + entity_id="switch.original_vocolinc_vp3_outlet", friendly_name="VOCOlinc-VP3-123456 Outlet", - unique_id="homekit-EU0121203xxxxx07-48", + unique_id="00:00:00:00:00:00_1_48", state="on", ), EntityTestInfo( - entity_id="sensor.vocolinc_vp3_123456_power", + entity_id="sensor.original_vocolinc_vp3_power", friendly_name="VOCOlinc-VP3-123456 Power", - unique_id="homekit-EU0121203xxxxx07-aid:1-sid:48-cid:97", + unique_id="00:00:00:00:00:00_1_48_97", unit_of_measurement=POWER_WATT, capabilities={"state_class": SensorStateClass.MEASUREMENT}, state="0", @@ -47,3 +63,12 @@ async def test_vocolinc_vp3_setup(hass): ], ), ) + + assert ( + entity_registry.async_get(outlet.entity_id).unique_id + == "00:00:00:00:00:00_1_48" + ) + assert ( + entity_registry.async_get(sensor.entity_id).unique_id + == "00:00:00:00:00:00_1_48_97" + ) diff --git a/tests/components/homekit_controller/test_alarm_control_panel.py b/tests/components/homekit_controller/test_alarm_control_panel.py index 46979bd41f3..2c2ff92ccb6 100644 --- a/tests/components/homekit_controller/test_alarm_control_panel.py +++ b/tests/components/homekit_controller/test_alarm_control_panel.py @@ -2,7 +2,9 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from .common import setup_test_component +from homeassistant.helpers import entity_registry as er + +from .common import get_next_aid, setup_test_component def create_security_system_service(accessory): @@ -119,3 +121,20 @@ async def test_switch_read_alarm_state(hass, utcnow): ) state = await helper.poll_and_get_state() assert state.state == "triggered" + + +async def test_migrate_unique_id(hass, utcnow): + """Test a we can migrate a alarm_control_panel unique id.""" + entity_registry = er.async_get(hass) + aid = get_next_aid() + alarm_control_panel_entry = entity_registry.async_get_or_create( + "alarm_control_panel", + "homekit_controller", + f"homekit-00:00:00:00:00:00-{aid}-8", + ) + await setup_test_component(hass, create_security_system_service) + + assert ( + entity_registry.async_get(alarm_control_panel_entry.entity_id).unique_id + == f"00:00:00:00:00:00_{aid}_8" + ) diff --git a/tests/components/homekit_controller/test_binary_sensor.py b/tests/components/homekit_controller/test_binary_sensor.py index e9cd9284332..7e926910da1 100644 --- a/tests/components/homekit_controller/test_binary_sensor.py +++ b/tests/components/homekit_controller/test_binary_sensor.py @@ -3,8 +3,9 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.helpers import entity_registry as er -from .common import setup_test_component +from .common import get_next_aid, setup_test_component def create_motion_sensor_service(accessory): @@ -169,3 +170,20 @@ async def test_leak_sensor_read_state(hass, utcnow): assert state.state == "on" assert state.attributes["device_class"] == BinarySensorDeviceClass.MOISTURE + + +async def test_migrate_unique_id(hass, utcnow): + """Test a we can migrate a binary_sensor unique id.""" + entity_registry = er.async_get(hass) + aid = get_next_aid() + binary_sensor_entry = entity_registry.async_get_or_create( + "binary_sensor", + "homekit_controller", + f"homekit-00:00:00:00:00:00-{aid}-8", + ) + await setup_test_component(hass, create_leak_sensor_service) + + assert ( + entity_registry.async_get(binary_sensor_entry.entity_id).unique_id + == f"00:00:00:00:00:00_{aid}_8" + ) diff --git a/tests/components/homekit_controller/test_button.py b/tests/components/homekit_controller/test_button.py index 58c1feb8900..77551668ea5 100644 --- a/tests/components/homekit_controller/test_button.py +++ b/tests/components/homekit_controller/test_button.py @@ -2,7 +2,9 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from .common import Helper, setup_test_component +from homeassistant.helpers import entity_registry as er + +from .common import Helper, get_next_aid, setup_test_component def create_switch_with_setup_button(accessory): @@ -89,3 +91,19 @@ async def test_ecobee_clear_hold_press_button(hass): CharacteristicsTypes.VENDOR_ECOBEE_CLEAR_HOLD: True, }, ) + + +async def test_migrate_unique_id(hass, utcnow): + """Test a we can migrate a button unique id.""" + entity_registry = er.async_get(hass) + aid = get_next_aid() + button_entry = entity_registry.async_get_or_create( + "button", + "homekit_controller", + f"homekit-0001-aid:{aid}-sid:1-cid:2", + ) + await setup_test_component(hass, create_switch_with_ecobee_clear_hold_button) + assert ( + entity_registry.async_get(button_entry.entity_id).unique_id + == f"00:00:00:00:00:00_{aid}_1_2" + ) diff --git a/tests/components/homekit_controller/test_camera.py b/tests/components/homekit_controller/test_camera.py index e0ba609b30e..f4207ca4ca9 100644 --- a/tests/components/homekit_controller/test_camera.py +++ b/tests/components/homekit_controller/test_camera.py @@ -5,8 +5,9 @@ from aiohomekit.model.services import ServicesTypes from aiohomekit.testing import FAKE_CAMERA_IMAGE from homeassistant.components import camera +from homeassistant.helpers import entity_registry as er -from .common import setup_test_component +from .common import get_next_aid, setup_test_component def create_camera(accessory): @@ -14,6 +15,22 @@ def create_camera(accessory): accessory.add_service(ServicesTypes.CAMERA_RTP_STREAM_MANAGEMENT) +async def test_migrate_unique_ids(hass, utcnow): + """Test migrating entity unique ids.""" + entity_registry = er.async_get(hass) + aid = get_next_aid() + camera = entity_registry.async_get_or_create( + "camera", + "homekit_controller", + f"homekit-0001-aid:{aid}", + ) + await setup_test_component(hass, create_camera) + assert ( + entity_registry.async_get(camera.entity_id).unique_id + == f"00:00:00:00:00:00_{aid}" + ) + + async def test_read_state(hass, utcnow): """Test reading the state of a HomeKit camera.""" helper = await setup_test_component(hass, create_camera) diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 0f669c9c51f..0f10f0f9fa0 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -17,8 +17,9 @@ from homeassistant.components.climate import ( SERVICE_SET_TEMPERATURE, HVACMode, ) +from homeassistant.helpers import entity_registry as er -from .common import setup_test_component +from .common import get_next_aid, setup_test_component # Test thermostat devices @@ -943,3 +944,19 @@ async def test_heater_cooler_turn_off(hass, utcnow): state = await helper.poll_and_get_state() assert state.state == "off" assert state.attributes["hvac_action"] == "off" + + +async def test_migrate_unique_id(hass, utcnow): + """Test a we can migrate a switch unique id.""" + entity_registry = er.async_get(hass) + aid = get_next_aid() + climate_entry = entity_registry.async_get_or_create( + "climate", + "homekit_controller", + f"homekit-00:00:00:00:00:00-{aid}-8", + ) + await setup_test_component(hass, create_heater_cooler_service) + assert ( + entity_registry.async_get(climate_entry.entity_id).unique_id + == f"00:00:00:00:00:00_{aid}_8" + ) diff --git a/tests/components/homekit_controller/test_connection.py b/tests/components/homekit_controller/test_connection.py index 9db07a45d16..b853989ab15 100644 --- a/tests/components/homekit_controller/test_connection.py +++ b/tests/components/homekit_controller/test_connection.py @@ -9,7 +9,6 @@ from homeassistant.components.homekit_controller.const import ( IDENTIFIER_ACCESSORY_ID, IDENTIFIER_LEGACY_ACCESSORY_ID, IDENTIFIER_LEGACY_SERIAL_NUMBER, - IDENTIFIER_SERIAL_NUMBER, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -36,7 +35,6 @@ DEVICE_MIGRATION_TESTS = [ manufacturer="RYSE Inc.", before={ (DOMAIN, IDENTIFIER_LEGACY_ACCESSORY_ID, "00:00:00:00:00:00"), - (DOMAIN, IDENTIFIER_LEGACY_SERIAL_NUMBER, "0401.3521.0679"), }, after={(IDENTIFIER_ACCESSORY_ID, "00:00:00:00:00:00:aid:1")}, ), @@ -55,11 +53,9 @@ DEVICE_MIGRATION_TESTS = [ manufacturer="Philips Lighting", before={ (DOMAIN, IDENTIFIER_LEGACY_ACCESSORY_ID, "00:00:00:00:00:00"), - (DOMAIN, IDENTIFIER_LEGACY_SERIAL_NUMBER, "123456"), }, after={ (IDENTIFIER_ACCESSORY_ID, "00:00:00:00:00:00:aid:1"), - (IDENTIFIER_SERIAL_NUMBER, "123456"), }, ), # Test migrating a Hue remote - it has a valid serial number @@ -72,7 +68,6 @@ DEVICE_MIGRATION_TESTS = [ }, after={ (IDENTIFIER_ACCESSORY_ID, "00:00:00:00:00:00:aid:6623462389072572"), - (IDENTIFIER_SERIAL_NUMBER, "6623462389072572"), }, ), # Test migrating a Koogeek LS1. This is just for completeness (testing hub and hub-less devices) @@ -85,7 +80,6 @@ DEVICE_MIGRATION_TESTS = [ }, after={ (IDENTIFIER_ACCESSORY_ID, "00:00:00:00:00:00:aid:1"), - (IDENTIFIER_SERIAL_NUMBER, "AAAA011111111111"), }, ), ] diff --git a/tests/components/homekit_controller/test_cover.py b/tests/components/homekit_controller/test_cover.py index 15422f2f0bc..6ceb57f5e09 100644 --- a/tests/components/homekit_controller/test_cover.py +++ b/tests/components/homekit_controller/test_cover.py @@ -2,7 +2,9 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from .common import setup_test_component +from homeassistant.helpers import entity_registry as er + +from .common import get_next_aid, setup_test_component def create_window_covering_service(accessory): @@ -277,3 +279,20 @@ async def test_read_door_state(hass, utcnow): ) state = await helper.poll_and_get_state() assert state.attributes["obstruction-detected"] is True + + +async def test_migrate_unique_id(hass, utcnow): + """Test a we can migrate a cover unique id.""" + entity_registry = er.async_get(hass) + aid = get_next_aid() + cover_entry = entity_registry.async_get_or_create( + "cover", + "homekit_controller", + f"homekit-00:00:00:00:00:00-{aid}-8", + ) + await setup_test_component(hass, create_garage_door_opener_service) + + assert ( + entity_registry.async_get(cover_entry.entity_id).unique_id + == f"00:00:00:00:00:00_{aid}_8" + ) diff --git a/tests/components/homekit_controller/test_fan.py b/tests/components/homekit_controller/test_fan.py index de13772b5a1..855f426da13 100644 --- a/tests/components/homekit_controller/test_fan.py +++ b/tests/components/homekit_controller/test_fan.py @@ -2,7 +2,9 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from .common import setup_test_component +from homeassistant.helpers import entity_registry as er + +from .common import get_next_aid, setup_test_component def create_fan_service(accessory): @@ -805,3 +807,20 @@ async def test_v2_set_percentage_non_standard_rotation_range(hass, utcnow): CharacteristicsTypes.ACTIVE: 0, }, ) + + +async def test_migrate_unique_id(hass, utcnow): + """Test a we can migrate a fan unique id.""" + entity_registry = er.async_get(hass) + aid = get_next_aid() + fan_entry = entity_registry.async_get_or_create( + "fan", + "homekit_controller", + f"homekit-00:00:00:00:00:00-{aid}-8", + ) + await setup_test_component(hass, create_fanv2_service_non_standard_rotation_range) + + assert ( + entity_registry.async_get(fan_entry.entity_id).unique_id + == f"00:00:00:00:00:00_{aid}_8" + ) diff --git a/tests/components/homekit_controller/test_humidifier.py b/tests/components/homekit_controller/test_humidifier.py index da981e9eac0..1128459c4a6 100644 --- a/tests/components/homekit_controller/test_humidifier.py +++ b/tests/components/homekit_controller/test_humidifier.py @@ -3,8 +3,9 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes from homeassistant.components.humidifier import DOMAIN, MODE_AUTO, MODE_NORMAL +from homeassistant.helpers import entity_registry as er -from .common import setup_test_component +from .common import get_next_aid, setup_test_component def create_humidifier_service(accessory): @@ -436,3 +437,20 @@ async def test_dehumidifier_target_humidity_modes(hass, utcnow): ) assert state.attributes["mode"] == "normal" assert state.attributes["humidity"] == 73 + + +async def test_migrate_entity_ids(hass, utcnow): + """Test that we can migrate humidifier entity ids.""" + aid = get_next_aid() + + entity_registry = er.async_get(hass) + humidifier_entry = entity_registry.async_get_or_create( + "humidifier", + "homekit_controller", + f"homekit-00:00:00:00:00:00-{aid}-8", + ) + await setup_test_component(hass, create_humidifier_service) + assert ( + entity_registry.async_get(humidifier_entry.entity_id).unique_id + == f"00:00:00:00:00:00_{aid}_8" + ) diff --git a/tests/components/homekit_controller/test_light.py b/tests/components/homekit_controller/test_light.py index 726be15a32c..31604f2b1dd 100644 --- a/tests/components/homekit_controller/test_light.py +++ b/tests/components/homekit_controller/test_light.py @@ -9,8 +9,9 @@ from homeassistant.components.light import ( ColorMode, ) from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_UNAVAILABLE +from homeassistant.helpers import entity_registry as er -from .common import setup_test_component +from .common import get_next_aid, setup_test_component LIGHT_BULB_NAME = "TestDevice" LIGHT_BULB_ENTITY_ID = "light.testdevice" @@ -335,3 +336,47 @@ async def test_light_unloaded_removed(hass, utcnow): # Make sure entity is removed assert hass.states.get(helper.entity_id).state == STATE_UNAVAILABLE + + +async def test_migrate_unique_id(hass, utcnow): + """Test a we can migrate a light unique id.""" + entity_registry = er.async_get(hass) + aid = get_next_aid() + light_entry = entity_registry.async_get_or_create( + "light", + "homekit_controller", + f"homekit-00:00:00:00:00:00-{aid}-8", + ) + await setup_test_component(hass, create_lightbulb_service_with_color_temp) + + assert ( + entity_registry.async_get(light_entry.entity_id).unique_id + == f"00:00:00:00:00:00_{aid}_8" + ) + + +async def test_only_migrate_once(hass, utcnow): + """Test a we handle migration happening after an upgrade and than a downgrade and then an upgrade.""" + entity_registry = er.async_get(hass) + aid = get_next_aid() + old_light_entry = entity_registry.async_get_or_create( + "light", + "homekit_controller", + f"homekit-00:00:00:00:00:00-{aid}-8", + ) + new_light_entry = entity_registry.async_get_or_create( + "light", + "homekit_controller", + f"00:00:00:00:00:00_{aid}_8", + ) + await setup_test_component(hass, create_lightbulb_service_with_color_temp) + + assert ( + entity_registry.async_get(old_light_entry.entity_id).unique_id + == f"homekit-00:00:00:00:00:00-{aid}-8" + ) + + assert ( + entity_registry.async_get(new_light_entry.entity_id).unique_id + == f"00:00:00:00:00:00_{aid}_8" + ) diff --git a/tests/components/homekit_controller/test_lock.py b/tests/components/homekit_controller/test_lock.py index af21f26a012..719ff66c766 100644 --- a/tests/components/homekit_controller/test_lock.py +++ b/tests/components/homekit_controller/test_lock.py @@ -2,7 +2,9 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from .common import setup_test_component +from homeassistant.helpers import entity_registry as er + +from .common import get_next_aid, setup_test_component def create_lock_service(accessory): @@ -112,3 +114,20 @@ async def test_switch_read_lock_state(hass, utcnow): ) state = await helper.poll_and_get_state() assert state.state == "unlocking" + + +async def test_migrate_unique_id(hass, utcnow): + """Test a we can migrate a lock unique id.""" + entity_registry = er.async_get(hass) + aid = get_next_aid() + lock_entry = entity_registry.async_get_or_create( + "lock", + "homekit_controller", + f"homekit-00:00:00:00:00:00-{aid}-8", + ) + await setup_test_component(hass, create_lock_service) + + assert ( + entity_registry.async_get(lock_entry.entity_id).unique_id + == f"00:00:00:00:00:00_{aid}_8" + ) diff --git a/tests/components/homekit_controller/test_media_player.py b/tests/components/homekit_controller/test_media_player.py index 7fb8c4edb2a..829f28cf341 100644 --- a/tests/components/homekit_controller/test_media_player.py +++ b/tests/components/homekit_controller/test_media_player.py @@ -6,7 +6,9 @@ from aiohomekit.model.characteristics import ( from aiohomekit.model.services import ServicesTypes import pytest -from .common import setup_test_component +from homeassistant.helpers import entity_registry as er + +from .common import get_next_aid, setup_test_component def create_tv_service(accessory): @@ -364,3 +366,20 @@ async def test_tv_set_source_fail(hass, utcnow): state = await helper.poll_and_get_state() assert state.attributes["source"] == "HDMI 1" + + +async def test_migrate_unique_id(hass, utcnow): + """Test a we can migrate a media_player unique id.""" + entity_registry = er.async_get(hass) + aid = get_next_aid() + media_player_entry = entity_registry.async_get_or_create( + "media_player", + "homekit_controller", + f"homekit-00:00:00:00:00:00-{aid}-8", + ) + await setup_test_component(hass, create_tv_service_with_target_media_state) + + assert ( + entity_registry.async_get(media_player_entry.entity_id).unique_id + == f"00:00:00:00:00:00_{aid}_8" + ) diff --git a/tests/components/homekit_controller/test_number.py b/tests/components/homekit_controller/test_number.py index 6d060416861..cc6950e0f90 100644 --- a/tests/components/homekit_controller/test_number.py +++ b/tests/components/homekit_controller/test_number.py @@ -2,7 +2,9 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from .common import Helper, setup_test_component +from homeassistant.helpers import entity_registry as er + +from .common import Helper, get_next_aid, setup_test_component def create_switch_with_spray_level(accessory): @@ -26,6 +28,24 @@ def create_switch_with_spray_level(accessory): return service +async def test_migrate_unique_id(hass, utcnow): + """Test a we can migrate a number unique id.""" + entity_registry = er.async_get(hass) + aid = get_next_aid() + number = entity_registry.async_get_or_create( + "number", + "homekit_controller", + f"homekit-0001-aid:{aid}-sid:8-cid:9", + suggested_object_id="testdevice_spray_quantity", + ) + await setup_test_component(hass, create_switch_with_spray_level) + + assert ( + entity_registry.async_get(number.entity_id).unique_id + == f"00:00:00:00:00:00_{aid}_8_9" + ) + + async def test_read_number(hass, utcnow): """Test a switch service that has a sensor characteristic is correctly handled.""" helper = await setup_test_component(hass, create_switch_with_spray_level) diff --git a/tests/components/homekit_controller/test_select.py b/tests/components/homekit_controller/test_select.py index 22cd53d7a31..d18f0b97ecc 100644 --- a/tests/components/homekit_controller/test_select.py +++ b/tests/components/homekit_controller/test_select.py @@ -3,7 +3,9 @@ from aiohomekit.model import Accessory from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from .common import Helper, setup_test_component +from homeassistant.helpers import entity_registry as er + +from .common import Helper, get_next_aid, setup_test_component def create_service_with_ecobee_mode(accessory: Accessory): @@ -19,6 +21,25 @@ def create_service_with_ecobee_mode(accessory: Accessory): return service +async def test_migrate_unique_id(hass, utcnow): + """Test we can migrate a select unique id.""" + entity_registry = er.async_get(hass) + aid = get_next_aid() + select = entity_registry.async_get_or_create( + "select", + "homekit_controller", + f"homekit-0001-aid:{aid}-sid:8-cid:14", + suggested_object_id="testdevice_current_mode", + ) + + await setup_test_component(hass, create_service_with_ecobee_mode) + + assert ( + entity_registry.async_get(select.entity_id).unique_id + == f"00:00:00:00:00:00_{aid}_8_14" + ) + + async def test_read_current_mode(hass, utcnow): """Test that Ecobee mode can be correctly read and show as human readable text.""" helper = await setup_test_component(hass, create_service_with_ecobee_mode) diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index 4bd6612026c..b769a916082 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.homekit_controller.sensor import ( thread_status_to_str, ) from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.helpers import entity_registry as er from .common import TEST_DEVICE_SERVICE_INFO, Helper, setup_test_component @@ -361,7 +362,6 @@ async def test_rssi_sensor( hass, utcnow, entity_registry_enabled_by_default, enable_bluetooth ): """Test an rssi sensor.""" - inject_bluetooth_service_info(hass, TEST_DEVICE_SERVICE_INFO) class FakeBLEPairing(FakePairing): @@ -378,3 +378,38 @@ async def test_rssi_sensor( hass, create_battery_level_sensor, suffix="battery", connection="BLE" ) assert hass.states.get("sensor.testdevice_signal_strength").state == "-56" + + +async def test_migrate_rssi_sensor_unique_id( + hass, utcnow, entity_registry_enabled_by_default, enable_bluetooth +): + """Test an rssi sensor unique id migration.""" + entity_registry = er.async_get(hass) + rssi_sensor = entity_registry.async_get_or_create( + "sensor", + "homekit_controller", + "homekit-0001-rssi", + suggested_object_id="renamed_rssi", + ) + + inject_bluetooth_service_info(hass, TEST_DEVICE_SERVICE_INFO) + + class FakeBLEPairing(FakePairing): + """Fake BLE pairing.""" + + @property + def transport(self): + return Transport.BLE + + with patch("aiohomekit.testing.FakePairing", FakeBLEPairing): + # Any accessory will do for this test, but we need at least + # one or the rssi sensor will not be created + await setup_test_component( + hass, create_battery_level_sensor, suffix="battery", connection="BLE" + ) + assert hass.states.get("sensor.renamed_rssi").state == "-56" + + assert ( + entity_registry.async_get(rssi_sensor.entity_id).unique_id + == "00:00:00:00:00:00_rssi" + ) diff --git a/tests/components/homekit_controller/test_switch.py b/tests/components/homekit_controller/test_switch.py index a034624bd60..1e9b1cab730 100644 --- a/tests/components/homekit_controller/test_switch.py +++ b/tests/components/homekit_controller/test_switch.py @@ -7,7 +7,9 @@ from aiohomekit.model.characteristics import ( ) from aiohomekit.model.services import ServicesTypes -from .common import setup_test_component +from homeassistant.helpers import entity_registry as er + +from .common import get_next_aid, setup_test_component def create_switch_service(accessory): @@ -215,3 +217,30 @@ async def test_char_switch_read_state(hass, utcnow): {CharacteristicsTypes.VENDOR_AQARA_PAIRING_MODE: False}, ) assert switch_1.state == "off" + + +async def test_migrate_unique_id(hass, utcnow): + """Test a we can migrate a switch unique id.""" + entity_registry = er.async_get(hass) + aid = get_next_aid() + switch_entry = entity_registry.async_get_or_create( + "switch", + "homekit_controller", + f"homekit-00:00:00:00:00:00-{aid}-8", + ) + switch_entry_2 = entity_registry.async_get_or_create( + "switch", + "homekit_controller", + f"homekit-0001-aid:{aid}-sid:8-cid:9", + ) + await setup_test_component(hass, create_char_switch_service, suffix="pairing_mode") + + assert ( + entity_registry.async_get(switch_entry.entity_id).unique_id + == f"00:00:00:00:00:00_{aid}_8" + ) + + assert ( + entity_registry.async_get(switch_entry_2.entity_id).unique_id + == f"00:00:00:00:00:00_{aid}_8_9" + ) From a18f8d2ff3581f555403853a229e1937bd70ecf1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Oct 2022 23:50:07 +0200 Subject: [PATCH 378/985] Add error handling to LaMetric button platform (#80136) --- homeassistant/components/lametric/button.py | 2 + homeassistant/components/lametric/helpers.py | 46 ++++++++++++++++ tests/components/lametric/test_button.py | 58 +++++++++++++++++++- 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/lametric/helpers.py diff --git a/homeassistant/components/lametric/button.py b/homeassistant/components/lametric/button.py index 4d8c75f0ab0..8de7ddb16ff 100644 --- a/homeassistant/components/lametric/button.py +++ b/homeassistant/components/lametric/button.py @@ -16,6 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .coordinator import LaMetricDataUpdateCoordinator from .entity import LaMetricEntity +from .helpers import lametric_exception_handler @dataclass @@ -81,6 +82,7 @@ class LaMetricButtonEntity(LaMetricEntity, ButtonEntity): self.entity_description = description self._attr_unique_id = f"{coordinator.data.serial_number}-{description.key}" + @lametric_exception_handler async def async_press(self) -> None: """Send out a command to LaMetric.""" await self.entity_description.press_fn(self.coordinator.lametric) diff --git a/homeassistant/components/lametric/helpers.py b/homeassistant/components/lametric/helpers.py new file mode 100644 index 00000000000..e9b1b941dae --- /dev/null +++ b/homeassistant/components/lametric/helpers.py @@ -0,0 +1,46 @@ +"""Helpers for LaMetric.""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from typing import Any, TypeVar + +from demetriek import LaMetricConnectionError, LaMetricError +from typing_extensions import Concatenate, ParamSpec + +from homeassistant.exceptions import HomeAssistantError + +from .entity import LaMetricEntity + +_LaMetricEntityT = TypeVar("_LaMetricEntityT", bound=LaMetricEntity) +_P = ParamSpec("_P") + + +def lametric_exception_handler( + func: Callable[Concatenate[_LaMetricEntityT, _P], Coroutine[Any, Any, Any]] +) -> Callable[Concatenate[_LaMetricEntityT, _P], Coroutine[Any, Any, None]]: + """Decorate LaMetric calls to handle LaMetric exceptions. + + A decorator that wraps the passed in function, catches LaMetric errors, + and handles the availability of the device in the data coordinator. + """ + + async def handler( + self: _LaMetricEntityT, *args: _P.args, **kwargs: _P.kwargs + ) -> None: + try: + await func(self, *args, **kwargs) + self.coordinator.async_update_listeners() + + except LaMetricConnectionError as error: + self.coordinator.last_update_success = False + self.coordinator.async_update_listeners() + raise HomeAssistantError( + "Error communicating with the LaMetric device" + ) from error + + except LaMetricError as error: + raise HomeAssistantError( + "Invalid response from the LaMetric device" + ) from error + + return handler diff --git a/tests/components/lametric/test_button.py b/tests/components/lametric/test_button.py index cd55c9914f5..d37b6dd1c18 100644 --- a/tests/components/lametric/test_button.py +++ b/tests/components/lametric/test_button.py @@ -1,12 +1,19 @@ """Tests for the LaMetric button platform.""" from unittest.mock import MagicMock +from demetriek import LaMetricConnectionError, LaMetricError import pytest from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.lametric.const import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, ATTR_ICON, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_ICON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import EntityCategory @@ -111,3 +118,52 @@ async def test_button_app_previous( state = hass.states.get("button.frenck_s_lametric_previous_app") assert state assert state.state == "2022-09-19T12:07:30+00:00" + + +@pytest.mark.freeze_time("2022-10-11 22:00:00") +async def test_button_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_lametric: MagicMock, +) -> None: + """Test error handling of the LaMetric buttons.""" + mock_lametric.app_next.side_effect = LaMetricError + + with pytest.raises( + HomeAssistantError, match="Invalid response from the LaMetric device" + ): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.frenck_s_lametric_next_app"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("button.frenck_s_lametric_next_app") + assert state + assert state.state == "2022-10-11T22:00:00+00:00" + + +async def test_button_connection_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_lametric: MagicMock, +) -> None: + """Test connection error handling of the LaMetric buttons.""" + mock_lametric.app_next.side_effect = LaMetricConnectionError + + with pytest.raises( + HomeAssistantError, match="Error communicating with the LaMetric device" + ): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.frenck_s_lametric_next_app"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("button.frenck_s_lametric_next_app") + assert state + assert state.state == STATE_UNAVAILABLE From 712f40b6b031f60c52b1c040e8494127fd7a1596 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Wed, 12 Oct 2022 02:22:21 +0200 Subject: [PATCH 379/985] Add has_entity_name for here_travel_time (#80011) * Add has_entity_name for here_travel_time * Duration in traffic --- homeassistant/components/here_travel_time/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 537c32e423b..e1d8342df19 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -57,7 +57,7 @@ def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...] native_unit_of_measurement=TIME_MINUTES, ), SensorEntityDescription( - name="Duration in Traffic", + name="Duration in traffic", icon=ICONS.get(travel_mode, ICON_CAR), key=ATTR_DURATION_IN_TRAFFIC, state_class=SensorStateClass.MEASUREMENT, @@ -106,7 +106,6 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = sensor_description - self._attr_name = f"{name} {sensor_description.name}" self._attr_unique_id = f"{unique_id_prefix}_{sensor_description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, unique_id_prefix)}, @@ -114,6 +113,7 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): name=name, manufacturer="HERE Technologies", ) + self._attr_has_entity_name = True async def async_added_to_hass(self) -> None: """Wait for start so origin and destination entities can be resolved.""" From 230fe4453fbbeebbba9484a3fe5d5f1839fcfd9c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 12 Oct 2022 00:40:30 +0000 Subject: [PATCH 380/985] [ci skip] Translation update --- .../components/abode/translations/no.json | 2 +- .../components/airvisual/translations/no.json | 2 +- .../aladdin_connect/translations/no.json | 2 +- .../components/apple_tv/translations/no.json | 2 +- .../components/august/translations/no.json | 2 +- .../aussie_broadband/translations/no.json | 2 +- .../components/awair/translations/no.json | 2 +- .../azure_devops/translations/no.json | 2 +- .../components/bosch_shc/translations/no.json | 2 +- .../components/braviatv/translations/no.json | 2 +- .../components/brunt/translations/no.json | 2 +- .../components/bthome/translations/no.json | 2 +- .../cloudflare/translations/no.json | 2 +- .../devolo_home_control/translations/no.json | 2 +- .../devolo_home_network/translations/ca.json | 8 +++++- .../devolo_home_network/translations/de.json | 8 +++++- .../devolo_home_network/translations/es.json | 8 +++++- .../devolo_home_network/translations/et.json | 8 +++++- .../devolo_home_network/translations/hu.json | 8 +++++- .../devolo_home_network/translations/id.json | 8 +++++- .../devolo_home_network/translations/no.json | 8 +++++- .../devolo_home_network/translations/pl.json | 8 +++++- .../translations/zh-Hant.json | 8 +++++- .../components/discord/translations/no.json | 2 +- .../components/efergy/translations/no.json | 2 +- .../enphase_envoy/translations/no.json | 2 +- .../components/esphome/translations/no.json | 2 +- .../components/fibaro/translations/no.json | 2 +- .../fireservicerota/translations/no.json | 2 +- .../components/flume/translations/no.json | 2 +- .../components/fritz/translations/no.json | 2 +- .../components/fritzbox/translations/no.json | 2 +- .../geocaching/translations/no.json | 2 +- .../components/google/translations/no.json | 2 +- .../google_sheets/translations/no.json | 2 +- .../components/hive/translations/no.json | 2 +- .../huawei_lte/translations/no.json | 2 +- .../components/hyperion/translations/no.json | 2 +- .../components/icloud/translations/no.json | 2 +- .../intellifire/translations/no.json | 2 +- .../components/isy994/translations/no.json | 2 +- .../lacrosse_view/translations/no.json | 2 +- .../lametric/translations/select.ca.json | 8 ++++++ .../lametric/translations/select.de.json | 8 ++++++ .../lametric/translations/select.es.json | 8 ++++++ .../lametric/translations/select.et.json | 8 ++++++ .../lametric/translations/select.hu.json | 8 ++++++ .../lametric/translations/select.id.json | 8 ++++++ .../lametric/translations/select.no.json | 8 ++++++ .../lametric/translations/select.pl.json | 8 ++++++ .../lametric/translations/select.ru.json | 8 ++++++ .../lametric/translations/select.zh-Hant.json | 8 ++++++ .../components/lidarr/translations/no.json | 2 +- .../components/life360/translations/no.json | 2 +- .../litterrobot/translations/no.json | 2 +- .../components/lyric/translations/no.json | 2 +- .../components/mazda/translations/no.json | 2 +- .../components/mikrotik/translations/no.json | 2 +- .../components/motioneye/translations/no.json | 2 +- .../components/myq/translations/no.json | 2 +- .../components/nam/translations/no.json | 2 +- .../components/neato/translations/no.json | 2 +- .../components/nest/translations/no.json | 2 +- .../components/netatmo/translations/no.json | 2 +- .../components/notion/translations/no.json | 2 +- .../components/nuki/translations/no.json | 2 +- .../components/octoprint/translations/no.json | 2 +- .../openexchangerates/translations/no.json | 2 +- .../components/overkiz/translations/no.json | 2 +- .../components/picnic/translations/no.json | 2 +- .../components/plex/translations/no.json | 2 +- .../components/powerwall/translations/no.json | 2 +- .../components/prosegur/translations/no.json | 2 +- .../components/pushover/translations/no.json | 2 +- .../components/pvoutput/translations/no.json | 2 +- .../components/radarr/translations/no.json | 2 +- .../components/renault/translations/no.json | 2 +- .../components/ridwell/translations/no.json | 2 +- .../components/samsungtv/translations/no.json | 2 +- .../components/sense/translations/no.json | 2 +- .../components/sensibo/translations/no.json | 2 +- .../components/sharkiq/translations/no.json | 2 +- .../components/shelly/translations/no.json | 2 +- .../shopping_list/translations/no.json | 2 +- .../simplisafe/translations/no.json | 2 +- .../components/skybell/translations/no.json | 2 +- .../components/sleepiq/translations/no.json | 2 +- .../components/smarttub/translations/no.json | 2 +- .../components/snooz/translations/ca.json | 21 +++++++++++++++ .../components/snooz/translations/de.json | 27 +++++++++++++++++++ .../components/snooz/translations/es.json | 27 +++++++++++++++++++ .../components/snooz/translations/et.json | 27 +++++++++++++++++++ .../components/snooz/translations/hu.json | 27 +++++++++++++++++++ .../components/snooz/translations/id.json | 27 +++++++++++++++++++ .../components/snooz/translations/no.json | 27 +++++++++++++++++++ .../components/snooz/translations/pl.json | 27 +++++++++++++++++++ .../components/snooz/translations/ru.json | 10 +++++++ .../snooz/translations/zh-Hant.json | 27 +++++++++++++++++++ .../components/sonarr/translations/no.json | 2 +- .../steam_online/translations/no.json | 2 +- .../synology_dsm/translations/no.json | 2 +- .../system_bridge/translations/no.json | 2 +- .../components/tailscale/translations/no.json | 2 +- .../tankerkoenig/translations/no.json | 2 +- .../components/tautulli/translations/no.json | 2 +- .../components/tile/translations/no.json | 2 +- .../totalconnect/translations/no.json | 2 +- .../components/tractive/translations/no.json | 2 +- .../trafikverket_ferry/translations/no.json | 2 +- .../trafikverket_train/translations/no.json | 2 +- .../transmission/translations/no.json | 2 +- .../components/unifi/translations/no.json | 2 +- .../uptimerobot/translations/no.json | 2 +- .../components/verisure/translations/no.json | 2 +- .../vlc_telnet/translations/no.json | 2 +- .../volvooncall/translations/no.json | 2 +- .../components/wallbox/translations/no.json | 2 +- .../components/watttime/translations/no.json | 2 +- .../xiaomi_ble/translations/no.json | 2 +- .../xiaomi_miio/translations/no.json | 2 +- .../yale_smart_alarm/translations/no.json | 2 +- .../components/yolink/translations/no.json | 2 +- .../components/zwave_js/translations/de.json | 3 ++- .../components/zwave_js/translations/en.json | 3 ++- .../components/zwave_js/translations/es.json | 3 ++- .../components/zwave_js/translations/id.json | 3 ++- .../components/zwave_js/translations/no.json | 3 ++- .../components/zwave_js/translations/pl.json | 3 ++- 128 files changed, 495 insertions(+), 108 deletions(-) create mode 100644 homeassistant/components/lametric/translations/select.ca.json create mode 100644 homeassistant/components/lametric/translations/select.de.json create mode 100644 homeassistant/components/lametric/translations/select.es.json create mode 100644 homeassistant/components/lametric/translations/select.et.json create mode 100644 homeassistant/components/lametric/translations/select.hu.json create mode 100644 homeassistant/components/lametric/translations/select.id.json create mode 100644 homeassistant/components/lametric/translations/select.no.json create mode 100644 homeassistant/components/lametric/translations/select.pl.json create mode 100644 homeassistant/components/lametric/translations/select.ru.json create mode 100644 homeassistant/components/lametric/translations/select.zh-Hant.json create mode 100644 homeassistant/components/snooz/translations/ca.json create mode 100644 homeassistant/components/snooz/translations/de.json create mode 100644 homeassistant/components/snooz/translations/es.json create mode 100644 homeassistant/components/snooz/translations/et.json create mode 100644 homeassistant/components/snooz/translations/hu.json create mode 100644 homeassistant/components/snooz/translations/id.json create mode 100644 homeassistant/components/snooz/translations/no.json create mode 100644 homeassistant/components/snooz/translations/pl.json create mode 100644 homeassistant/components/snooz/translations/ru.json create mode 100644 homeassistant/components/snooz/translations/zh-Hant.json diff --git a/homeassistant/components/abode/translations/no.json b/homeassistant/components/abode/translations/no.json index 27706c3d797..6918b238d42 100644 --- a/homeassistant/components/abode/translations/no.json +++ b/homeassistant/components/abode/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { diff --git a/homeassistant/components/airvisual/translations/no.json b/homeassistant/components/airvisual/translations/no.json index d4ca80d4805..89ff3fca958 100644 --- a/homeassistant/components/airvisual/translations/no.json +++ b/homeassistant/components/airvisual/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Plasseringen er allerede konfigurert eller Node / Pro ID er allerede registrert.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/aladdin_connect/translations/no.json b/homeassistant/components/aladdin_connect/translations/no.json index c6e6e8413b3..464a6bf1071 100644 --- a/homeassistant/components/aladdin_connect/translations/no.json +++ b/homeassistant/components/aladdin_connect/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/apple_tv/translations/no.json b/homeassistant/components/apple_tv/translations/no.json index 97f80c7dfe7..d0250d31dfa 100644 --- a/homeassistant/components/apple_tv/translations/no.json +++ b/homeassistant/components/apple_tv/translations/no.json @@ -9,7 +9,7 @@ "inconsistent_device": "Forventede protokoller ble ikke funnet under oppdagelsen. Dette indikerer vanligvis et problem med multicast DNS (Zeroconf). Pr\u00f8v \u00e5 legge til enheten p\u00e5 nytt.", "ipv6_not_supported": "IPv6 st\u00f8ttes ikke.", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "setup_failed": "Kunne ikke konfigurere enheten.", "unknown": "Uventet feil" }, diff --git a/homeassistant/components/august/translations/no.json b/homeassistant/components/august/translations/no.json index 8ea4cd7141f..11e30ed8bf6 100644 --- a/homeassistant/components/august/translations/no.json +++ b/homeassistant/components/august/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/aussie_broadband/translations/no.json b/homeassistant/components/aussie_broadband/translations/no.json index 00e911bbfba..c755bcc3e93 100644 --- a/homeassistant/components/aussie_broadband/translations/no.json +++ b/homeassistant/components/aussie_broadband/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Kontoen er allerede konfigurert", "no_services_found": "Ingen tjenester ble funnet for denne kontoen", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/awair/translations/no.json b/homeassistant/components/awair/translations/no.json index b71736d7a6d..983a47ecfed 100644 --- a/homeassistant/components/awair/translations/no.json +++ b/homeassistant/components/awair/translations/no.json @@ -5,7 +5,7 @@ "already_configured_account": "Kontoen er allerede konfigurert", "already_configured_device": "Enheten er allerede konfigurert", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "unreachable": "Tilkobling mislyktes" }, "error": { diff --git a/homeassistant/components/azure_devops/translations/no.json b/homeassistant/components/azure_devops/translations/no.json index ba4ff946595..e765e4b79ce 100644 --- a/homeassistant/components/azure_devops/translations/no.json +++ b/homeassistant/components/azure_devops/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/bosch_shc/translations/no.json b/homeassistant/components/bosch_shc/translations/no.json index 1b5b9fb0642..9d5ecc41b4c 100644 --- a/homeassistant/components/bosch_shc/translations/no.json +++ b/homeassistant/components/bosch_shc/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json index 881cccde8a5..dec2157d6a3 100644 --- a/homeassistant/components/braviatv/translations/no.json +++ b/homeassistant/components/braviatv/translations/no.json @@ -4,7 +4,7 @@ "already_configured": "Enheten er allerede konfigurert", "no_ip_control": "IP-kontrollen er deaktivert p\u00e5 TVen eller TV-en st\u00f8ttes ikke.", "not_bravia_device": "Enheten er ikke en Bravia TV.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "reauth_unsuccessful": "Re-autentisering mislyktes. Fjern integrasjonen og konfigurer den p\u00e5 nytt." }, "error": { diff --git a/homeassistant/components/brunt/translations/no.json b/homeassistant/components/brunt/translations/no.json index b77151ac92a..78dde9c6234 100644 --- a/homeassistant/components/brunt/translations/no.json +++ b/homeassistant/components/brunt/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/bthome/translations/no.json b/homeassistant/components/bthome/translations/no.json index ba68150db4c..84f7e7853df 100644 --- a/homeassistant/components/bthome/translations/no.json +++ b/homeassistant/components/bthome/translations/no.json @@ -4,7 +4,7 @@ "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "decryption_failed": "Den oppgitte bindingsn\u00f8kkelen fungerte ikke, sensordata kunne ikke dekrypteres. Vennligst sjekk det og pr\u00f8v igjen.", diff --git a/homeassistant/components/cloudflare/translations/no.json b/homeassistant/components/cloudflare/translations/no.json index 1329429474a..00792b4b83c 100644 --- a/homeassistant/components/cloudflare/translations/no.json +++ b/homeassistant/components/cloudflare/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", "unknown": "Uventet feil" }, diff --git a/homeassistant/components/devolo_home_control/translations/no.json b/homeassistant/components/devolo_home_control/translations/no.json index 1f1ee69ae47..82f71bc7065 100644 --- a/homeassistant/components/devolo_home_control/translations/no.json +++ b/homeassistant/components/devolo_home_control/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning", diff --git a/homeassistant/components/devolo_home_network/translations/ca.json b/homeassistant/components/devolo_home_network/translations/ca.json index c175a1a1246..c0278a4733d 100644 --- a/homeassistant/components/devolo_home_network/translations/ca.json +++ b/homeassistant/components/devolo_home_network/translations/ca.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", - "home_control": "La unitat central de control dom\u00e8stic de devolo no funciona amb aquesta integraci\u00f3." + "home_control": "La unitat central de control dom\u00e8stic de devolo no funciona amb aquesta integraci\u00f3.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -10,6 +11,11 @@ }, "flow_title": "{product} ({name})", "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + } + }, "user": { "data": { "ip_address": "Adre\u00e7a IP" diff --git a/homeassistant/components/devolo_home_network/translations/de.json b/homeassistant/components/devolo_home_network/translations/de.json index c018c757d16..8a850e01bcf 100644 --- a/homeassistant/components/devolo_home_network/translations/de.json +++ b/homeassistant/components/devolo_home_network/translations/de.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "home_control": "Die devolo Home Control-Zentraleinheit funktioniert nicht mit dieser Integration." + "home_control": "Die devolo Home Control-Zentraleinheit funktioniert nicht mit dieser Integration.", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -10,6 +11,11 @@ }, "flow_title": "{product} ({name})", "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + } + }, "user": { "data": { "ip_address": "IP-Adresse" diff --git a/homeassistant/components/devolo_home_network/translations/es.json b/homeassistant/components/devolo_home_network/translations/es.json index 645f0347554..9eea1b52415 100644 --- a/homeassistant/components/devolo_home_network/translations/es.json +++ b/homeassistant/components/devolo_home_network/translations/es.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "home_control": "La unidad central Home Control de devolo no funciona con esta integraci\u00f3n." + "home_control": "La unidad central Home Control de devolo no funciona con esta integraci\u00f3n.", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", @@ -10,6 +11,11 @@ }, "flow_title": "{product} ({name})", "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + } + }, "user": { "data": { "ip_address": "Direcci\u00f3n IP" diff --git a/homeassistant/components/devolo_home_network/translations/et.json b/homeassistant/components/devolo_home_network/translations/et.json index dff9df53c72..6e985371e93 100644 --- a/homeassistant/components/devolo_home_network/translations/et.json +++ b/homeassistant/components/devolo_home_network/translations/et.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", - "home_control": "Devolo Home Controli kesk\u00fcksus ei t\u00f6\u00f6ta selle sidumisega." + "home_control": "Devolo Home Controli kesk\u00fcksus ei t\u00f6\u00f6ta selle sidumisega.", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", @@ -10,6 +11,11 @@ }, "flow_title": "{product} ( {name} )", "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + } + }, "user": { "data": { "ip_address": "IP aadress" diff --git a/homeassistant/components/devolo_home_network/translations/hu.json b/homeassistant/components/devolo_home_network/translations/hu.json index dfae08312df..6e202527929 100644 --- a/homeassistant/components/devolo_home_network/translations/hu.json +++ b/homeassistant/components/devolo_home_network/translations/hu.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "home_control": "A devolo Home Control k\u00f6zponti egys\u00e9g nem m\u0171k\u00f6dik ezzel az integr\u00e1ci\u00f3val." + "home_control": "A devolo Home Control k\u00f6zponti egys\u00e9g nem m\u0171k\u00f6dik ezzel az integr\u00e1ci\u00f3val.", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -10,6 +11,11 @@ }, "flow_title": "{product} ({name})", "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + } + }, "user": { "data": { "ip_address": "IP c\u00edm" diff --git a/homeassistant/components/devolo_home_network/translations/id.json b/homeassistant/components/devolo_home_network/translations/id.json index 0950f6a2711..5e6d5cd67d5 100644 --- a/homeassistant/components/devolo_home_network/translations/id.json +++ b/homeassistant/components/devolo_home_network/translations/id.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", - "home_control": "Unit Central devolo Home Control tidak berfungsi dengan integrasi ini." + "home_control": "Unit Central devolo Home Control tidak berfungsi dengan integrasi ini.", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", @@ -10,6 +11,11 @@ }, "flow_title": "{product} ({name})", "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + } + }, "user": { "data": { "ip_address": "Alamat IP" diff --git a/homeassistant/components/devolo_home_network/translations/no.json b/homeassistant/components/devolo_home_network/translations/no.json index 405434abc4a..e7554b9aa91 100644 --- a/homeassistant/components/devolo_home_network/translations/no.json +++ b/homeassistant/components/devolo_home_network/translations/no.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "home_control": "Devolo Home Control Central Unit fungerer ikke med denne integrasjonen." + "home_control": "Devolo Home Control Central Unit fungerer ikke med denne integrasjonen.", + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -10,6 +11,11 @@ }, "flow_title": "{product} ( {name} )", "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + } + }, "user": { "data": { "ip_address": "IP adresse" diff --git a/homeassistant/components/devolo_home_network/translations/pl.json b/homeassistant/components/devolo_home_network/translations/pl.json index 4abe2667100..70bcee1ecfc 100644 --- a/homeassistant/components/devolo_home_network/translations/pl.json +++ b/homeassistant/components/devolo_home_network/translations/pl.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "home_control": "Ta jednostka devolo Home Control Central nie wsp\u00f3\u0142pracuje z t\u0105 integracj\u0105." + "home_control": "Ta jednostka devolo Home Control Central nie wsp\u00f3\u0142pracuje z t\u0105 integracj\u0105.", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -10,6 +11,11 @@ }, "flow_title": "{product} ({name})", "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + } + }, "user": { "data": { "ip_address": "Adres IP" diff --git a/homeassistant/components/devolo_home_network/translations/zh-Hant.json b/homeassistant/components/devolo_home_network/translations/zh-Hant.json index bccc6aa24e3..e17f7fc106b 100644 --- a/homeassistant/components/devolo_home_network/translations/zh-Hant.json +++ b/homeassistant/components/devolo_home_network/translations/zh-Hant.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "home_control": "Devolo \u667a\u80fd\u5bb6\u5ead\u7db2\u8def\u88dd\u7f6e\u8207\u6b64\u6574\u5408\u4e0d\u76f8\u5bb9\u3002" + "home_control": "Devolo \u667a\u80fd\u5bb6\u5ead\u7db2\u8def\u88dd\u7f6e\u8207\u6b64\u6574\u5408\u4e0d\u76f8\u5bb9\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -10,6 +11,11 @@ }, "flow_title": "{product} ({name})", "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + } + }, "user": { "data": { "ip_address": "IP \u4f4d\u5740" diff --git a/homeassistant/components/discord/translations/no.json b/homeassistant/components/discord/translations/no.json index e8a36e5c794..414df1519c1 100644 --- a/homeassistant/components/discord/translations/no.json +++ b/homeassistant/components/discord/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Tjenesten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/efergy/translations/no.json b/homeassistant/components/efergy/translations/no.json index 4a109ab8fa9..29aeb402191 100644 --- a/homeassistant/components/efergy/translations/no.json +++ b/homeassistant/components/efergy/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/enphase_envoy/translations/no.json b/homeassistant/components/enphase_envoy/translations/no.json index 091d76d55ec..5fffefe035f 100644 --- a/homeassistant/components/enphase_envoy/translations/no.json +++ b/homeassistant/components/enphase_envoy/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/esphome/translations/no.json b/homeassistant/components/esphome/translations/no.json index 1fabb774aa2..e17b9dad815 100644 --- a/homeassistant/components/esphome/translations/no.json +++ b/homeassistant/components/esphome/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "connection_error": "Kan ikke koble til ESP. Kontroller at YAML filen din inneholder en \"api:\" linje.", diff --git a/homeassistant/components/fibaro/translations/no.json b/homeassistant/components/fibaro/translations/no.json index f98835533f5..8a52c2de4ab 100644 --- a/homeassistant/components/fibaro/translations/no.json +++ b/homeassistant/components/fibaro/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/fireservicerota/translations/no.json b/homeassistant/components/fireservicerota/translations/no.json index be485577e65..03ecc365e74 100644 --- a/homeassistant/components/fireservicerota/translations/no.json +++ b/homeassistant/components/fireservicerota/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "create_entry": { "default": "Vellykket godkjenning" diff --git a/homeassistant/components/flume/translations/no.json b/homeassistant/components/flume/translations/no.json index aeda0eae271..b0066f9decb 100644 --- a/homeassistant/components/flume/translations/no.json +++ b/homeassistant/components/flume/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/fritz/translations/no.json b/homeassistant/components/fritz/translations/no.json index 5ed97636675..7e773022475 100644 --- a/homeassistant/components/fritz/translations/no.json +++ b/homeassistant/components/fritz/translations/no.json @@ -4,7 +4,7 @@ "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "ignore_ip6_link_local": "IPv6-lenkens lokale adresse st\u00f8ttes ikke.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "already_configured": "Enheten er allerede konfigurert", diff --git a/homeassistant/components/fritzbox/translations/no.json b/homeassistant/components/fritzbox/translations/no.json index 98174053a61..e6d464b3f37 100644 --- a/homeassistant/components/fritzbox/translations/no.json +++ b/homeassistant/components/fritzbox/translations/no.json @@ -6,7 +6,7 @@ "ignore_ip6_link_local": "IPv6-lenkens lokale adresse st\u00f8ttes ikke.", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", "not_supported": "Tilkoblet AVM FRITZ! Box, men den klarer ikke \u00e5 kontrollere Smart Home-enheter.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning" diff --git a/homeassistant/components/geocaching/translations/no.json b/homeassistant/components/geocaching/translations/no.json index f0b0b724861..2dee344a648 100644 --- a/homeassistant/components/geocaching/translations/no.json +++ b/homeassistant/components/geocaching/translations/no.json @@ -7,7 +7,7 @@ "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", "oauth_error": "Mottatt ugyldige token data.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "create_entry": { "default": "Vellykket godkjenning" diff --git a/homeassistant/components/google/translations/no.json b/homeassistant/components/google/translations/no.json index 55103db2a47..d020a0f294e 100644 --- a/homeassistant/components/google/translations/no.json +++ b/homeassistant/components/google/translations/no.json @@ -11,7 +11,7 @@ "invalid_access_token": "Ugyldig tilgangstoken", "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "oauth_error": "Mottatt ugyldige token data.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "timeout_connect": "Tidsavbrudd oppretter forbindelse" }, "create_entry": { diff --git a/homeassistant/components/google_sheets/translations/no.json b/homeassistant/components/google_sheets/translations/no.json index c4cec211828..3d7a5358376 100644 --- a/homeassistant/components/google_sheets/translations/no.json +++ b/homeassistant/components/google_sheets/translations/no.json @@ -12,7 +12,7 @@ "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "oauth_error": "Mottatt ugyldige token data.", "open_spreadsheet_failure": "Feil under \u00e5pning av regnearket, se feillogg for detaljer", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "timeout_connect": "Tidsavbrudd oppretter forbindelse", "unknown": "Uventet feil" }, diff --git a/homeassistant/components/hive/translations/no.json b/homeassistant/components/hive/translations/no.json index 17241b940c4..120f50733af 100644 --- a/homeassistant/components/hive/translations/no.json +++ b/homeassistant/components/hive/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "unknown_entry": "Kunne ikke finne eksisterende oppf\u00f8ring." }, "error": { diff --git a/homeassistant/components/huawei_lte/translations/no.json b/homeassistant/components/huawei_lte/translations/no.json index f9f16fdf2b2..4261d2af9b2 100644 --- a/homeassistant/components/huawei_lte/translations/no.json +++ b/homeassistant/components/huawei_lte/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_huawei_lte": "Ikke en Huawei LTE-enhet", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "connection_timeout": "Tilkoblingsavbrudd", diff --git a/homeassistant/components/hyperion/translations/no.json b/homeassistant/components/hyperion/translations/no.json index 8fed4ee2437..9fe812601c1 100644 --- a/homeassistant/components/hyperion/translations/no.json +++ b/homeassistant/components/hyperion/translations/no.json @@ -8,7 +8,7 @@ "auth_required_error": "Kan ikke fastsl\u00e5 om autorisasjon er n\u00f8dvendig", "cannot_connect": "Tilkobling mislyktes", "no_id": "Hyperion Ambilight-forekomsten rapporterte ikke ID-en", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/icloud/translations/no.json b/homeassistant/components/icloud/translations/no.json index 662600eac36..1423f117126 100644 --- a/homeassistant/components/icloud/translations/no.json +++ b/homeassistant/components/icloud/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Kontoen er allerede konfigurert", "no_device": "Ingen av enhetene dine har \"Finn min iPhone\" aktivert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning", diff --git a/homeassistant/components/intellifire/translations/no.json b/homeassistant/components/intellifire/translations/no.json index 53cf7495b94..77fdb74a3ab 100644 --- a/homeassistant/components/intellifire/translations/no.json +++ b/homeassistant/components/intellifire/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "not_intellifire_device": "Ikke en IntelliFire-enhet.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "api_error": "Innlogging feilet", diff --git a/homeassistant/components/isy994/translations/no.json b/homeassistant/components/isy994/translations/no.json index e18666d7fc4..813053aa83a 100644 --- a/homeassistant/components/isy994/translations/no.json +++ b/homeassistant/components/isy994/translations/no.json @@ -7,7 +7,7 @@ "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", "invalid_host": "Vertsoppf\u00f8ringen var ikke i fullstendig URL-format, for eksempel http://192.168.10.100:80", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "unknown": "Uventet feil" }, "flow_title": "{name} ( {host} )", diff --git a/homeassistant/components/lacrosse_view/translations/no.json b/homeassistant/components/lacrosse_view/translations/no.json index cd512e6fb86..0b18e06b8c7 100644 --- a/homeassistant/components/lacrosse_view/translations/no.json +++ b/homeassistant/components/lacrosse_view/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning", diff --git a/homeassistant/components/lametric/translations/select.ca.json b/homeassistant/components/lametric/translations/select.ca.json new file mode 100644 index 00000000000..045ad08acf4 --- /dev/null +++ b/homeassistant/components/lametric/translations/select.ca.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "Autom\u00e0tic", + "manual": "Manual" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/select.de.json b/homeassistant/components/lametric/translations/select.de.json new file mode 100644 index 00000000000..1b1a5ab8ce6 --- /dev/null +++ b/homeassistant/components/lametric/translations/select.de.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "Automatisch", + "manual": "Manuell" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/select.es.json b/homeassistant/components/lametric/translations/select.es.json new file mode 100644 index 00000000000..dcf5c796e00 --- /dev/null +++ b/homeassistant/components/lametric/translations/select.es.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "Autom\u00e1tico", + "manual": "Manual" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/select.et.json b/homeassistant/components/lametric/translations/select.et.json new file mode 100644 index 00000000000..69c1bcd94f2 --- /dev/null +++ b/homeassistant/components/lametric/translations/select.et.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "Automaatne", + "manual": "K\u00e4sitsi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/select.hu.json b/homeassistant/components/lametric/translations/select.hu.json new file mode 100644 index 00000000000..231888f3252 --- /dev/null +++ b/homeassistant/components/lametric/translations/select.hu.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "Automatikus", + "manual": "Manu\u00e1lis" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/select.id.json b/homeassistant/components/lametric/translations/select.id.json new file mode 100644 index 00000000000..737f4ac624b --- /dev/null +++ b/homeassistant/components/lametric/translations/select.id.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "Otomatis", + "manual": "Manual" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/select.no.json b/homeassistant/components/lametric/translations/select.no.json new file mode 100644 index 00000000000..a23b382dc49 --- /dev/null +++ b/homeassistant/components/lametric/translations/select.no.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "Automatisk", + "manual": "Manuell" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/select.pl.json b/homeassistant/components/lametric/translations/select.pl.json new file mode 100644 index 00000000000..5b9bf31994d --- /dev/null +++ b/homeassistant/components/lametric/translations/select.pl.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "automatyczny", + "manual": "r\u0119czny" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/select.ru.json b/homeassistant/components/lametric/translations/select.ru.json new file mode 100644 index 00000000000..b9676872659 --- /dev/null +++ b/homeassistant/components/lametric/translations/select.ru.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438", + "manual": "\u0412\u0440\u0443\u0447\u043d\u0443\u044e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/select.zh-Hant.json b/homeassistant/components/lametric/translations/select.zh-Hant.json new file mode 100644 index 00000000000..a7c9c771d68 --- /dev/null +++ b/homeassistant/components/lametric/translations/select.zh-Hant.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "\u81ea\u52d5", + "manual": "\u624b\u52d5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/no.json b/homeassistant/components/lidarr/translations/no.json index 74514056485..23c63f562b5 100644 --- a/homeassistant/components/lidarr/translations/no.json +++ b/homeassistant/components/lidarr/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Tjenesten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/life360/translations/no.json b/homeassistant/components/life360/translations/no.json index 7213a665607..5095ced59f0 100644 --- a/homeassistant/components/life360/translations/no.json +++ b/homeassistant/components/life360/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Kontoen er allerede konfigurert", "invalid_auth": "Ugyldig godkjenning", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "unknown": "Uventet feil" }, "create_entry": { diff --git a/homeassistant/components/litterrobot/translations/no.json b/homeassistant/components/litterrobot/translations/no.json index 6268bdf0ff7..92853ca057c 100644 --- a/homeassistant/components/litterrobot/translations/no.json +++ b/homeassistant/components/litterrobot/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/lyric/translations/no.json b/homeassistant/components/lyric/translations/no.json index e41e3a0c581..1f9ff8bd737 100644 --- a/homeassistant/components/lyric/translations/no.json +++ b/homeassistant/components/lyric/translations/no.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "create_entry": { "default": "Vellykket godkjenning" diff --git a/homeassistant/components/mazda/translations/no.json b/homeassistant/components/mazda/translations/no.json index 875e21e8c04..567e1741a78 100644 --- a/homeassistant/components/mazda/translations/no.json +++ b/homeassistant/components/mazda/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "account_locked": "Kontoen er l\u00e5st. Pr\u00f8v igjen senere.", diff --git a/homeassistant/components/mikrotik/translations/no.json b/homeassistant/components/mikrotik/translations/no.json index baad78fd111..c39ce4becfc 100644 --- a/homeassistant/components/mikrotik/translations/no.json +++ b/homeassistant/components/mikrotik/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/motioneye/translations/no.json b/homeassistant/components/motioneye/translations/no.json index 1d7f3bab29f..91b06e86067 100644 --- a/homeassistant/components/motioneye/translations/no.json +++ b/homeassistant/components/motioneye/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Tjenesten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/myq/translations/no.json b/homeassistant/components/myq/translations/no.json index b43115f6b93..81021c1c0be 100644 --- a/homeassistant/components/myq/translations/no.json +++ b/homeassistant/components/myq/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Tjenesten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/nam/translations/no.json b/homeassistant/components/nam/translations/no.json index f66a6b148e3..758ea3c4aba 100644 --- a/homeassistant/components/nam/translations/no.json +++ b/homeassistant/components/nam/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "device_unsupported": "Enheten st\u00f8ttes ikke.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "reauth_unsuccessful": "Re-autentisering mislyktes. Fjern integrasjonen og konfigurer den p\u00e5 nytt." }, "error": { diff --git a/homeassistant/components/neato/translations/no.json b/homeassistant/components/neato/translations/no.json index d62c8e5d1f1..3c9ce3fb76d 100644 --- a/homeassistant/components/neato/translations/no.json +++ b/homeassistant/components/neato/translations/no.json @@ -5,7 +5,7 @@ "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "create_entry": { "default": "Vellykket godkjenning" diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json index 62514bf34a4..c72577c1744 100644 --- a/homeassistant/components/nest/translations/no.json +++ b/homeassistant/components/nest/translations/no.json @@ -9,7 +9,7 @@ "invalid_access_token": "Ugyldig tilgangstoken", "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", "unknown_authorize_url_generation": "Ukjent feil under generering av en autoriserings-URL." }, diff --git a/homeassistant/components/netatmo/translations/no.json b/homeassistant/components/netatmo/translations/no.json index dc751d3a4b5..cddfa3b3ffb 100644 --- a/homeassistant/components/netatmo/translations/no.json +++ b/homeassistant/components/netatmo/translations/no.json @@ -4,7 +4,7 @@ "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "create_entry": { diff --git a/homeassistant/components/notion/translations/no.json b/homeassistant/components/notion/translations/no.json index b8ffb36e040..deb1a20f3cc 100644 --- a/homeassistant/components/notion/translations/no.json +++ b/homeassistant/components/notion/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning", diff --git a/homeassistant/components/nuki/translations/no.json b/homeassistant/components/nuki/translations/no.json index 1ae4eb03624..0cfb713ba6a 100644 --- a/homeassistant/components/nuki/translations/no.json +++ b/homeassistant/components/nuki/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/octoprint/translations/no.json b/homeassistant/components/octoprint/translations/no.json index 870a5188ff0..fe6cec2cae1 100644 --- a/homeassistant/components/octoprint/translations/no.json +++ b/homeassistant/components/octoprint/translations/no.json @@ -4,7 +4,7 @@ "already_configured": "Enheten er allerede konfigurert", "auth_failed": "Kan ikke hente APAn\u00f8kkel for program", "cannot_connect": "Tilkobling mislyktes", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "unknown": "Uventet feil" }, "error": { diff --git a/homeassistant/components/openexchangerates/translations/no.json b/homeassistant/components/openexchangerates/translations/no.json index fbd3cd7c206..1e810f5a52e 100644 --- a/homeassistant/components/openexchangerates/translations/no.json +++ b/homeassistant/components/openexchangerates/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Tjenesten er allerede konfigurert", "cannot_connect": "Tilkobling mislyktes", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "timeout_connect": "Tidsavbrudd oppretter forbindelse" }, "error": { diff --git a/homeassistant/components/overkiz/translations/no.json b/homeassistant/components/overkiz/translations/no.json index 589b85be025..062cf053fbd 100644 --- a/homeassistant/components/overkiz/translations/no.json +++ b/homeassistant/components/overkiz/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "reauth_wrong_account": "Du kan bare autentisere denne oppf\u00f8ringen p\u00e5 nytt med samme Overkiz-konto og hub" }, "error": { diff --git a/homeassistant/components/picnic/translations/no.json b/homeassistant/components/picnic/translations/no.json index 1ebb4b8dece..eeb6f517802 100644 --- a/homeassistant/components/picnic/translations/no.json +++ b/homeassistant/components/picnic/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/plex/translations/no.json b/homeassistant/components/plex/translations/no.json index c34b4b1c257..3f504ac89c4 100644 --- a/homeassistant/components/plex/translations/no.json +++ b/homeassistant/components/plex/translations/no.json @@ -4,7 +4,7 @@ "all_configured": "Alle knyttet servere som allerede er konfigurert", "already_configured": "Denne Plex-serveren er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "token_request_timeout": "Tidsavbrudd ved innhenting av token", "unknown": "Uventet feil" }, diff --git a/homeassistant/components/powerwall/translations/no.json b/homeassistant/components/powerwall/translations/no.json index 01cf58c6ed4..b6d6c051490 100644 --- a/homeassistant/components/powerwall/translations/no.json +++ b/homeassistant/components/powerwall/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "cannot_connect": "Tilkobling mislyktes", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/prosegur/translations/no.json b/homeassistant/components/prosegur/translations/no.json index 73bacd26c14..6e553ffa1c6 100644 --- a/homeassistant/components/prosegur/translations/no.json +++ b/homeassistant/components/prosegur/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/pushover/translations/no.json b/homeassistant/components/pushover/translations/no.json index 32f733eedcb..8ecd1b83404 100644 --- a/homeassistant/components/pushover/translations/no.json +++ b/homeassistant/components/pushover/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Tjenesten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/pvoutput/translations/no.json b/homeassistant/components/pvoutput/translations/no.json index 899483c7adb..6f64f05bcd8 100644 --- a/homeassistant/components/pvoutput/translations/no.json +++ b/homeassistant/components/pvoutput/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/radarr/translations/no.json b/homeassistant/components/radarr/translations/no.json index 4b6b2adb523..4a86867a3fd 100644 --- a/homeassistant/components/radarr/translations/no.json +++ b/homeassistant/components/radarr/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Tjenesten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/renault/translations/no.json b/homeassistant/components/renault/translations/no.json index 1e53c2718eb..3891d56b928 100644 --- a/homeassistant/components/renault/translations/no.json +++ b/homeassistant/components/renault/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Kontoen er allerede konfigurert", "kamereon_no_account": "Finner ikke Kamereon-konto", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "invalid_credentials": "Ugyldig godkjenning" diff --git a/homeassistant/components/ridwell/translations/no.json b/homeassistant/components/ridwell/translations/no.json index 0e161b5d614..9ed0aa5a853 100644 --- a/homeassistant/components/ridwell/translations/no.json +++ b/homeassistant/components/ridwell/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning", diff --git a/homeassistant/components/samsungtv/translations/no.json b/homeassistant/components/samsungtv/translations/no.json index 3b515608128..049b3c27198 100644 --- a/homeassistant/components/samsungtv/translations/no.json +++ b/homeassistant/components/samsungtv/translations/no.json @@ -7,7 +7,7 @@ "cannot_connect": "Tilkobling mislyktes", "id_missing": "Denne Samsung-enheten har ikke serienummer.", "not_supported": "Denne Samsung-enheten st\u00f8ttes forel\u00f8pig ikke.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "unknown": "Uventet feil" }, "error": { diff --git a/homeassistant/components/sense/translations/no.json b/homeassistant/components/sense/translations/no.json index 004580b5192..2fdf39a0d89 100644 --- a/homeassistant/components/sense/translations/no.json +++ b/homeassistant/components/sense/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/sensibo/translations/no.json b/homeassistant/components/sensibo/translations/no.json index ec40be62e89..02067c773fd 100644 --- a/homeassistant/components/sensibo/translations/no.json +++ b/homeassistant/components/sensibo/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/sharkiq/translations/no.json b/homeassistant/components/sharkiq/translations/no.json index 4454bd940d4..f9fe98cf3a4 100644 --- a/homeassistant/components/sharkiq/translations/no.json +++ b/homeassistant/components/sharkiq/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Kontoen er allerede konfigurert", "cannot_connect": "Tilkobling mislyktes", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "unknown": "Uventet feil" }, "error": { diff --git a/homeassistant/components/shelly/translations/no.json b/homeassistant/components/shelly/translations/no.json index e6cd94ee09a..2f483843e52 100644 --- a/homeassistant/components/shelly/translations/no.json +++ b/homeassistant/components/shelly/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "reauth_unsuccessful": "Re-autentisering mislyktes. Fjern integrasjonen og konfigurer den p\u00e5 nytt.", "unsupported_firmware": "Enheten bruker en ikke-st\u00f8ttet firmwareversjon." }, diff --git a/homeassistant/components/shopping_list/translations/no.json b/homeassistant/components/shopping_list/translations/no.json index 6bad2dd5774..c6754d8a6aa 100644 --- a/homeassistant/components/shopping_list/translations/no.json +++ b/homeassistant/components/shopping_list/translations/no.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "\u00d8nsker du \u00e5 konfigurere handleliste?", + "description": "\u00d8nsker du \u00e5 konfigurere handlelisten?", "title": "Handleliste" } } diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json index e2c369e9dd8..5d6f81c4444 100644 --- a/homeassistant/components/simplisafe/translations/no.json +++ b/homeassistant/components/simplisafe/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Denne SimpliSafe-kontoen er allerede i bruk.", "email_2fa_timed_out": "Tidsavbrudd mens du ventet p\u00e5 e-postbasert tofaktorautentisering.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "wrong_account": "Oppgitt brukerlegitimasjon samsvarer ikke med denne SimpliSafe-kontoen." }, "error": { diff --git a/homeassistant/components/skybell/translations/no.json b/homeassistant/components/skybell/translations/no.json index 25735dcf804..c152b2ae18e 100644 --- a/homeassistant/components/skybell/translations/no.json +++ b/homeassistant/components/skybell/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/sleepiq/translations/no.json b/homeassistant/components/sleepiq/translations/no.json index ffe74f7048a..52a8ef89165 100644 --- a/homeassistant/components/sleepiq/translations/no.json +++ b/homeassistant/components/sleepiq/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/smarttub/translations/no.json b/homeassistant/components/smarttub/translations/no.json index 10b3ccc421d..2689787f725 100644 --- a/homeassistant/components/smarttub/translations/no.json +++ b/homeassistant/components/smarttub/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning" diff --git a/homeassistant/components/snooz/translations/ca.json b/homeassistant/components/snooz/translations/ca.json new file mode 100644 index 00000000000..0cd4571dc9d --- /dev/null +++ b/homeassistant/components/snooz/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/snooz/translations/de.json b/homeassistant/components/snooz/translations/de.json new file mode 100644 index 00000000000..75b8acddc86 --- /dev/null +++ b/homeassistant/components/snooz/translations/de.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "flow_title": "{name}", + "progress": { + "wait_for_pairing_mode": "Um die Einrichtung abzuschlie\u00dfen, versetze das Ger\u00e4t in den Pairing-Modus.\n\n### So rufst du den Pairing-Modus auf:\n1. Beende die SNOOZ Mobile-Apps zwangsweise.\n2. Dr\u00fccke und halte die Einschalttaste am Ger\u00e4t. Lasse sie los, wenn die Lichter zu blinken beginnen (ca. 5 Sekunden)." + }, + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "pairing_timeout": { + "description": "Das Ger\u00e4t konnte nicht in den Pairing-Modus wechseln. Klicke auf Senden, um es erneut zu versuchen.\n\n### Fehlerbehebung\n1. Stelle sicher, dass das Ger\u00e4t nicht mit der mobilen App verbunden ist.\n2. Trenne das Ger\u00e4t f\u00fcr 5 Sekunden vom Stromnetz und schlie\u00dfe es dann wieder an." + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/snooz/translations/es.json b/homeassistant/components/snooz/translations/es.json new file mode 100644 index 00000000000..e19e5a2af15 --- /dev/null +++ b/homeassistant/components/snooz/translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red" + }, + "flow_title": "{name}", + "progress": { + "wait_for_pairing_mode": "Para completar la configuraci\u00f3n, pon este dispositivo en modo de emparejamiento. \n\n### C\u00f3mo ingresar al modo de emparejamiento\n1. Fuerza el cierre de las aplicaciones m\u00f3viles de SNOOZ.\n2. Manten pulsado el bot\u00f3n de encendido del dispositivo. Suelta cuando las luces comiencen a parpadear (aproximadamente 5 segundos)." + }, + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "pairing_timeout": { + "description": "El dispositivo no entr\u00f3 al modo de emparejamiento. Haz clic en Enviar para volver a intentarlo. \n\n### Soluci\u00f3n de problemas\n1. Verifica que el dispositivo no est\u00e9 conectado a la aplicaci\u00f3n m\u00f3vil.\n2. Desenchufa el dispositivo durante 5 segundos y luego vuelve a enchufarlo." + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/snooz/translations/et.json b/homeassistant/components/snooz/translations/et.json new file mode 100644 index 00000000000..53ec0cec70b --- /dev/null +++ b/homeassistant/components/snooz/translations/et.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud" + }, + "flow_title": "{name}", + "progress": { + "wait_for_pairing_mode": "Seadistamise l\u00f5petamiseks l\u00fclita see seade sidumisre\u017eiimi. \n\n ### Sidumisre\u017eiimi sisenemine\n 1. Sunni SNOOZi mobiilirakendustest v\u00e4ljuma.\n 2. Vajuta ja hoia all seadme toitenuppu. Vabasta kui tuled hakkavad vilkuma (umbes 5 sekundit)." + }, + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "pairing_timeout": { + "description": "Seade ei sisenenud sidumisre\u017eiimi. Uuesti proovimiseks kl\u00f5psa nuppu Esita. \n\n ### Veaotsing\n 1. Veendu, et seade pole mobiilirakendusega \u00fchendatud.\n 2. \u00dchenda seade 5 sekundiks lahti, seej\u00e4rel \u00fchenda see uuesti." + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/snooz/translations/hu.json b/homeassistant/components/snooz/translations/hu.json new file mode 100644 index 00000000000..900b8670fa3 --- /dev/null +++ b/homeassistant/components/snooz/translations/hu.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "flow_title": "{name}", + "progress": { + "wait_for_pairing_mode": "A be\u00e1ll\u00edt\u00e1s befejez\u00e9s\u00e9hez \u00e1ll\u00edtsa a k\u00e9sz\u00fcl\u00e9ket p\u00e1ros\u00edt\u00e1si m\u00f3dba.\n\n### Hogyan l\u00e9phet be a p\u00e1ros\u00edt\u00e1si m\u00f3dba\n1. A SNOOZ mobilalkalmaz\u00e1sokat k\u00e9nyszer\u00edtve \u00e1ll\u00edtsa le.\n2. Nyomja meg \u00e9s tartsa lenyomva a k\u00e9sz\u00fcl\u00e9k bekapcsol\u00f3gombj\u00e1t. Engedje el, amikor a f\u00e9nyek villogni kezdenek (k\u00f6r\u00fclbel\u00fcl 5 m\u00e1sodperc)." + }, + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "pairing_timeout": { + "description": "A k\u00e9sz\u00fcl\u00e9k nem l\u00e9pett p\u00e1ros\u00edt\u00e1si m\u00f3dba. Kattintson a K\u00fcld\u00e9s gombra az \u00fajb\u00f3li pr\u00f3b\u00e1lkoz\u00e1shoz.\n\n### Hibaelh\u00e1r\u00edt\u00e1s\n1. Ellen\u0151rizze, hogy a k\u00e9sz\u00fcl\u00e9k nincs-e csatlakoztatva a mobilalkalmaz\u00e1shoz.\n2. H\u00fazza ki a k\u00e9sz\u00fcl\u00e9ket 5 m\u00e1sodpercre, majd dugja vissza." + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/snooz/translations/id.json b/homeassistant/components/snooz/translations/id.json new file mode 100644 index 00000000000..a62bff72f1d --- /dev/null +++ b/homeassistant/components/snooz/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "flow_title": "{name}", + "progress": { + "wait_for_pairing_mode": "Untuk menyelesaikan penyiapan, siapkan perangkat ini dalam mode pairing.\n\n### Cara memasuki mode pairing\n1. Keluar paksa dari aplikasi seluler SNOOZ.\n2. Tekan dan tahan tombol daya pada perangkat. Lepaskan saat lampu mulai berkedip (kira-kira 5 detik)." + }, + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "pairing_timeout": { + "description": "Perangkat tidak masuk ke mode pairing. Klik Kirim untuk mencoba lagi.\n\n### Pemecahan Masalah\n1. Periksa apakah perangkat sudah tidak terhubung ke aplikasi seluler.\n2. Cabut perangkat selama 5 detik, kemudian colokkan kembali." + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/snooz/translations/no.json b/homeassistant/components/snooz/translations/no.json new file mode 100644 index 00000000000..c16e7ce6d94 --- /dev/null +++ b/homeassistant/components/snooz/translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" + }, + "flow_title": "{name}", + "progress": { + "wait_for_pairing_mode": "For \u00e5 fullf\u00f8re oppsettet, sett denne enheten i sammenkoblingsmodus. \n\n ### Hvordan g\u00e5 inn i paringsmodus\n 1. Tving avslutning av SNOOZ-mobilapper.\n 2. Trykk og hold inne str\u00f8mknappen p\u00e5 enheten. Slipp n\u00e5r lysene begynner \u00e5 blinke (omtrent 5 sekunder)." + }, + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, + "pairing_timeout": { + "description": "Enheten gikk ikke i sammenkoblingsmodus. Klikk p\u00e5 Send for \u00e5 pr\u00f8ve igjen. \n\n ### Feils\u00f8king\n 1. Sjekk at enheten ikke er koblet til mobilappen.\n 2. Koble fra enheten i 5 sekunder, og koble den deretter til igjen." + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/snooz/translations/pl.json b/homeassistant/components/snooz/translations/pl.json new file mode 100644 index 00000000000..dfb76c73eb5 --- /dev/null +++ b/homeassistant/components/snooz/translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "flow_title": "{name}", + "progress": { + "wait_for_pairing_mode": "Aby zako\u0144czy\u0107 konfiguracj\u0119, prze\u0142\u0105cz to urz\u0105dzenie w tryb parowania. \n\n### Jak wej\u015b\u0107 w tryb parowania\n1. Wymu\u015b zamkni\u0119cie aplikacji mobilnych SNOOZ.\n2. Naci\u015bnij i przytrzymaj przycisk zasilania na urz\u0105dzeniu. Zwolnij, gdy kontrolki zaczn\u0105 miga\u0107 (oko\u0142o 5 sekund)." + }, + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "pairing_timeout": { + "description": "Urz\u0105dzenie nie wesz\u0142o w tryb parowania. Kliknij Zatwierd\u017a, aby spr\u00f3bowa\u0107 ponownie. \n\n### Rozwi\u0105zywanie problem\u00f3w\n1. Sprawd\u017a, czy urz\u0105dzenie nie jest po\u0142\u0105czone z aplikacj\u0105 mobiln\u0105.\n2. Od\u0142\u0105cz urz\u0105dzenie na 5 sekund, a nast\u0119pnie pod\u0142\u0105cz je ponownie." + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/snooz/translations/ru.json b/homeassistant/components/snooz/translations/ru.json new file mode 100644 index 00000000000..3488392c218 --- /dev/null +++ b/homeassistant/components/snooz/translations/ru.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/snooz/translations/zh-Hant.json b/homeassistant/components/snooz/translations/zh-Hant.json new file mode 100644 index 00000000000..adfec147839 --- /dev/null +++ b/homeassistant/components/snooz/translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "flow_title": "{name}", + "progress": { + "wait_for_pairing_mode": "\u6b32\u5b8c\u6210\u8a2d\u5b9a\u3001\u5148\u8b93\u88dd\u7f6e\u9032\u5165\u914d\u5c0d\u6a21\u5f0f\u3002\n\n### \u5982\u4f55\u9032\u5165\u914d\u5c0d\u6a21\u5f0f\n1. \u5f37\u5236\u9000\u51fa SNOOZ \u624b\u6a5f App\u3002\n2. \u6309\u4f4f\u88dd\u7f6e\u4e0a\u7684\u96fb\u6e90\u9375\u4e0d\u653e\u3001\u7576\u71c8\u865f\u958b\u59cb\u9583\u8000\u6642\u653e\u958b\uff08\u5927\u7d04 5 \u79d2\uff09\u3002" + }, + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "pairing_timeout": { + "description": "\u88dd\u7f6e\u4e26\u672a\u9032\u5165\u914d\u5c0d\u6a21\u5f0f\u3001\u9ede\u9078\u50b3\u9001\u518d\u8a66\u4e00\u6b21\u3002\n\n### \u554f\u984c\u6392\u9664\n1. \u6aa2\u67e5\u88dd\u7f6e\u4e26\u672a\u8207\u624b\u6a5f App \u9023\u7dda\u3002\n2. \u62d4\u4e0b\u88dd\u7f6e\u7b49\u5019 5 \u79d2\u3001\u7136\u5f8c\u518d\u63d2\u4e0a\u3002" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/no.json b/homeassistant/components/sonarr/translations/no.json index 3d64a9199a4..e51a76b5918 100644 --- a/homeassistant/components/sonarr/translations/no.json +++ b/homeassistant/components/sonarr/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Tjenesten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "unknown": "Uventet feil" }, "error": { diff --git a/homeassistant/components/steam_online/translations/no.json b/homeassistant/components/steam_online/translations/no.json index 08defe9e2be..d321a4c0843 100644 --- a/homeassistant/components/steam_online/translations/no.json +++ b/homeassistant/components/steam_online/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Tjenesten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/synology_dsm/translations/no.json b/homeassistant/components/synology_dsm/translations/no.json index 89ca80b168e..8ce16f6019d 100644 --- a/homeassistant/components/synology_dsm/translations/no.json +++ b/homeassistant/components/synology_dsm/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "reconfigure_successful": "Omkonfigurasjonen var vellykket" }, "error": { diff --git a/homeassistant/components/system_bridge/translations/no.json b/homeassistant/components/system_bridge/translations/no.json index 22ab7f91a57..23b7aaba767 100644 --- a/homeassistant/components/system_bridge/translations/no.json +++ b/homeassistant/components/system_bridge/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "unknown": "Uventet feil" }, "error": { diff --git a/homeassistant/components/tailscale/translations/no.json b/homeassistant/components/tailscale/translations/no.json index 5c1ae4c6bc0..609589aec6e 100644 --- a/homeassistant/components/tailscale/translations/no.json +++ b/homeassistant/components/tailscale/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/tankerkoenig/translations/no.json b/homeassistant/components/tankerkoenig/translations/no.json index 369ac4d3ce4..f0eac9a8f0e 100644 --- a/homeassistant/components/tankerkoenig/translations/no.json +++ b/homeassistant/components/tankerkoenig/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Plasseringen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning", diff --git a/homeassistant/components/tautulli/translations/no.json b/homeassistant/components/tautulli/translations/no.json index 00a5c8c943a..0528a97beb9 100644 --- a/homeassistant/components/tautulli/translations/no.json +++ b/homeassistant/components/tautulli/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Tjenesten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { diff --git a/homeassistant/components/tile/translations/no.json b/homeassistant/components/tile/translations/no.json index c449beb2382..4d6229b09fb 100644 --- a/homeassistant/components/tile/translations/no.json +++ b/homeassistant/components/tile/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning" diff --git a/homeassistant/components/totalconnect/translations/no.json b/homeassistant/components/totalconnect/translations/no.json index 4ea6b791b23..a263d3004c1 100644 --- a/homeassistant/components/totalconnect/translations/no.json +++ b/homeassistant/components/totalconnect/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Kontoen er allerede konfigurert", "no_locations": "Ingen plasseringer er tilgjengelige for denne brukeren, sjekk TotalConnect-innstillingene", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning", diff --git a/homeassistant/components/tractive/translations/no.json b/homeassistant/components/tractive/translations/no.json index a768b453848..3e5061e027d 100644 --- a/homeassistant/components/tractive/translations/no.json +++ b/homeassistant/components/tractive/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "reauth_failed_existing": "Kunne ikke oppdatere konfigurasjonsoppf\u00f8ringen. Fjern integrasjonen og sett den opp igjen.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning", diff --git a/homeassistant/components/trafikverket_ferry/translations/no.json b/homeassistant/components/trafikverket_ferry/translations/no.json index 4cc6286d78f..bff0bcd45e4 100644 --- a/homeassistant/components/trafikverket_ferry/translations/no.json +++ b/homeassistant/components/trafikverket_ferry/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/trafikverket_train/translations/no.json b/homeassistant/components/trafikverket_train/translations/no.json index 12feb2f6abf..37d771628bd 100644 --- a/homeassistant/components/trafikverket_train/translations/no.json +++ b/homeassistant/components/trafikverket_train/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/transmission/translations/no.json b/homeassistant/components/transmission/translations/no.json index fe15e4adc43..dfe188f6e3b 100644 --- a/homeassistant/components/transmission/translations/no.json +++ b/homeassistant/components/transmission/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/unifi/translations/no.json b/homeassistant/components/unifi/translations/no.json index 2253bbd5023..339f39ff537 100644 --- a/homeassistant/components/unifi/translations/no.json +++ b/homeassistant/components/unifi/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "UniFi Network-nettstedet er allerede konfigurert", "configuration_updated": "Konfigurasjonen er oppdatert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "faulty_credentials": "Ugyldig godkjenning", diff --git a/homeassistant/components/uptimerobot/translations/no.json b/homeassistant/components/uptimerobot/translations/no.json index df7a7f8045a..e3cbe428b64 100644 --- a/homeassistant/components/uptimerobot/translations/no.json +++ b/homeassistant/components/uptimerobot/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Kontoen er allerede konfigurert", "reauth_failed_existing": "Kunne ikke oppdatere konfigurasjonsoppf\u00f8ringen. Fjern integrasjonen og sett den opp igjen.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "unknown": "Uventet feil" }, "error": { diff --git a/homeassistant/components/verisure/translations/no.json b/homeassistant/components/verisure/translations/no.json index 195f8aadd3d..4c29acd5a32 100644 --- a/homeassistant/components/verisure/translations/no.json +++ b/homeassistant/components/verisure/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning", diff --git a/homeassistant/components/vlc_telnet/translations/no.json b/homeassistant/components/vlc_telnet/translations/no.json index 9becf574700..d3e8a3005d7 100644 --- a/homeassistant/components/vlc_telnet/translations/no.json +++ b/homeassistant/components/vlc_telnet/translations/no.json @@ -4,7 +4,7 @@ "already_configured": "Tjenesten er allerede konfigurert", "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_successful": "Re-autentisering var vellykket", "unknown": "Uventet feil" }, "error": { diff --git a/homeassistant/components/volvooncall/translations/no.json b/homeassistant/components/volvooncall/translations/no.json index 2d60c5983fc..48639f07b67 100644 --- a/homeassistant/components/volvooncall/translations/no.json +++ b/homeassistant/components/volvooncall/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning", diff --git a/homeassistant/components/wallbox/translations/no.json b/homeassistant/components/wallbox/translations/no.json index 498362fad1d..c4cf220e5e5 100644 --- a/homeassistant/components/wallbox/translations/no.json +++ b/homeassistant/components/wallbox/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/watttime/translations/no.json b/homeassistant/components/watttime/translations/no.json index 19ec82e863c..5b94a79bad2 100644 --- a/homeassistant/components/watttime/translations/no.json +++ b/homeassistant/components/watttime/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning", diff --git a/homeassistant/components/xiaomi_ble/translations/no.json b/homeassistant/components/xiaomi_ble/translations/no.json index ff428d248d1..46a8158cad9 100644 --- a/homeassistant/components/xiaomi_ble/translations/no.json +++ b/homeassistant/components/xiaomi_ble/translations/no.json @@ -7,7 +7,7 @@ "expected_24_characters": "Forventet en heksadesimal bindingsn\u00f8kkel p\u00e5 24 tegn.", "expected_32_characters": "Forventet en heksadesimal bindingsn\u00f8kkel p\u00e5 32 tegn.", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "decryption_failed": "Den oppgitte bindingsn\u00f8kkelen fungerte ikke, sensordata kunne ikke dekrypteres. Vennligst sjekk det og pr\u00f8v igjen.", diff --git a/homeassistant/components/xiaomi_miio/translations/no.json b/homeassistant/components/xiaomi_miio/translations/no.json index 3d831df207c..fc39a454eaf 100644 --- a/homeassistant/components/xiaomi_miio/translations/no.json +++ b/homeassistant/components/xiaomi_miio/translations/no.json @@ -5,7 +5,7 @@ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "incomplete_info": "Ufullstendig informasjon til installasjonsenheten, ingen vert eller token leveres.", "not_xiaomi_miio": "Enheten st\u00f8ttes (enn\u00e5) ikke av Xiaomi Miio.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/yale_smart_alarm/translations/no.json b/homeassistant/components/yale_smart_alarm/translations/no.json index 579f61f2d71..8299c80ebc2 100644 --- a/homeassistant/components/yale_smart_alarm/translations/no.json +++ b/homeassistant/components/yale_smart_alarm/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/yolink/translations/no.json b/homeassistant/components/yolink/translations/no.json index b5e26ac910d..2482a294ce1 100644 --- a/homeassistant/components/yolink/translations/no.json +++ b/homeassistant/components/yolink/translations/no.json @@ -7,7 +7,7 @@ "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", "oauth_error": "Mottatt ugyldige token data.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Re-autentisering var vellykket" }, "create_entry": { "default": "Vellykket godkjenning" diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json index e200e086444..fb3c7e1a69d 100644 --- a/homeassistant/components/zwave_js/translations/de.json +++ b/homeassistant/components/zwave_js/translations/de.json @@ -10,7 +10,8 @@ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "cannot_connect": "Verbindung fehlgeschlagen", "discovery_requires_supervisor": "Discovery erfordert den Supervisor.", - "not_zwave_device": "Das erkannte Ger\u00e4t ist kein Z-Wave-Ger\u00e4t." + "not_zwave_device": "Das erkannte Ger\u00e4t ist kein Z-Wave-Ger\u00e4t.", + "not_zwave_js_addon": "Das entdeckte Add-on ist nicht das offizielle Z-Wave JS-Add-on." }, "error": { "addon_start_failed": "Fehler beim Starten des Z-Wave JS Add-Ons. \u00dcberpr\u00fcfe die Konfiguration.", diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index 9224e27d90b..3d288c3ebae 100644 --- a/homeassistant/components/zwave_js/translations/en.json +++ b/homeassistant/components/zwave_js/translations/en.json @@ -10,7 +10,8 @@ "already_in_progress": "Configuration flow is already in progress", "cannot_connect": "Failed to connect", "discovery_requires_supervisor": "Discovery requires the supervisor.", - "not_zwave_device": "Discovered device is not a Z-Wave device." + "not_zwave_device": "Discovered device is not a Z-Wave device.", + "not_zwave_js_addon": "Discovered add-on is not the official Z-Wave JS add-on." }, "error": { "addon_start_failed": "Failed to start the Z-Wave JS add-on. Check the configuration.", diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json index ff4d48f3f21..73da28f6d1e 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -10,7 +10,8 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", "discovery_requires_supervisor": "El descubrimiento requiere del supervisor.", - "not_zwave_device": "El dispositivo descubierto no es un dispositivo Z-Wave." + "not_zwave_device": "El dispositivo descubierto no es un dispositivo Z-Wave.", + "not_zwave_js_addon": "El complemento descubierto no es el complemento oficial de Z-Wave JS." }, "error": { "addon_start_failed": "No se pudo iniciar el complemento Z-Wave JS. Comprueba la configuraci\u00f3n.", diff --git a/homeassistant/components/zwave_js/translations/id.json b/homeassistant/components/zwave_js/translations/id.json index 1aa3c5258f5..c8fe3f87f66 100644 --- a/homeassistant/components/zwave_js/translations/id.json +++ b/homeassistant/components/zwave_js/translations/id.json @@ -10,7 +10,8 @@ "already_in_progress": "Alur konfigurasi sedang berlangsung", "cannot_connect": "Gagal terhubung", "discovery_requires_supervisor": "Fitur penemuan membutuhkan supervisor.", - "not_zwave_device": "Perangkat yang ditemukan bukan perangkat Z-Wave." + "not_zwave_device": "Perangkat yang ditemukan bukan perangkat Z-Wave.", + "not_zwave_js_addon": "Add-on yang ditemukan bukanlah add-on Z-Wave JS resmi." }, "error": { "addon_start_failed": "Gagal memulai add-on Z-Wave JS. Periksa konfigurasi.", diff --git a/homeassistant/components/zwave_js/translations/no.json b/homeassistant/components/zwave_js/translations/no.json index 6e9ae85cd74..7c3cec3f6f9 100644 --- a/homeassistant/components/zwave_js/translations/no.json +++ b/homeassistant/components/zwave_js/translations/no.json @@ -10,7 +10,8 @@ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "cannot_connect": "Tilkobling mislyktes", "discovery_requires_supervisor": "Oppdagelsen krever veilederen.", - "not_zwave_device": "Oppdaget enhet er ikke en Z-Wave-enhet." + "not_zwave_device": "Oppdaget enhet er ikke en Z-Wave-enhet.", + "not_zwave_js_addon": "Oppdaget tillegg er ikke det offisielle Z-Wave JS tillegget." }, "error": { "addon_start_failed": "Kunne ikke starte Z-Wave JS-tillegg. Sjekk konfigurasjonen.", diff --git a/homeassistant/components/zwave_js/translations/pl.json b/homeassistant/components/zwave_js/translations/pl.json index a4d5519491b..2028a8a1229 100644 --- a/homeassistant/components/zwave_js/translations/pl.json +++ b/homeassistant/components/zwave_js/translations/pl.json @@ -10,7 +10,8 @@ "already_in_progress": "Konfiguracja jest ju\u017c w toku", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "discovery_requires_supervisor": "Wykrywanie wymaga Supervisora.", - "not_zwave_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem Z-Wave." + "not_zwave_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem Z-Wave.", + "not_zwave_js_addon": "Wykryty dodatek nie jest oficjalnym dodatkiem Z-Wave JS." }, "error": { "addon_start_failed": "Nie uda\u0142o si\u0119 uruchomi\u0107 dodatku Z-Wave JS. Sprawd\u017a konfiguracj\u0119", From 46794f7a5d7697632eb106f14d3aa6c5615dc736 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 12 Oct 2022 00:20:32 -0500 Subject: [PATCH 381/985] Add sensor platform to Jellyfin (#79966) --- homeassistant/components/jellyfin/__init__.py | 30 +- homeassistant/components/jellyfin/const.py | 9 +- .../components/jellyfin/coordinator.py | 69 + homeassistant/components/jellyfin/entity.py | 33 + .../components/jellyfin/media_source.py | 11 +- homeassistant/components/jellyfin/models.py | 16 + homeassistant/components/jellyfin/sensor.py | 74 + tests/components/jellyfin/conftest.py | 15 + .../jellyfin/fixtures/sessions.json | 1762 +++++++++++++++++ tests/components/jellyfin/test_sensor.py | 51 + 10 files changed, 2051 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/jellyfin/coordinator.py create mode 100644 homeassistant/components/jellyfin/entity.py create mode 100644 homeassistant/components/jellyfin/models.py create mode 100644 homeassistant/components/jellyfin/sensor.py create mode 100644 tests/components/jellyfin/fixtures/sessions.json create mode 100644 tests/components/jellyfin/test_sensor.py diff --git a/homeassistant/components/jellyfin/__init__.py b/homeassistant/components/jellyfin/__init__.py index c0839cafa09..e1d8600530f 100644 --- a/homeassistant/components/jellyfin/__init__.py +++ b/homeassistant/components/jellyfin/__init__.py @@ -1,14 +1,14 @@ """The Jellyfin integration.""" -import logging +from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input -from .const import CONF_CLIENT_DEVICE_ID, DATA_CLIENT, DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import CONF_CLIENT_DEVICE_ID, DOMAIN, LOGGER, PLATFORMS +from .coordinator import JellyfinDataUpdateCoordinator, SessionsDataUpdateCoordinator +from .models import JellyfinData async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -26,14 +26,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: - await validate_input(hass, dict(entry.data), client) + _, connect_result = await validate_input(hass, dict(entry.data), client) except CannotConnect as ex: raise ConfigEntryNotReady("Cannot connect to Jellyfin server") from ex except InvalidAuth: - _LOGGER.error("Failed to login to Jellyfin server") + LOGGER.error("Failed to login to Jellyfin server") return False - else: - hass.data[DOMAIN][entry.entry_id] = {DATA_CLIENT: client} + + server_info: dict[str, Any] = connect_result["Servers"][0] + + coordinators: dict[str, JellyfinDataUpdateCoordinator[Any]] = { + "sessions": SessionsDataUpdateCoordinator(hass, client, server_info), + } + + for coordinator in coordinators.values(): + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id] = JellyfinData( + jellyfin_client=client, + coordinators=coordinators, + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/jellyfin/const.py b/homeassistant/components/jellyfin/const.py index d11ae195892..67956899cab 100644 --- a/homeassistant/components/jellyfin/const.py +++ b/homeassistant/components/jellyfin/const.py @@ -1,8 +1,8 @@ """Constants for the Jellyfin integration.""" - +import logging from typing import Final -from homeassistant.const import __version__ as hass_version +from homeassistant.const import Platform, __version__ as hass_version DOMAIN: Final = "jellyfin" @@ -13,7 +13,7 @@ COLLECTION_TYPE_MUSIC: Final = "music" CONF_CLIENT_DEVICE_ID: Final = "client_device_id" -DATA_CLIENT: Final = "client" +DEFAULT_NAME: Final = "Jellyfin" ITEM_KEY_COLLECTION_TYPE: Final = "CollectionType" ITEM_KEY_ID: Final = "Id" @@ -43,3 +43,6 @@ SUPPORTED_COLLECTION_TYPES: Final = [COLLECTION_TYPE_MUSIC, COLLECTION_TYPE_MOVI USER_APP_NAME: Final = "Home Assistant" USER_AGENT: Final = f"Home-Assistant/{CLIENT_VERSION}" + +PLATFORMS = [Platform.SENSOR] +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/jellyfin/coordinator.py b/homeassistant/components/jellyfin/coordinator.py new file mode 100644 index 00000000000..6bf913747ab --- /dev/null +++ b/homeassistant/components/jellyfin/coordinator.py @@ -0,0 +1,69 @@ +"""Data update coordinator for the Jellyfin integration.""" +from __future__ import annotations + +from abc import abstractmethod +from datetime import timedelta +from typing import Any, TypeVar, Union + +from jellyfin_apiclient_python import JellyfinClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, LOGGER + +JellyfinDataT = TypeVar( + "JellyfinDataT", + bound=Union[ + dict[str, dict[str, Any]], + dict[str, Any], + ], +) + + +class JellyfinDataUpdateCoordinator(DataUpdateCoordinator[JellyfinDataT]): + """Data update coordinator for the Jellyfin integration.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + api_client: JellyfinClient, + system_info: dict[str, Any], + ) -> None: + """Initialize the coordinator.""" + super().__init__( + hass=hass, + logger=LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=30), + ) + self.api_client: JellyfinClient = api_client + self.server_id: str = system_info["Id"] + self.server_name: str = system_info["Name"] + self.server_version: str | None = system_info.get("Version") + + async def _async_update_data(self) -> JellyfinDataT: + """Get the latest data from Jellyfin.""" + return await self._fetch_data() + + @abstractmethod + async def _fetch_data(self) -> JellyfinDataT: + """Fetch the actual data.""" + raise NotImplementedError + + +class SessionsDataUpdateCoordinator( + JellyfinDataUpdateCoordinator[dict[str, dict[str, Any]]] +): + """Sessions update coordinator for Jellyfin.""" + + async def _fetch_data(self) -> dict: + """Fetch the data.""" + sessions = await self.hass.async_add_executor_job( + self.api_client.jellyfin.sessions + ) + + return {session["Id"]: session for session in sessions} diff --git a/homeassistant/components/jellyfin/entity.py b/homeassistant/components/jellyfin/entity.py new file mode 100644 index 00000000000..eb74b5d5c51 --- /dev/null +++ b/homeassistant/components/jellyfin/entity.py @@ -0,0 +1,33 @@ +"""Base Entity for Jellyfin.""" +from __future__ import annotations + +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DEFAULT_NAME, DOMAIN +from .coordinator import JellyfinDataT, JellyfinDataUpdateCoordinator + + +class JellyfinEntity(CoordinatorEntity[JellyfinDataUpdateCoordinator[JellyfinDataT]]): + """Defines a base Jellyfin entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: JellyfinDataUpdateCoordinator[JellyfinDataT], + description: EntityDescription, + ) -> None: + """Initialize the Jellyfin entity.""" + super().__init__(coordinator) + self.coordinator = coordinator + self.entity_description = description + self._attr_unique_id = f"{coordinator.server_id}-{description.key}" + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, self.coordinator.server_id)}, + manufacturer=DEFAULT_NAME, + name=self.coordinator.server_name, + sw_version=self.coordinator.server_version, + ) diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index 2cb211acb9b..dfb5bd82924 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -1,7 +1,6 @@ """The Media Source implementation for the Jellyfin integration.""" from __future__ import annotations -import logging import mimetypes from typing import Any @@ -20,7 +19,6 @@ from homeassistant.core import HomeAssistant from .const import ( COLLECTION_TYPE_MOVIES, COLLECTION_TYPE_MUSIC, - DATA_CLIENT, DOMAIN, ITEM_KEY_COLLECTION_TYPE, ITEM_KEY_ID, @@ -41,19 +39,16 @@ from .const import ( MEDIA_TYPE_VIDEO, SUPPORTED_COLLECTION_TYPES, ) - -_LOGGER = logging.getLogger(__name__) +from .models import JellyfinData async def async_get_media_source(hass: HomeAssistant) -> MediaSource: """Set up Jellyfin media source.""" # Currently only a single Jellyfin server is supported entry = hass.config_entries.async_entries(DOMAIN)[0] + jellyfin_data: JellyfinData = hass.data[DOMAIN][entry.entry_id] - data = hass.data[DOMAIN][entry.entry_id] - client: JellyfinClient = data[DATA_CLIENT] - - return JellyfinSource(hass, client) + return JellyfinSource(hass, jellyfin_data.jellyfin_client) class JellyfinSource(MediaSource): diff --git a/homeassistant/components/jellyfin/models.py b/homeassistant/components/jellyfin/models.py new file mode 100644 index 00000000000..913e40e14d5 --- /dev/null +++ b/homeassistant/components/jellyfin/models.py @@ -0,0 +1,16 @@ +"""Models for the Jellyfin integration.""" +from __future__ import annotations + +from dataclasses import dataclass + +from jellyfin_apiclient_python import JellyfinClient + +from .coordinator import JellyfinDataUpdateCoordinator + + +@dataclass +class JellyfinData: + """Data for the Jellyfin integration.""" + + jellyfin_client: JellyfinClient + coordinators: dict[str, JellyfinDataUpdateCoordinator] diff --git a/homeassistant/components/jellyfin/sensor.py b/homeassistant/components/jellyfin/sensor.py new file mode 100644 index 00000000000..1957adfc6eb --- /dev/null +++ b/homeassistant/components/jellyfin/sensor.py @@ -0,0 +1,74 @@ +"""Support for Jellyfin sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .const import DOMAIN +from .coordinator import JellyfinDataT +from .entity import JellyfinEntity +from .models import JellyfinData + + +@dataclass +class JellyfinSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[JellyfinDataT], StateType] + + +@dataclass +class JellyfinSensorEntityDescription( + SensorEntityDescription, JellyfinSensorEntityDescriptionMixin +): + """Describes Jellyfin sensor entity.""" + + +def _count_now_playing(data: JellyfinDataT) -> int: + """Count the number of now playing.""" + session_ids = [ + sid for (sid, session) in data.items() if "NowPlayingItem" in session + ] + + return len(session_ids) + + +SENSOR_TYPES: dict[str, JellyfinSensorEntityDescription] = { + "sessions": JellyfinSensorEntityDescription( + key="watching", + icon="mdi:television-play", + native_unit_of_measurement="Watching", + value_fn=_count_now_playing, + ) +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Jellyfin sensor based on a config entry.""" + jellyfin_data: JellyfinData = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + JellyfinSensor(jellyfin_data.coordinators[coordinator_type], description) + for coordinator_type, description in SENSOR_TYPES.items() + ) + + +class JellyfinSensor(JellyfinEntity, SensorEntity): + """Defines a Jellyfin sensor entity.""" + + entity_description: JellyfinSensorEntityDescription + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/tests/components/jellyfin/conftest.py b/tests/components/jellyfin/conftest.py index 4d32e3a72ef..65b66e5b663 100644 --- a/tests/components/jellyfin/conftest.py +++ b/tests/components/jellyfin/conftest.py @@ -12,6 +12,7 @@ import pytest from homeassistant.components.jellyfin.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME +from homeassistant.core import HomeAssistant from . import load_json_fixture from .const import TEST_PASSWORD, TEST_URL, TEST_USERNAME @@ -70,6 +71,7 @@ def mock_api() -> MagicMock: """Return a mocked API.""" jf_api = create_autospec(API) jf_api.get_user_settings.return_value = load_json_fixture("get-user-settings.json") + jf_api.sessions.return_value = load_json_fixture("sessions.json") return jf_api @@ -106,3 +108,16 @@ def mock_jellyfin(mock_client: MagicMock) -> Generator[None, MagicMock, None]: jf.get_client.return_value = mock_client yield jf + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_jellyfin: MagicMock +) -> MockConfigEntry: + """Set up the Jellyfin integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/jellyfin/fixtures/sessions.json b/tests/components/jellyfin/fixtures/sessions.json new file mode 100644 index 00000000000..c51be6a0aa4 --- /dev/null +++ b/tests/components/jellyfin/fixtures/sessions.json @@ -0,0 +1,1762 @@ +[ + { + "PlayState": { + "PositionTicks": 0, + "CanSeek": true, + "IsPaused": true, + "IsMuted": true, + "VolumeLevel": 0, + "AudioStreamIndex": 0, + "SubtitleStreamIndex": 0, + "MediaSourceId": "string", + "PlayMethod": "Transcode", + "RepeatMode": "RepeatNone", + "LiveStreamId": "string" + }, + "AdditionalUsers": [ + { + "UserId": "08ba1929-681e-4b24-929b-9245852f65c0", + "UserName": "string" + } + ], + "Capabilities": { + "PlayableMediaTypes": ["string"], + "SupportedCommands": ["MoveUp"], + "SupportsMediaControl": true, + "SupportsContentUploading": true, + "MessageCallbackUrl": "string", + "SupportsPersistentIdentifier": true, + "SupportsSync": true, + "DeviceProfile": { + "Name": "string", + "Id": "string", + "Identification": { + "FriendlyName": "string", + "ModelNumber": "string", + "SerialNumber": "string", + "ModelName": "string", + "ModelDescription": "string", + "ModelUrl": "string", + "Manufacturer": "string", + "ManufacturerUrl": "string", + "Headers": [ + { + "Name": "string", + "Value": "string", + "Match": "Equals" + } + ] + }, + "FriendlyName": "string", + "Manufacturer": "string", + "ManufacturerUrl": "string", + "ModelName": "string", + "ModelDescription": "string", + "ModelNumber": "string", + "ModelUrl": "string", + "SerialNumber": "string", + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "string", + "UserId": "string", + "AlbumArtPn": "string", + "MaxAlbumArtWidth": 0, + "MaxAlbumArtHeight": 0, + "MaxIconWidth": 0, + "MaxIconHeight": 0, + "MaxStreamingBitrate": 0, + "MaxStaticBitrate": 0, + "MusicStreamingTranscodingBitrate": 0, + "MaxStaticMusicBitrate": 0, + "SonyAggregationFlags": "string", + "ProtocolInfo": "string", + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "XmlRootAttributes": [ + { + "Name": "string", + "Value": "string" + } + ], + "DirectPlayProfiles": [ + { + "Container": "string", + "AudioCodec": "string", + "VideoCodec": "string", + "Type": "Audio" + } + ], + "TranscodingProfiles": [ + { + "Container": "string", + "Type": "Audio", + "VideoCodec": "string", + "AudioCodec": "string", + "Protocol": "string", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "string", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "Conditions": [ + { + "Condition": "Equals", + "Property": "AudioChannels", + "Value": "string", + "IsRequired": true + } + ] + } + ], + "ContainerProfiles": [ + { + "Type": "Audio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "AudioChannels", + "Value": "string", + "IsRequired": true + } + ], + "Container": "string" + } + ], + "CodecProfiles": [ + { + "Type": "Video", + "Conditions": [ + { + "Condition": "Equals", + "Property": "AudioChannels", + "Value": "string", + "IsRequired": true + } + ], + "ApplyConditions": [ + { + "Condition": "Equals", + "Property": "AudioChannels", + "Value": "string", + "IsRequired": true + } + ], + "Codec": "string", + "Container": "string" + } + ], + "ResponseProfiles": [ + { + "Container": "string", + "AudioCodec": "string", + "VideoCodec": "string", + "Type": "Audio", + "OrgPn": "string", + "MimeType": "string", + "Conditions": [ + { + "Condition": "Equals", + "Property": "AudioChannels", + "Value": "string", + "IsRequired": true + } + ] + } + ], + "SubtitleProfiles": [ + { + "Format": "string", + "Method": "Encode", + "DidlMode": "string", + "Language": "string", + "Container": "string" + } + ] + }, + "AppStoreUrl": "string", + "IconUrl": "string" + }, + "RemoteEndPoint": "string", + "PlayableMediaTypes": ["string"], + "Id": "string", + "UserId": "08ba1929-681e-4b24-929b-9245852f65c0", + "UserName": "string", + "Client": "string", + "LastActivityDate": "2019-08-24T14:15:22Z", + "LastPlaybackCheckIn": "2019-08-24T14:15:22Z", + "DeviceName": "string", + "DeviceType": "string", + "NowPlayingItem": { + "Name": "string", + "OriginalTitle": "string", + "ServerId": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "Etag": "string", + "SourceType": "string", + "PlaylistItemId": "string", + "DateCreated": "2019-08-24T14:15:22Z", + "DateLastMediaAdded": "2019-08-24T14:15:22Z", + "ExtraType": "string", + "AirsBeforeSeasonNumber": 0, + "AirsAfterSeasonNumber": 0, + "AirsBeforeEpisodeNumber": 0, + "CanDelete": true, + "CanDownload": true, + "HasSubtitles": true, + "PreferredMetadataLanguage": "string", + "PreferredMetadataCountryCode": "string", + "SupportsSync": true, + "Container": "string", + "SortName": "string", + "ForcedSortName": "string", + "Video3DFormat": "HalfSideBySide", + "PremiereDate": "2019-08-24T14:15:22Z", + "ExternalUrls": [ + { + "Name": "string", + "Url": "string" + } + ], + "MediaSources": [ + { + "Protocol": "File", + "Id": "string", + "Path": "string", + "EncoderPath": "string", + "EncoderProtocol": "File", + "Type": "Default", + "Container": "string", + "Size": 0, + "Name": "string", + "IsRemote": true, + "ETag": "string", + "RunTimeTicks": 0, + "ReadAtNativeFramerate": true, + "IgnoreDts": true, + "IgnoreIndex": true, + "GenPtsInput": true, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "IsInfiniteStream": true, + "RequiresOpening": true, + "OpenToken": "string", + "RequiresClosing": true, + "LiveStreamId": "string", + "BufferMs": 0, + "RequiresLooping": true, + "SupportsProbing": true, + "VideoType": "VideoFile", + "IsoType": "Dvd", + "Video3DFormat": "HalfSideBySide", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "MediaAttachments": [ + { + "Codec": "string", + "CodecTag": "string", + "Comment": "string", + "Index": 0, + "FileName": "string", + "MimeType": "string", + "DeliveryUrl": "string" + } + ], + "Formats": ["string"], + "Bitrate": 0, + "Timestamp": "None", + "RequiredHttpHeaders": { + "property1": "string", + "property2": "string" + }, + "TranscodingUrl": "string", + "TranscodingSubProtocol": "string", + "TranscodingContainer": "string", + "AnalyzeDurationMs": 0, + "DefaultAudioStreamIndex": 0, + "DefaultSubtitleStreamIndex": 0 + } + ], + "CriticRating": 0, + "ProductionLocations": ["string"], + "Path": "string", + "EnableMediaSourceDisplay": true, + "OfficialRating": "string", + "CustomRating": "string", + "ChannelId": "04b0b2a5-93cb-474d-8ea9-3df0f84eb0ff", + "ChannelName": "string", + "Overview": "string", + "Taglines": ["string"], + "Genres": ["string"], + "CommunityRating": 0, + "CumulativeRunTimeTicks": 0, + "RunTimeTicks": 0, + "PlayAccess": "Full", + "AspectRatio": "string", + "ProductionYear": 0, + "IsPlaceHolder": true, + "Number": "string", + "ChannelNumber": "string", + "IndexNumber": 0, + "IndexNumberEnd": 0, + "ParentIndexNumber": 0, + "RemoteTrailers": [ + { + "Url": "string", + "Name": "string" + } + ], + "ProviderIds": { + "property1": "string", + "property2": "string" + }, + "IsHD": true, + "IsFolder": true, + "ParentId": "c54e2d15-b5eb-48b7-9b04-53f376904b1e", + "Type": "AggregateFolder", + "People": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "Role": "string", + "Type": "string", + "PrimaryImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + } + } + ], + "Studios": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "GenreItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "ParentLogoItemId": "c78d400f-de5c-421e-8714-4fb05d387233", + "ParentBackdropItemId": "c22fd826-17fc-44f4-9b04-1eb3e8fb9173", + "ParentBackdropImageTags": ["string"], + "LocalTrailerCount": 0, + "UserData": { + "Rating": 0, + "PlayedPercentage": 0, + "UnplayedItemCount": 0, + "PlaybackPositionTicks": 0, + "PlayCount": 0, + "IsFavorite": true, + "Likes": true, + "LastPlayedDate": "2019-08-24T14:15:22Z", + "Played": true, + "Key": "string", + "ItemId": "string" + }, + "RecursiveItemCount": 0, + "ChildCount": 0, + "SeriesName": "string", + "SeriesId": "c7b70af4-4902-4a7e-95ab-28349b6c7afc", + "SeasonId": "badb6463-e5b7-45c5-8141-71204420ec8f", + "SpecialFeatureCount": 0, + "DisplayPreferencesId": "string", + "Status": "string", + "AirTime": "string", + "AirDays": ["Sunday"], + "Tags": ["string"], + "PrimaryImageAspectRatio": 0, + "Artists": ["string"], + "ArtistItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "Album": "string", + "CollectionType": "string", + "DisplayOrder": "string", + "AlbumId": "21af9851-8e39-43a9-9c47-513d3b9e99fc", + "AlbumPrimaryImageTag": "string", + "SeriesPrimaryImageTag": "string", + "AlbumArtist": "string", + "AlbumArtists": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "SeasonName": "string", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "VideoType": "VideoFile", + "PartCount": 0, + "MediaSourceCount": 0, + "ImageTags": { + "property1": "string", + "property2": "string" + }, + "BackdropImageTags": ["string"], + "ScreenshotImageTags": ["string"], + "ParentLogoImageTag": "string", + "ParentArtItemId": "10c1875b-b82c-48e8-bae9-939a5e68dc2f", + "ParentArtImageTag": "string", + "SeriesThumbImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + }, + "SeriesStudio": "string", + "ParentThumbItemId": "ae6ff707-333d-4994-be6d-b83ca1b35f46", + "ParentThumbImageTag": "string", + "ParentPrimaryImageItemId": "string", + "ParentPrimaryImageTag": "string", + "Chapters": [ + { + "StartPositionTicks": 0, + "Name": "string", + "ImagePath": "string", + "ImageDateModified": "2019-08-24T14:15:22Z", + "ImageTag": "string" + } + ], + "LocationType": "FileSystem", + "IsoType": "Dvd", + "MediaType": "string", + "EndDate": "2019-08-24T14:15:22Z", + "LockedFields": ["Cast"], + "TrailerCount": 0, + "MovieCount": 0, + "SeriesCount": 0, + "ProgramCount": 0, + "EpisodeCount": 0, + "SongCount": 0, + "AlbumCount": 0, + "ArtistCount": 0, + "MusicVideoCount": 0, + "LockData": true, + "Width": 0, + "Height": 0, + "CameraMake": "string", + "CameraModel": "string", + "Software": "string", + "ExposureTime": 0, + "FocalLength": 0, + "ImageOrientation": "TopLeft", + "Aperture": 0, + "ShutterSpeed": 0, + "Latitude": 0, + "Longitude": 0, + "Altitude": 0, + "IsoSpeedRating": 0, + "SeriesTimerId": "string", + "ProgramId": "string", + "ChannelPrimaryImageTag": "string", + "StartDate": "2019-08-24T14:15:22Z", + "CompletionPercentage": 0, + "IsRepeat": true, + "EpisodeTitle": "string", + "ChannelType": "TV", + "Audio": "Mono", + "IsMovie": true, + "IsSports": true, + "IsSeries": true, + "IsLive": true, + "IsNews": true, + "IsKids": true, + "IsPremiere": true, + "TimerId": "string", + "CurrentProgram": {} + }, + "FullNowPlayingItem": { + "Size": 0, + "Container": "string", + "IsHD": true, + "IsShortcut": true, + "ShortcutPath": "string", + "Width": 0, + "Height": 0, + "ExtraIds": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "DateLastSaved": "2019-08-24T14:15:22Z", + "RemoteTrailers": [ + { + "Url": "string", + "Name": "string" + } + ], + "SupportsExternalTransfer": true + }, + "NowViewingItem": { + "Name": "string", + "OriginalTitle": "string", + "ServerId": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "Etag": "string", + "SourceType": "string", + "PlaylistItemId": "string", + "DateCreated": "2019-08-24T14:15:22Z", + "DateLastMediaAdded": "2019-08-24T14:15:22Z", + "ExtraType": "string", + "AirsBeforeSeasonNumber": 0, + "AirsAfterSeasonNumber": 0, + "AirsBeforeEpisodeNumber": 0, + "CanDelete": true, + "CanDownload": true, + "HasSubtitles": true, + "PreferredMetadataLanguage": "string", + "PreferredMetadataCountryCode": "string", + "SupportsSync": true, + "Container": "string", + "SortName": "string", + "ForcedSortName": "string", + "Video3DFormat": "HalfSideBySide", + "PremiereDate": "2019-08-24T14:15:22Z", + "ExternalUrls": [ + { + "Name": "string", + "Url": "string" + } + ], + "MediaSources": [ + { + "Protocol": "File", + "Id": "string", + "Path": "string", + "EncoderPath": "string", + "EncoderProtocol": "File", + "Type": "Default", + "Container": "string", + "Size": 0, + "Name": "string", + "IsRemote": true, + "ETag": "string", + "RunTimeTicks": 0, + "ReadAtNativeFramerate": true, + "IgnoreDts": true, + "IgnoreIndex": true, + "GenPtsInput": true, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "IsInfiniteStream": true, + "RequiresOpening": true, + "OpenToken": "string", + "RequiresClosing": true, + "LiveStreamId": "string", + "BufferMs": 0, + "RequiresLooping": true, + "SupportsProbing": true, + "VideoType": "VideoFile", + "IsoType": "Dvd", + "Video3DFormat": "HalfSideBySide", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "MediaAttachments": [ + { + "Codec": "string", + "CodecTag": "string", + "Comment": "string", + "Index": 0, + "FileName": "string", + "MimeType": "string", + "DeliveryUrl": "string" + } + ], + "Formats": ["string"], + "Bitrate": 0, + "Timestamp": "None", + "RequiredHttpHeaders": { + "property1": "string", + "property2": "string" + }, + "TranscodingUrl": "string", + "TranscodingSubProtocol": "string", + "TranscodingContainer": "string", + "AnalyzeDurationMs": 0, + "DefaultAudioStreamIndex": 0, + "DefaultSubtitleStreamIndex": 0 + } + ], + "CriticRating": 0, + "ProductionLocations": ["string"], + "Path": "string", + "EnableMediaSourceDisplay": true, + "OfficialRating": "string", + "CustomRating": "string", + "ChannelId": "04b0b2a5-93cb-474d-8ea9-3df0f84eb0ff", + "ChannelName": "string", + "Overview": "string", + "Taglines": ["string"], + "Genres": ["string"], + "CommunityRating": 0, + "CumulativeRunTimeTicks": 0, + "RunTimeTicks": 0, + "PlayAccess": "Full", + "AspectRatio": "string", + "ProductionYear": 0, + "IsPlaceHolder": true, + "Number": "string", + "ChannelNumber": "string", + "IndexNumber": 0, + "IndexNumberEnd": 0, + "ParentIndexNumber": 0, + "RemoteTrailers": [ + { + "Url": "string", + "Name": "string" + } + ], + "ProviderIds": { + "property1": "string", + "property2": "string" + }, + "IsHD": true, + "IsFolder": true, + "ParentId": "c54e2d15-b5eb-48b7-9b04-53f376904b1e", + "Type": "AggregateFolder", + "People": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "Role": "string", + "Type": "string", + "PrimaryImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + } + } + ], + "Studios": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "GenreItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "ParentLogoItemId": "c78d400f-de5c-421e-8714-4fb05d387233", + "ParentBackdropItemId": "c22fd826-17fc-44f4-9b04-1eb3e8fb9173", + "ParentBackdropImageTags": ["string"], + "LocalTrailerCount": 0, + "UserData": { + "Rating": 0, + "PlayedPercentage": 0, + "UnplayedItemCount": 0, + "PlaybackPositionTicks": 0, + "PlayCount": 0, + "IsFavorite": true, + "Likes": true, + "LastPlayedDate": "2019-08-24T14:15:22Z", + "Played": true, + "Key": "string", + "ItemId": "string" + }, + "RecursiveItemCount": 0, + "ChildCount": 0, + "SeriesName": "string", + "SeriesId": "c7b70af4-4902-4a7e-95ab-28349b6c7afc", + "SeasonId": "badb6463-e5b7-45c5-8141-71204420ec8f", + "SpecialFeatureCount": 0, + "DisplayPreferencesId": "string", + "Status": "string", + "AirTime": "string", + "AirDays": ["Sunday"], + "Tags": ["string"], + "PrimaryImageAspectRatio": 0, + "Artists": ["string"], + "ArtistItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "Album": "string", + "CollectionType": "string", + "DisplayOrder": "string", + "AlbumId": "21af9851-8e39-43a9-9c47-513d3b9e99fc", + "AlbumPrimaryImageTag": "string", + "SeriesPrimaryImageTag": "string", + "AlbumArtist": "string", + "AlbumArtists": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "SeasonName": "string", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "VideoType": "VideoFile", + "PartCount": 0, + "MediaSourceCount": 0, + "ImageTags": { + "property1": "string", + "property2": "string" + }, + "BackdropImageTags": ["string"], + "ScreenshotImageTags": ["string"], + "ParentLogoImageTag": "string", + "ParentArtItemId": "10c1875b-b82c-48e8-bae9-939a5e68dc2f", + "ParentArtImageTag": "string", + "SeriesThumbImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + }, + "SeriesStudio": "string", + "ParentThumbItemId": "ae6ff707-333d-4994-be6d-b83ca1b35f46", + "ParentThumbImageTag": "string", + "ParentPrimaryImageItemId": "string", + "ParentPrimaryImageTag": "string", + "Chapters": [ + { + "StartPositionTicks": 0, + "Name": "string", + "ImagePath": "string", + "ImageDateModified": "2019-08-24T14:15:22Z", + "ImageTag": "string" + } + ], + "LocationType": "FileSystem", + "IsoType": "Dvd", + "MediaType": "string", + "EndDate": "2019-08-24T14:15:22Z", + "LockedFields": ["Cast"], + "TrailerCount": 0, + "MovieCount": 0, + "SeriesCount": 0, + "ProgramCount": 0, + "EpisodeCount": 0, + "SongCount": 0, + "AlbumCount": 0, + "ArtistCount": 0, + "MusicVideoCount": 0, + "LockData": true, + "Width": 0, + "Height": 0, + "CameraMake": "string", + "CameraModel": "string", + "Software": "string", + "ExposureTime": 0, + "FocalLength": 0, + "ImageOrientation": "TopLeft", + "Aperture": 0, + "ShutterSpeed": 0, + "Latitude": 0, + "Longitude": 0, + "Altitude": 0, + "IsoSpeedRating": 0, + "SeriesTimerId": "string", + "ProgramId": "string", + "ChannelPrimaryImageTag": "string", + "StartDate": "2019-08-24T14:15:22Z", + "CompletionPercentage": 0, + "IsRepeat": true, + "EpisodeTitle": "string", + "ChannelType": "TV", + "Audio": "Mono", + "IsMovie": true, + "IsSports": true, + "IsSeries": true, + "IsLive": true, + "IsNews": true, + "IsKids": true, + "IsPremiere": true, + "TimerId": "string", + "CurrentProgram": {} + }, + "DeviceId": "string", + "ApplicationVersion": "string", + "TranscodingInfo": { + "AudioCodec": "string", + "VideoCodec": "string", + "Container": "string", + "IsVideoDirect": true, + "IsAudioDirect": true, + "Bitrate": 0, + "Framerate": 0, + "CompletionPercentage": 0, + "Width": 0, + "Height": 0, + "AudioChannels": 0, + "HardwareAccelerationType": "AMF", + "TranscodeReasons": "ContainerNotSupported" + }, + "IsActive": true, + "SupportsMediaControl": true, + "SupportsRemoteControl": true, + "NowPlayingQueue": [ + { + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "PlaylistItemId": "string" + } + ], + "NowPlayingQueueFullItems": [ + { + "Name": "string", + "OriginalTitle": "string", + "ServerId": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "Etag": "string", + "SourceType": "string", + "PlaylistItemId": "string", + "DateCreated": "2019-08-24T14:15:22Z", + "DateLastMediaAdded": "2019-08-24T14:15:22Z", + "ExtraType": "string", + "AirsBeforeSeasonNumber": 0, + "AirsAfterSeasonNumber": 0, + "AirsBeforeEpisodeNumber": 0, + "CanDelete": true, + "CanDownload": true, + "HasSubtitles": true, + "PreferredMetadataLanguage": "string", + "PreferredMetadataCountryCode": "string", + "SupportsSync": true, + "Container": "string", + "SortName": "string", + "ForcedSortName": "string", + "Video3DFormat": "HalfSideBySide", + "PremiereDate": "2019-08-24T14:15:22Z", + "ExternalUrls": [ + { + "Name": "string", + "Url": "string" + } + ], + "MediaSources": [ + { + "Protocol": "File", + "Id": "string", + "Path": "string", + "EncoderPath": "string", + "EncoderProtocol": "File", + "Type": "Default", + "Container": "string", + "Size": 0, + "Name": "string", + "IsRemote": true, + "ETag": "string", + "RunTimeTicks": 0, + "ReadAtNativeFramerate": true, + "IgnoreDts": true, + "IgnoreIndex": true, + "GenPtsInput": true, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "IsInfiniteStream": true, + "RequiresOpening": true, + "OpenToken": "string", + "RequiresClosing": true, + "LiveStreamId": "string", + "BufferMs": 0, + "RequiresLooping": true, + "SupportsProbing": true, + "VideoType": "VideoFile", + "IsoType": "Dvd", + "Video3DFormat": "HalfSideBySide", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "MediaAttachments": [ + { + "Codec": "string", + "CodecTag": "string", + "Comment": "string", + "Index": 0, + "FileName": "string", + "MimeType": "string", + "DeliveryUrl": "string" + } + ], + "Formats": ["string"], + "Bitrate": 0, + "Timestamp": "None", + "RequiredHttpHeaders": { + "property1": "string", + "property2": "string" + }, + "TranscodingUrl": "string", + "TranscodingSubProtocol": "string", + "TranscodingContainer": "string", + "AnalyzeDurationMs": 0, + "DefaultAudioStreamIndex": 0, + "DefaultSubtitleStreamIndex": 0 + } + ], + "CriticRating": 0, + "ProductionLocations": ["string"], + "Path": "string", + "EnableMediaSourceDisplay": true, + "OfficialRating": "string", + "CustomRating": "string", + "ChannelId": "04b0b2a5-93cb-474d-8ea9-3df0f84eb0ff", + "ChannelName": "string", + "Overview": "string", + "Taglines": ["string"], + "Genres": ["string"], + "CommunityRating": 0, + "CumulativeRunTimeTicks": 0, + "RunTimeTicks": 0, + "PlayAccess": "Full", + "AspectRatio": "string", + "ProductionYear": 0, + "IsPlaceHolder": true, + "Number": "string", + "ChannelNumber": "string", + "IndexNumber": 0, + "IndexNumberEnd": 0, + "ParentIndexNumber": 0, + "RemoteTrailers": [ + { + "Url": "string", + "Name": "string" + } + ], + "ProviderIds": { + "property1": "string", + "property2": "string" + }, + "IsHD": true, + "IsFolder": true, + "ParentId": "c54e2d15-b5eb-48b7-9b04-53f376904b1e", + "Type": "AggregateFolder", + "People": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "Role": "string", + "Type": "string", + "PrimaryImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + } + } + ], + "Studios": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "GenreItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "ParentLogoItemId": "c78d400f-de5c-421e-8714-4fb05d387233", + "ParentBackdropItemId": "c22fd826-17fc-44f4-9b04-1eb3e8fb9173", + "ParentBackdropImageTags": ["string"], + "LocalTrailerCount": 0, + "UserData": { + "Rating": 0, + "PlayedPercentage": 0, + "UnplayedItemCount": 0, + "PlaybackPositionTicks": 0, + "PlayCount": 0, + "IsFavorite": true, + "Likes": true, + "LastPlayedDate": "2019-08-24T14:15:22Z", + "Played": true, + "Key": "string", + "ItemId": "string" + }, + "RecursiveItemCount": 0, + "ChildCount": 0, + "SeriesName": "string", + "SeriesId": "c7b70af4-4902-4a7e-95ab-28349b6c7afc", + "SeasonId": "badb6463-e5b7-45c5-8141-71204420ec8f", + "SpecialFeatureCount": 0, + "DisplayPreferencesId": "string", + "Status": "string", + "AirTime": "string", + "AirDays": ["Sunday"], + "Tags": ["string"], + "PrimaryImageAspectRatio": 0, + "Artists": ["string"], + "ArtistItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "Album": "string", + "CollectionType": "string", + "DisplayOrder": "string", + "AlbumId": "21af9851-8e39-43a9-9c47-513d3b9e99fc", + "AlbumPrimaryImageTag": "string", + "SeriesPrimaryImageTag": "string", + "AlbumArtist": "string", + "AlbumArtists": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "SeasonName": "string", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "VideoType": "VideoFile", + "PartCount": 0, + "MediaSourceCount": 0, + "ImageTags": { + "property1": "string", + "property2": "string" + }, + "BackdropImageTags": ["string"], + "ScreenshotImageTags": ["string"], + "ParentLogoImageTag": "string", + "ParentArtItemId": "10c1875b-b82c-48e8-bae9-939a5e68dc2f", + "ParentArtImageTag": "string", + "SeriesThumbImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + }, + "SeriesStudio": "string", + "ParentThumbItemId": "ae6ff707-333d-4994-be6d-b83ca1b35f46", + "ParentThumbImageTag": "string", + "ParentPrimaryImageItemId": "string", + "ParentPrimaryImageTag": "string", + "Chapters": [ + { + "StartPositionTicks": 0, + "Name": "string", + "ImagePath": "string", + "ImageDateModified": "2019-08-24T14:15:22Z", + "ImageTag": "string" + } + ], + "LocationType": "FileSystem", + "IsoType": "Dvd", + "MediaType": "string", + "EndDate": "2019-08-24T14:15:22Z", + "LockedFields": ["Cast"], + "TrailerCount": 0, + "MovieCount": 0, + "SeriesCount": 0, + "ProgramCount": 0, + "EpisodeCount": 0, + "SongCount": 0, + "AlbumCount": 0, + "ArtistCount": 0, + "MusicVideoCount": 0, + "LockData": true, + "Width": 0, + "Height": 0, + "CameraMake": "string", + "CameraModel": "string", + "Software": "string", + "ExposureTime": 0, + "FocalLength": 0, + "ImageOrientation": "TopLeft", + "Aperture": 0, + "ShutterSpeed": 0, + "Latitude": 0, + "Longitude": 0, + "Altitude": 0, + "IsoSpeedRating": 0, + "SeriesTimerId": "string", + "ProgramId": "string", + "ChannelPrimaryImageTag": "string", + "StartDate": "2019-08-24T14:15:22Z", + "CompletionPercentage": 0, + "IsRepeat": true, + "EpisodeTitle": "string", + "ChannelType": "TV", + "Audio": "Mono", + "IsMovie": true, + "IsSports": true, + "IsSeries": true, + "IsLive": true, + "IsNews": true, + "IsKids": true, + "IsPremiere": true, + "TimerId": "string", + "CurrentProgram": {} + } + ], + "HasCustomDeviceName": true, + "PlaylistItemId": "string", + "ServerId": "string", + "UserPrimaryImageTag": "string", + "SupportedCommands": ["MoveUp"] + } +] diff --git a/tests/components/jellyfin/test_sensor.py b/tests/components/jellyfin/test_sensor.py new file mode 100644 index 00000000000..6b52fe442d9 --- /dev/null +++ b/tests/components/jellyfin/test_sensor.py @@ -0,0 +1,51 @@ +"""Tests for the Jellyfin sensor platform.""" +from unittest.mock import MagicMock + +from homeassistant.components.jellyfin.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_watching( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_jellyfin: MagicMock, +) -> None: + """Test the Jellyfin watching sensor.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + state = hass.states.get("sensor.jellyfin_server") + assert state + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "JELLYFIN-SERVER" + assert state.attributes.get(ATTR_ICON) == "mdi:television-play" + assert state.attributes.get(ATTR_STATE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Watching" + assert state.state == "1" + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry.device_id + assert entry.entity_category is None + assert entry.unique_id == "SERVER-UUID-watching" + + device = device_registry.async_get(entry.device_id) + assert device + assert device.configuration_url is None + assert device.connections == set() + assert device.entry_type is dr.DeviceEntryType.SERVICE + assert device.hw_version is None + assert device.identifiers == {(DOMAIN, "SERVER-UUID")} + assert device.manufacturer == "Jellyfin" + assert device.name == "JELLYFIN-SERVER" + assert device.sw_version is None From a9f8bb32775968b69a1aa19944277a705ce7a200 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Oct 2022 19:29:58 -1000 Subject: [PATCH 382/985] Bump ibeacon-ble to 0.7.4 (#80147) --- homeassistant/components/ibeacon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ibeacon/manifest.json b/homeassistant/components/ibeacon/manifest.json index a2b55a69403..3df2b9e000d 100644 --- a/homeassistant/components/ibeacon/manifest.json +++ b/homeassistant/components/ibeacon/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ibeacon", "dependencies": ["bluetooth"], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [2, 21] }], - "requirements": ["ibeacon_ble==0.7.3"], + "requirements": ["ibeacon_ble==0.7.4"], "codeowners": ["@bdraco"], "iot_class": "local_push", "loggers": ["bleak"], diff --git a/requirements_all.txt b/requirements_all.txt index e07b76c077f..bf1aacdd845 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -904,7 +904,7 @@ iammeter==0.1.7 iaqualink==0.4.1 # homeassistant.components.ibeacon -ibeacon_ble==0.7.3 +ibeacon_ble==0.7.4 # homeassistant.components.watson_tts ibm-watson==5.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e31e0e2e99d..2dc2e6f06fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -675,7 +675,7 @@ hyperion-py==0.7.5 iaqualink==0.4.1 # homeassistant.components.ibeacon -ibeacon_ble==0.7.3 +ibeacon_ble==0.7.4 # homeassistant.components.ping icmplib==3.0 From d71a9d6ab32d6a535890bbb44936b687143378d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Oct 2022 19:30:09 -1000 Subject: [PATCH 383/985] Bump yalexs to 1.2.6 (#80142) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index a816ddc06ff..b7dde070049 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.2.4"], + "requirements": ["yalexs==1.2.6"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index bf1aacdd845..bff3579e651 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2577,7 +2577,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==1.9.2 # homeassistant.components.august -yalexs==1.2.4 +yalexs==1.2.6 # homeassistant.components.yeelight yeelight==0.7.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2dc2e6f06fe..3f0a33b38fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1787,7 +1787,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==1.9.2 # homeassistant.components.august -yalexs==1.2.4 +yalexs==1.2.6 # homeassistant.components.yeelight yeelight==0.7.10 From 75886d7213b92d1bf0b9b302a0ef5ec36145b3e6 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Wed, 12 Oct 2022 10:04:48 +0200 Subject: [PATCH 384/985] Strip whitespace from Nut "zero" serialno (#80141) --- homeassistant/components/nut/__init__.py | 2 +- tests/components/nut/fixtures/CP1500PFCLCD.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 27332e50b18..b4110736e55 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -148,7 +148,7 @@ def _serial_from_status(status: dict[str, str]) -> str | None: """Find the best serialvalue from the status.""" serial = status.get("device.serial") or status.get("ups.serial") if serial and ( - serial.lower() in NUT_FAKE_SERIAL or serial.count("0") == len(serial) + serial.lower() in NUT_FAKE_SERIAL or serial.count("0") == len(serial.strip()) ): return None return serial diff --git a/tests/components/nut/fixtures/CP1500PFCLCD.json b/tests/components/nut/fixtures/CP1500PFCLCD.json index 3a42a01b054..f3121b147ac 100644 --- a/tests/components/nut/fixtures/CP1500PFCLCD.json +++ b/tests/components/nut/fixtures/CP1500PFCLCD.json @@ -5,7 +5,7 @@ "driver.parameter.pollfreq": "30", "ups.beeper.status": "disabled", "input.voltage.nominal": "120", - "device.serial": "000000000000", + "device.serial": "000000000000 ", "ups.timer.shutdown": "-60", "input.voltage": "122.0", "ups.status": "OL", From d03e0380bbe8fd56049b93b2df9cba55f65bcf64 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Oct 2022 10:59:15 +0200 Subject: [PATCH 385/985] Add brightness controls to LaMetric (#79804) --- homeassistant/components/lametric/number.py | 12 +++++ tests/components/lametric/test_number.py | 58 ++++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lametric/number.py b/homeassistant/components/lametric/number.py index c788eb3255e..e66b130b3e2 100644 --- a/homeassistant/components/lametric/number.py +++ b/homeassistant/components/lametric/number.py @@ -34,6 +34,18 @@ class LaMetricNumberEntityDescription( NUMBERS = [ + LaMetricNumberEntityDescription( + key="brightness", + name="Brightness", + icon="mdi:brightness-6", + entity_category=EntityCategory.CONFIG, + native_step=1, + native_min_value=0, + native_max_value=100, + native_unit_of_measurement="%", + value_fn=lambda device: device.display.brightness, + set_value_fn=lambda device, bri: device.display(brightness=int(bri)), + ), LaMetricNumberEntityDescription( key="volume", name="Volume", diff --git a/tests/components/lametric/test_number.py b/tests/components/lametric/test_number.py index 92d4e262bdc..1acf939fc99 100644 --- a/tests/components/lametric/test_number.py +++ b/tests/components/lametric/test_number.py @@ -2,18 +2,20 @@ from unittest.mock import MagicMock from homeassistant.components.lametric.const import DOMAIN -from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE -from homeassistant.components.number.const import ( +from homeassistant.components.number import ( ATTR_MAX, ATTR_MIN, ATTR_STEP, ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -22,6 +24,58 @@ from homeassistant.helpers.entity import EntityCategory from tests.common import MockConfigEntry +async def test_brightness( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_lametric: MagicMock, +) -> None: + """Test the LaMetric display brightness controls.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + state = hass.states.get("number.frenck_s_lametric_brightness") + assert state + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's LaMetric Brightness" + assert state.attributes.get(ATTR_ICON) == "mdi:brightness-6" + assert state.attributes.get(ATTR_MAX) == 100 + assert state.attributes.get(ATTR_MIN) == 0 + assert state.attributes.get(ATTR_STEP) == 1 + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "%" + assert state.state == "100" + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry.device_id + assert entry.entity_category is EntityCategory.CONFIG + assert entry.unique_id == "SA110405124500W00BS9-brightness" + + device = device_registry.async_get(entry.device_id) + assert device + assert device.configuration_url is None + assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")} + assert device.entry_type is None + assert device.hw_version is None + assert device.identifiers == {(DOMAIN, "SA110405124500W00BS9")} + assert device.manufacturer == "LaMetric Inc." + assert device.name == "Frenck's LaMetric" + assert device.sw_version == "2.2.2" + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.frenck_s_lametric_brightness", + ATTR_VALUE: 21, + }, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(mock_lametric.display.mock_calls) == 1 + mock_lametric.display.assert_called_once_with(brightness=21) + + async def test_volume( hass: HomeAssistant, init_integration: MockConfigEntry, From 107e1ed16ca422f13c5512114c419b53a9478454 Mon Sep 17 00:00:00 2001 From: CharlB <41644590+CharlieBailly@users.noreply.github.com> Date: Wed, 12 Oct 2022 11:27:46 +0200 Subject: [PATCH 386/985] Fix, improve input validation and add tests to ClickSend tts (#76669) Co-authored-by: Martin Hjelmare --- .../components/clicksend_tts/notify.py | 22 ++-- tests/components/clicksend_tts/__init__.py | 1 + tests/components/clicksend_tts/test_notify.py | 122 ++++++++++++++++++ 3 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 tests/components/clicksend_tts/__init__.py create mode 100644 tests/components/clicksend_tts/test_notify.py diff --git a/homeassistant/components/clicksend_tts/notify.py b/homeassistant/components/clicksend_tts/notify.py index 8026c8e150b..5ff38c41fc9 100644 --- a/homeassistant/components/clicksend_tts/notify.py +++ b/homeassistant/components/clicksend_tts/notify.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import ( CONF_API_KEY, + CONF_NAME, CONF_RECIPIENT, CONF_USERNAME, CONTENT_TYPE_JSON, @@ -23,20 +24,27 @@ HEADERS = {"Content-Type": CONTENT_TYPE_JSON} CONF_LANGUAGE = "language" CONF_VOICE = "voice" -CONF_CALLER = "caller" +MALE_VOICE = "male" +FEMALE_VOICE = "female" + +DEFAULT_NAME = "clicksend_tts" DEFAULT_LANGUAGE = "en-us" -DEFAULT_VOICE = "female" +DEFAULT_VOICE = FEMALE_VOICE TIMEOUT = 5 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_RECIPIENT): cv.string, + vol.Required(CONF_RECIPIENT): vol.All( + cv.string, vol.Match(r"^\+?[1-9]\d{1,14}$") + ), vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): cv.string, - vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): cv.string, - vol.Optional(CONF_CALLER): cv.string, + vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In( + [MALE_VOICE, FEMALE_VOICE] + ), } ) @@ -60,9 +68,6 @@ class ClicksendNotificationService(BaseNotificationService): self.recipient = config[CONF_RECIPIENT] self.language = config[CONF_LANGUAGE] self.voice = config[CONF_VOICE] - self.caller = config.get(CONF_CALLER) - if self.caller is None: - self.caller = self.recipient def send_message(self, message="", **kwargs): """Send a voice call to a user.""" @@ -70,7 +75,6 @@ class ClicksendNotificationService(BaseNotificationService): "messages": [ { "source": "hass.notify", - "from": self.caller, "to": self.recipient, "body": message, "lang": self.language, diff --git a/tests/components/clicksend_tts/__init__.py b/tests/components/clicksend_tts/__init__.py new file mode 100644 index 00000000000..c822773ef70 --- /dev/null +++ b/tests/components/clicksend_tts/__init__.py @@ -0,0 +1 @@ +"""Tests for the ClickSend TTS component.""" diff --git a/tests/components/clicksend_tts/test_notify.py b/tests/components/clicksend_tts/test_notify.py new file mode 100644 index 00000000000..9bebb3cfbca --- /dev/null +++ b/tests/components/clicksend_tts/test_notify.py @@ -0,0 +1,122 @@ +"""The test for the Facebook notify module.""" +import base64 +from http import HTTPStatus +import logging +from unittest.mock import patch + +import pytest +import requests_mock + +from homeassistant.components import notify +import homeassistant.components.clicksend_tts.notify as cs_tts +from homeassistant.setup import async_setup_component + +from tests.common import assert_setup_component + +# Infos from https://developers.clicksend.com/docs/rest/v3/#testing +TEST_USERNAME = "nocredit" +TEST_API_KEY = "D83DED51-9E35-4D42-9BB9-0E34B7CA85AE" +TEST_VOICE_NUMBER = "+61411111111" + +TEST_VOICE = "male" +TEST_LANGUAGE = "fr-fr" +TEST_MESSAGE = "Just a test message!" + + +CONFIG = { + notify.DOMAIN: { + "platform": "clicksend_tts", + cs_tts.CONF_USERNAME: TEST_USERNAME, + cs_tts.CONF_API_KEY: TEST_API_KEY, + cs_tts.CONF_RECIPIENT: TEST_VOICE_NUMBER, + cs_tts.CONF_LANGUAGE: TEST_LANGUAGE, + cs_tts.CONF_VOICE: TEST_VOICE, + } +} + + +@pytest.fixture +def mock_clicksend_tts_notify(): + """Mock Clicksend TTS notify service.""" + with patch( + "homeassistant.components.clicksend_tts.notify.get_service", autospec=True + ) as ns: + yield ns + + +async def setup_notify(hass): + """Test setup.""" + with assert_setup_component(1, notify.DOMAIN) as config: + assert await async_setup_component(hass, notify.DOMAIN, CONFIG) + assert config[notify.DOMAIN] + await hass.async_block_till_done() + + +async def test_no_notify_service(hass, mock_clicksend_tts_notify, caplog): + """Test missing platform notify service instance.""" + caplog.set_level(logging.ERROR) + mock_clicksend_tts_notify.return_value = None + await setup_notify(hass) + await hass.async_block_till_done() + assert mock_clicksend_tts_notify.called + assert "Failed to initialize notification service clicksend_tts" in caplog.text + + +async def test_send_simple_message(hass): + """Test sending a simple message with success.""" + + with requests_mock.Mocker() as mock: + # Mocking authentication endpoint + mock.get( + f"{cs_tts.BASE_API_URL}/account", + status_code=HTTPStatus.OK, + ) + + # Mocking TTS endpoint + mock.post( + f"{cs_tts.BASE_API_URL}/voice/send", + status_code=HTTPStatus.OK, + ) + + # Setting up integration + await setup_notify(hass) + + # Sending message + data = { + notify.ATTR_MESSAGE: TEST_MESSAGE, + } + await hass.services.async_call( + notify.DOMAIN, cs_tts.DEFAULT_NAME, data, blocking=True + ) + + # Checking if everything went well + assert mock.called + assert mock.call_count == 2 + + expected_body = { + "messages": [ + { + "source": "hass.notify", + "to": TEST_VOICE_NUMBER, + "body": TEST_MESSAGE, + "lang": TEST_LANGUAGE, + "voice": TEST_VOICE, + } + ] + } + assert mock.last_request.json() == expected_body + + expected_content_type = "application/json" + assert ( + "Content-Type" in mock.last_request.headers.keys() + and mock.last_request.headers["Content-Type"] == expected_content_type + ) + + encoded_auth = base64.b64encode( + f"{TEST_USERNAME}:{TEST_API_KEY}".encode() + ).decode() + expected_auth = f"Basic {encoded_auth}" + assert ( + "Authorization" in mock.last_request.headers + and mock.last_request.headers["Authorization"] == expected_auth + ) From 77571c8a84e093fa6cae50b850e96a5fa8e9f02c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Oct 2022 11:33:09 +0200 Subject: [PATCH 387/985] Add error handling to LaMetric number platform (#80159) --- homeassistant/components/lametric/number.py | 2 + tests/components/lametric/test_number.py | 67 +++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/homeassistant/components/lametric/number.py b/homeassistant/components/lametric/number.py index e66b130b3e2..9275160f497 100644 --- a/homeassistant/components/lametric/number.py +++ b/homeassistant/components/lametric/number.py @@ -16,6 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .coordinator import LaMetricDataUpdateCoordinator from .entity import LaMetricEntity +from .helpers import lametric_exception_handler @dataclass @@ -96,6 +97,7 @@ class LaMetricNumberEntity(LaMetricEntity, NumberEntity): """Return the number value.""" return self.entity_description.value_fn(self.coordinator.data) + @lametric_exception_handler async def async_set_native_value(self, value: float) -> None: """Change to new number value.""" await self.entity_description.set_value_fn(self.coordinator.lametric, value) diff --git a/tests/components/lametric/test_number.py b/tests/components/lametric/test_number.py index 1acf939fc99..f80b2214577 100644 --- a/tests/components/lametric/test_number.py +++ b/tests/components/lametric/test_number.py @@ -1,6 +1,9 @@ """Tests for the LaMetric number platform.""" from unittest.mock import MagicMock +from demetriek import LaMetricConnectionError, LaMetricError +import pytest + from homeassistant.components.lametric.const import DOMAIN from homeassistant.components.number import ( ATTR_MAX, @@ -16,8 +19,10 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import EntityCategory @@ -125,3 +130,65 @@ async def test_volume( assert len(mock_lametric.audio.mock_calls) == 1 mock_lametric.audio.assert_called_once_with(volume=42) + + +async def test_number_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_lametric: MagicMock, +) -> None: + """Test error handling of the LaMetric numbers.""" + mock_lametric.audio.side_effect = LaMetricError + + state = hass.states.get("number.frenck_s_lametric_volume") + assert state + assert state.state == "100" + + with pytest.raises( + HomeAssistantError, match="Invalid response from the LaMetric device" + ): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.frenck_s_lametric_volume", + ATTR_VALUE: 42, + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("number.frenck_s_lametric_volume") + assert state + assert state.state == "100" + + +async def test_number_connection_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_lametric: MagicMock, +) -> None: + """Test connection error handling of the LaMetric numbers.""" + mock_lametric.audio.side_effect = LaMetricConnectionError + + state = hass.states.get("number.frenck_s_lametric_volume") + assert state + assert state.state == "100" + + with pytest.raises( + HomeAssistantError, match="Error communicating with the LaMetric device" + ): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.frenck_s_lametric_volume", + ATTR_VALUE: 42, + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("number.frenck_s_lametric_volume") + assert state + assert state.state == STATE_UNAVAILABLE From 93961690609032589340fa69529b53700da21348 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Oct 2022 11:34:08 +0200 Subject: [PATCH 388/985] Add error handling to LaMetric select platform (#80160) --- homeassistant/components/lametric/select.py | 2 + tests/components/lametric/test_select.py | 67 ++++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lametric/select.py b/homeassistant/components/lametric/select.py index 4fcdfbaf2cb..e15e33facfc 100644 --- a/homeassistant/components/lametric/select.py +++ b/homeassistant/components/lametric/select.py @@ -16,6 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .coordinator import LaMetricDataUpdateCoordinator from .entity import LaMetricEntity +from .helpers import lametric_exception_handler @dataclass @@ -83,6 +84,7 @@ class LaMetricSelectEntity(LaMetricEntity, SelectEntity): """Return the selected entity option to represent the entity state.""" return self.entity_description.current_fn(self.coordinator.data) + @lametric_exception_handler async def async_select_option(self, option: str) -> None: """Change the selected option.""" await self.entity_description.select_fn(self.coordinator.lametric, option) diff --git a/tests/components/lametric/test_select.py b/tests/components/lametric/test_select.py index 8d9394b9068..65c1df2ab3d 100644 --- a/tests/components/lametric/test_select.py +++ b/tests/components/lametric/test_select.py @@ -1,7 +1,8 @@ """Tests for the LaMetric select platform.""" from unittest.mock import MagicMock -from demetriek import BrightnessMode +from demetriek import BrightnessMode, LaMetricConnectionError, LaMetricError +import pytest from homeassistant.components.lametric.const import DOMAIN from homeassistant.components.select import ( @@ -14,8 +15,10 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_OPTION, + STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import EntityCategory @@ -69,3 +72,65 @@ async def test_brightness_mode( assert len(mock_lametric.display.mock_calls) == 1 mock_lametric.display.assert_called_once_with(brightness_mode=BrightnessMode.MANUAL) + + +async def test_select_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_lametric: MagicMock, +) -> None: + """Test error handling of the LaMetric selects.""" + mock_lametric.display.side_effect = LaMetricError + + state = hass.states.get("select.frenck_s_lametric_brightness_mode") + assert state + assert state.state == BrightnessMode.AUTO + + with pytest.raises( + HomeAssistantError, match="Invalid response from the LaMetric device" + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.frenck_s_lametric_brightness_mode", + ATTR_OPTION: "manual", + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("select.frenck_s_lametric_brightness_mode") + assert state + assert state.state == BrightnessMode.AUTO + + +async def test_select_connection_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_lametric: MagicMock, +) -> None: + """Test connection error handling of the LaMetric selects.""" + mock_lametric.display.side_effect = LaMetricConnectionError + + state = hass.states.get("select.frenck_s_lametric_brightness_mode") + assert state + assert state.state == BrightnessMode.AUTO + + with pytest.raises( + HomeAssistantError, match="Error communicating with the LaMetric device" + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.frenck_s_lametric_brightness_mode", + ATTR_OPTION: "manual", + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("select.frenck_s_lametric_brightness_mode") + assert state + assert state.state == STATE_UNAVAILABLE From 1191f4b61db3562cae274a6eb67762c348f45464 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Oct 2022 11:35:09 +0200 Subject: [PATCH 389/985] Add error handling to LaMetric switch platform (#80161) --- homeassistant/components/lametric/switch.py | 3 + tests/components/lametric/test_switch.py | 64 +++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/homeassistant/components/lametric/switch.py b/homeassistant/components/lametric/switch.py index 8c0acac65e6..c9f7ce047aa 100644 --- a/homeassistant/components/lametric/switch.py +++ b/homeassistant/components/lametric/switch.py @@ -16,6 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .coordinator import LaMetricDataUpdateCoordinator from .entity import LaMetricEntity +from .helpers import lametric_exception_handler @dataclass @@ -91,11 +92,13 @@ class LaMetricSwitchEntity(LaMetricEntity, SwitchEntity): """Return state of the switch.""" return self.entity_description.is_on_fn(self.coordinator.data) + @lametric_exception_handler async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" await self.entity_description.set_fn(self.coordinator.lametric, True) await self.coordinator.async_request_refresh() + @lametric_exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" await self.entity_description.set_fn(self.coordinator.lametric, False) diff --git a/tests/components/lametric/test_switch.py b/tests/components/lametric/test_switch.py index 350fa1b24f8..7ed47fe463e 100644 --- a/tests/components/lametric/test_switch.py +++ b/tests/components/lametric/test_switch.py @@ -1,6 +1,9 @@ """Tests for the LaMetric switch platform.""" from unittest.mock import MagicMock +from demetriek import LaMetricConnectionError, LaMetricError +import pytest + from homeassistant.components.lametric.const import DOMAIN, SCAN_INTERVAL from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, @@ -16,6 +19,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import EntityCategory import homeassistant.util.dt as dt_util @@ -87,3 +91,63 @@ async def test_bluetooth( state = hass.states.get("switch.frenck_s_lametric_bluetooth") assert state assert state.state == STATE_UNAVAILABLE + + +async def test_switch_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_lametric: MagicMock, +) -> None: + """Test error handling of the LaMetric switches.""" + mock_lametric.bluetooth.side_effect = LaMetricError + + state = hass.states.get("switch.frenck_s_lametric_bluetooth") + assert state + assert state.state == STATE_OFF + + with pytest.raises( + HomeAssistantError, match="Invalid response from the LaMetric device" + ): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "switch.frenck_s_lametric_bluetooth", + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.frenck_s_lametric_bluetooth") + assert state + assert state.state == STATE_OFF + + +async def test_switch_connection_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_lametric: MagicMock, +) -> None: + """Test connection error handling of the LaMetric switches.""" + mock_lametric.bluetooth.side_effect = LaMetricConnectionError + + state = hass.states.get("switch.frenck_s_lametric_bluetooth") + assert state + assert state.state == STATE_OFF + + with pytest.raises( + HomeAssistantError, match="Error communicating with the LaMetric device" + ): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "switch.frenck_s_lametric_bluetooth", + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.frenck_s_lametric_bluetooth") + assert state + assert state.state == STATE_UNAVAILABLE From 237b03150ecf1de86a9488ebb391838007420cb7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Oct 2022 23:37:26 -1000 Subject: [PATCH 390/985] Bump dbus-fast to 1.44.0 (#80149) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index d5f0539bda7..aaaff591e6b 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.4", - "dbus-fast==1.41.0" + "dbus-fast==1.44.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 85b8aee71ac..f6eece8de0b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.4 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.41.0 +dbus-fast==1.44.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index bff3579e651..e2977c1e23a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -546,7 +546,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.41.0 +dbus-fast==1.44.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f0a33b38fd..b1d0498d985 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -426,7 +426,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.41.0 +dbus-fast==1.44.0 # homeassistant.components.debugpy debugpy==1.6.3 From e9e3fb1cc8ef93d28a369227152e83fd22a63c94 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Oct 2022 11:38:43 +0200 Subject: [PATCH 391/985] Move attribution to standalone attribute [c-d] (#80150) --- .../components/comed_hourly_pricing/sensor.py | 6 ++---- homeassistant/components/coronavirus/sensor.py | 3 +-- .../components/currencylayer/sensor.py | 17 +++-------------- homeassistant/components/darksky/sensor.py | 5 +---- .../components/digital_ocean/binary_sensor.py | 4 ++-- .../components/digital_ocean/switch.py | 4 ++-- homeassistant/components/discogs/sensor.py | 13 +++---------- .../components/dublin_bus_transport/sensor.py | 7 +++---- .../components/dwd_weather_warnings/sensor.py | 6 +++--- 9 files changed, 20 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/comed_hourly_pricing/sensor.py b/homeassistant/components/comed_hourly_pricing/sensor.py index 38421813439..0e26b3406b8 100644 --- a/homeassistant/components/comed_hourly_pricing/sensor.py +++ b/homeassistant/components/comed_hourly_pricing/sensor.py @@ -15,7 +15,7 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_OFFSET +from homeassistant.const import CONF_NAME, CONF_OFFSET from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -27,8 +27,6 @@ _RESOURCE = "https://hourlypricing.comed.com/api" SCAN_INTERVAL = timedelta(minutes=5) -ATTRIBUTION = "Data provided by ComEd Hourly Pricing service" - CONF_CURRENT_HOUR_AVERAGE = "current_hour_average" CONF_FIVE_MINUTE = "five_minute" CONF_MONITORED_FEEDS = "monitored_feeds" @@ -91,7 +89,7 @@ async def async_setup_platform( class ComedHourlyPricingSensor(SensorEntity): """Implementation of a ComEd Hourly Pricing sensor.""" - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = "Data provided by ComEd Hourly Pricing service" def __init__(self, websession, offset, name, description: SensorEntityDescription): """Initialize the sensor.""" diff --git a/homeassistant/components/coronavirus/sensor.py b/homeassistant/components/coronavirus/sensor.py index 1a4f5b72fcd..7fa7c5aed08 100644 --- a/homeassistant/components/coronavirus/sensor.py +++ b/homeassistant/components/coronavirus/sensor.py @@ -1,7 +1,6 @@ """Sensor platform for the Corona virus.""" from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -34,12 +33,12 @@ async def async_setup_entry( class CoronavirusSensor(CoordinatorEntity, SensorEntity): """Sensor representing corona virus data.""" + _attr_attribution = ATTRIBUTION _attr_native_unit_of_measurement = "people" def __init__(self, coordinator, country, info_type): """Initialize coronavirus sensor.""" super().__init__(coordinator) - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} self._attr_icon = SENSORS[info_type] self._attr_unique_id = f"{country}-{info_type}" if country == OPTION_WORLDWIDE: diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index 85f8b876765..9905228c26a 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -8,13 +8,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_API_KEY, - CONF_BASE, - CONF_NAME, - CONF_QUOTE, -) +from homeassistant.const import CONF_API_KEY, CONF_BASE, CONF_NAME, CONF_QUOTE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -23,8 +17,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) _RESOURCE = "http://apilayer.net/api/live" -ATTRIBUTION = "Data provided by currencylayer.com" - DEFAULT_BASE = "USD" DEFAULT_NAME = "CurrencyLayer Sensor" @@ -67,6 +59,8 @@ def setup_platform( class CurrencylayerSensor(SensorEntity): """Implementing the Currencylayer sensor.""" + _attr_attribution = "Data provided by currencylayer.com" + def __init__(self, rest, base, quote): """Initialize the sensor.""" self.rest = rest @@ -94,11 +88,6 @@ class CurrencylayerSensor(SensorEntity): """Return the state of the sensor.""" return self._state - @property - def extra_state_attributes(self): - """Return the state attributes of the sensor.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} - def update(self) -> None: """Update current date.""" self.rest.update() diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index db79b82de53..ccd4516e39c 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -18,7 +18,6 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, @@ -49,8 +48,6 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Powered by Dark Sky" - CONF_FORECAST = "forecast" CONF_HOURLY_FORECAST = "hourly_forecast" CONF_LANGUAGE = "language" @@ -647,8 +644,8 @@ def setup_platform( class DarkSkySensor(SensorEntity): """Implementation of a Dark Sky sensor.""" + _attr_attribution = "Powered by Dark Sky" entity_description: DarkskySensorEntityDescription - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} def __init__( self, diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py index 9728da99a6f..59c6f7961c2 100644 --- a/homeassistant/components/digital_ocean/binary_sensor.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -10,7 +10,6 @@ from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -64,6 +63,8 @@ def setup_platform( class DigitalOceanBinarySensor(BinarySensorEntity): """Representation of a Digital Ocean droplet sensor.""" + _attr_attribution = ATTRIBUTION + def __init__(self, do, droplet_id): # pylint: disable=invalid-name """Initialize a new Digital Ocean sensor.""" self._digital_ocean = do @@ -90,7 +91,6 @@ class DigitalOceanBinarySensor(BinarySensorEntity): def extra_state_attributes(self): """Return the state attributes of the Digital Ocean droplet.""" return { - ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_CREATED_AT: self.data.created_at, ATTR_DROPLET_ID: self.data.id, ATTR_DROPLET_NAME: self.data.name, diff --git a/homeassistant/components/digital_ocean/switch.py b/homeassistant/components/digital_ocean/switch.py index da955e221a3..2791d83d6bc 100644 --- a/homeassistant/components/digital_ocean/switch.py +++ b/homeassistant/components/digital_ocean/switch.py @@ -7,7 +7,6 @@ from typing import Any import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -62,6 +61,8 @@ def setup_platform( class DigitalOceanSwitch(SwitchEntity): """Representation of a Digital Ocean droplet switch.""" + _attr_attribution = ATTRIBUTION + def __init__(self, do, droplet_id): # pylint: disable=invalid-name """Initialize a new Digital Ocean sensor.""" self._digital_ocean = do @@ -83,7 +84,6 @@ class DigitalOceanSwitch(SwitchEntity): def extra_state_attributes(self): """Return the state attributes of the Digital Ocean droplet.""" return { - ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_CREATED_AT: self.data.created_at, ATTR_DROPLET_ID: self.data.id, ATTR_DROPLET_NAME: self.data.name, diff --git a/homeassistant/components/discogs/sensor.py b/homeassistant/components/discogs/sensor.py index e207755ec24..51c69449d22 100644 --- a/homeassistant/components/discogs/sensor.py +++ b/homeassistant/components/discogs/sensor.py @@ -13,12 +13,7 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - CONF_TOKEN, -) +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE import homeassistant.helpers.config_validation as cv @@ -29,8 +24,6 @@ _LOGGER = logging.getLogger(__name__) ATTR_IDENTITY = "identity" -ATTRIBUTION = "Data provided by Discogs" - DEFAULT_NAME = "Discogs" ICON_RECORD = "mdi:album" @@ -111,6 +104,8 @@ def setup_platform( class DiscogsSensor(SensorEntity): """Create a new Discogs sensor for a specific type.""" + _attr_attribution = "Data provided by Discogs" + def __init__(self, discogs_data, name, description: SensorEntityDescription): """Initialize the Discogs sensor.""" self.entity_description = description @@ -135,12 +130,10 @@ class DiscogsSensor(SensorEntity): "format": f"{self._attrs['formats'][0]['name']} ({self._attrs['formats'][0]['descriptions'][0]})", "label": self._attrs["labels"][0]["name"], "released": self._attrs["year"], - ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_IDENTITY: self._discogs_data["user"], } return { - ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_IDENTITY: self._discogs_data["user"], } diff --git a/homeassistant/components/dublin_bus_transport/sensor.py b/homeassistant/components/dublin_bus_transport/sensor.py index cf65c18e91c..6b9d47aecb3 100644 --- a/homeassistant/components/dublin_bus_transport/sensor.py +++ b/homeassistant/components/dublin_bus_transport/sensor.py @@ -14,7 +14,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, TIME_MINUTES +from homeassistant.const import CONF_NAME, TIME_MINUTES from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -29,8 +29,6 @@ ATTR_DUE_IN = "Due in" ATTR_DUE_AT = "Due at" ATTR_NEXT_UP = "Later Bus" -ATTRIBUTION = "Data provided by data.dublinked.ie" - CONF_STOP_ID = "stopid" CONF_ROUTE = "route" @@ -79,6 +77,8 @@ def setup_platform( class DublinPublicTransportSensor(SensorEntity): """Implementation of an Dublin public transport sensor.""" + _attr_attribution = "Data provided by data.dublinked.ie" + def __init__(self, data, stop, route, name): """Initialize the sensor.""" self.data = data @@ -111,7 +111,6 @@ class DublinPublicTransportSensor(SensorEntity): ATTR_DUE_AT: self._times[0][ATTR_DUE_AT], ATTR_STOP_ID: self._stop, ATTR_ROUTE: self._times[0][ATTR_ROUTE], - ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_NEXT_UP: next_up, } diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 1436b416031..d6b3f6c2a0d 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -22,7 +22,7 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) -from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -31,7 +31,6 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by DWD" ATTR_REGION_NAME = "region_name" ATTR_REGION_ID = "region_id" ATTR_LAST_UPDATE = "last_update" @@ -108,6 +107,8 @@ def setup_platform( class DwdWeatherWarningsSensor(SensorEntity): """Representation of a DWD-Weather-Warnings sensor.""" + _attr_attribution = "Data provided by DWD" + def __init__( self, api, @@ -130,7 +131,6 @@ class DwdWeatherWarningsSensor(SensorEntity): def extra_state_attributes(self): """Return the state attributes of the DWD-Weather-Warnings.""" data = { - ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_REGION_NAME: self._api.api.warncell_name, ATTR_REGION_ID: self._api.api.warncell_id, ATTR_LAST_UPDATE: self._api.api.last_update, From b28ad1282a8b31b604d5bd2496a7fa75ad2c7e77 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Oct 2022 11:56:18 +0200 Subject: [PATCH 392/985] Use percentage constant as unit in LaMetric brightness (#80162) --- homeassistant/components/lametric/number.py | 3 ++- tests/components/lametric/test_number.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lametric/number.py b/homeassistant/components/lametric/number.py index 9275160f497..c82fded83c8 100644 --- a/homeassistant/components/lametric/number.py +++ b/homeassistant/components/lametric/number.py @@ -9,6 +9,7 @@ from demetriek import Device, LaMetricDevice from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -43,7 +44,7 @@ NUMBERS = [ native_step=1, native_min_value=0, native_max_value=100, - native_unit_of_measurement="%", + native_unit_of_measurement=PERCENTAGE, value_fn=lambda device: device.display.brightness, set_value_fn=lambda device, bri: device.display(brightness=int(bri)), ), diff --git a/tests/components/lametric/test_number.py b/tests/components/lametric/test_number.py index f80b2214577..2ecfc43246e 100644 --- a/tests/components/lametric/test_number.py +++ b/tests/components/lametric/test_number.py @@ -19,6 +19,7 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant @@ -46,7 +47,7 @@ async def test_brightness( assert state.attributes.get(ATTR_MAX) == 100 assert state.attributes.get(ATTR_MIN) == 0 assert state.attributes.get(ATTR_STEP) == 1 - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "%" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "100" entry = entity_registry.async_get(state.entity_id) From f43c802a03640b893472de5a376a1bcc3723044c Mon Sep 17 00:00:00 2001 From: Jeef Date: Wed, 12 Oct 2022 04:05:22 -0600 Subject: [PATCH 393/985] Flume code quality improvments (#79815) --- homeassistant/components/flume/binary_sensor.py | 10 ++++++---- homeassistant/components/flume/entity.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/flume/binary_sensor.py b/homeassistant/components/flume/binary_sensor.py index 235d7c3edd6..f6b1f63a26c 100644 --- a/homeassistant/components/flume/binary_sensor.py +++ b/homeassistant/components/flume/binary_sensor.py @@ -33,6 +33,11 @@ from .coordinator import ( from .entity import FlumeEntity from .util import get_valid_flume_devices +BINARY_SENSOR_DESCRIPTION_CONNECTED = BinarySensorEntityDescription( + name="Connected", + key="connected", +) + @dataclass class FlumeBinarySensorRequiredKeysMixin: @@ -93,10 +98,7 @@ async def async_setup_entry( connection_sensor = FlumeConnectionBinarySensor( coordinator=connection_coordinator, - description=BinarySensorEntityDescription( - name="Connected", - key="connected", - ), + description=BINARY_SENSOR_DESCRIPTION_CONNECTED, device_id=device_id, location_name=device_location_name, is_bridge=(device[KEY_DEVICE_TYPE] is FLUME_TYPE_BRIDGE), diff --git a/homeassistant/components/flume/entity.py b/homeassistant/components/flume/entity.py index 4aeba6d2bc6..7cd84127c64 100644 --- a/homeassistant/components/flume/entity.py +++ b/homeassistant/components/flume/entity.py @@ -10,7 +10,7 @@ from homeassistant.helpers.update_coordinator import ( from .const import DOMAIN -class FlumeEntity(CoordinatorEntity[DataUpdateCoordinator]): +class FlumeEntity(CoordinatorEntity[DataUpdateCoordinator[None]]): """Base entity class.""" _attr_attribution = "Data provided by Flume API" From 6e2786ae1cff327213eab8fb91511e122ba9e3ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Oct 2022 12:05:36 +0200 Subject: [PATCH 394/985] Bump dorny/paths-filter from 2.10.2 to 2.11.0 (#80151) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bb50142c9e0..098f4f590f7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -70,7 +70,7 @@ jobs: echo "::set-output name=key::pre-commit-${{ env.CACHE_VERSION }}-${{ hashFiles('.pre-commit-config.yaml') }}" - name: Filter for core changes - uses: dorny/paths-filter@v2.10.2 + uses: dorny/paths-filter@v2.11.0 id: core with: filters: .core_files.yaml @@ -85,7 +85,7 @@ jobs: echo "Result:" cat .integration_paths.yaml - name: Filter for integration changes - uses: dorny/paths-filter@v2.10.2 + uses: dorny/paths-filter@v2.11.0 id: integrations with: filters: .integration_paths.yaml From ec55a7b603a09b562dde1674f8191e467c974c76 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Wed, 12 Oct 2022 06:23:12 -0400 Subject: [PATCH 395/985] Add logger to default config for set level service (#80033) --- .../components/default_config/manifest.json | 1 + homeassistant/components/logger/__init__.py | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index d33aee6e030..8e1a2f01168 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -21,6 +21,7 @@ "input_select", "input_text", "logbook", + "logger", "map", "media_source", "mobile_app", diff --git a/homeassistant/components/logger/__init__.py b/homeassistant/components/logger/__init__.py index 2b8bec957fa..5fc999d7d11 100644 --- a/homeassistant/components/logger/__init__.py +++ b/homeassistant/components/logger/__init__.py @@ -23,8 +23,6 @@ LOGSEVERITY = { "NOTSET": 0, } -DEFAULT_LOGSEVERITY = "DEBUG" - LOGGER_DEFAULT = "default" LOGGER_LOGS = "logs" LOGGER_FILTERS = "filters" @@ -68,13 +66,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _set_log_level(logging.getLogger(key), value) # Set default log severity - set_default_log_level(config[DOMAIN].get(LOGGER_DEFAULT, DEFAULT_LOGSEVERITY)) + logger_config = config.get(DOMAIN, {}) - if LOGGER_LOGS in config[DOMAIN]: + if LOGGER_DEFAULT in logger_config: + set_default_log_level(logger_config[LOGGER_DEFAULT]) + + if LOGGER_LOGS in logger_config: set_log_levels(config[DOMAIN][LOGGER_LOGS]) - if LOGGER_FILTERS in config[DOMAIN]: - for key, value in config[DOMAIN][LOGGER_FILTERS].items(): + if LOGGER_FILTERS in logger_config: + for key, value in logger_config[LOGGER_FILTERS].items(): logger = logging.getLogger(key) _add_log_filter(logger, value) From 6abf677092e2d45d39c515c8d4fa7e1787394766 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Wed, 12 Oct 2022 14:48:09 +0200 Subject: [PATCH 396/985] Bump plugwise to v0.25.0 and adapt relevant plugwise code (#80129) --- homeassistant/components/plugwise/const.py | 4 +- homeassistant/components/plugwise/gateway.py | 4 +- .../components/plugwise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/plugwise/conftest.py | 15 +++++-- .../all_data.json | 31 +++++++++---- .../anna_heatpump_heating/all_data.json | 21 +++++---- .../fixtures/m_adam_cooling/all_data.json | 44 ++++++++++++------- .../fixtures/m_adam_heating/all_data.json | 22 ++++++---- .../m_anna_heatpump_cooling/all_data.json | 22 ++++++---- .../m_anna_heatpump_idle/all_data.json | 38 +++++++++------- .../fixtures/p1v3_full_option/all_data.json | 21 ++++++--- .../fixtures/stretch_v31/all_data.json | 6 +-- tests/components/plugwise/test_diagnostics.py | 31 +++++++++---- 15 files changed, 175 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index c2d0d75c8a0..c1f759622fa 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -31,8 +31,8 @@ PLATFORMS_GATEWAY: Final[list[str]] = [ Platform.SWITCH, ] ZEROCONF_MAP: Final[dict[str, str]] = { - "smile": "P1", - "smile_thermo": "Anna", + "smile": "Smile P1", + "smile_thermo": "Smile Anna", "smile_open_therm": "Adam", "stretch": "Stretch", } diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index 4fde6a54a4a..5d951e6f997 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -72,8 +72,8 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: config_entry_id=entry.entry_id, identifiers={(DOMAIN, str(api.gateway_id))}, manufacturer="Plugwise", - name=entry.title, - model=f"Smile {api.smile_name}", + model=api.smile_model, + name=api.smile_name, sw_version=api.smile_version[0], ) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 1b17f3e49f5..f49e7b7c508 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==0.21.4"], + "requirements": ["plugwise==0.25.0"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index e2977c1e23a..d111949e8ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1312,7 +1312,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.21.4 +plugwise==0.25.0 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1d0498d985..e5e6894b928 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -939,7 +939,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.21.4 +plugwise==0.25.0 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index d7941f74450..aa34fc8bed6 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -63,6 +63,7 @@ def mock_smile_config_flow() -> Generator[None, MagicMock, None]: ) as smile_mock: smile = smile_mock.return_value smile.smile_hostname = "smile12345" + smile.smile_model = "Test Model" smile.smile_name = "Test Smile Name" smile.connect.return_value = True yield smile @@ -83,6 +84,7 @@ def mock_smile_adam() -> Generator[None, MagicMock, None]: smile.smile_version = "3.0.15" smile.smile_type = "thermostat" smile.smile_hostname = "smile98765" + smile.smile_model = "Gateway" smile.smile_name = "Adam" smile.connect.return_value = True @@ -108,6 +110,7 @@ def mock_smile_adam_2() -> Generator[None, MagicMock, None]: smile.smile_version = "3.6.4" smile.smile_type = "thermostat" smile.smile_hostname = "smile98765" + smile.smile_model = "Gateway" smile.smile_name = "Adam" smile.connect.return_value = True @@ -133,6 +136,7 @@ def mock_smile_adam_3() -> Generator[None, MagicMock, None]: smile.smile_version = "3.6.4" smile.smile_type = "thermostat" smile.smile_hostname = "smile98765" + smile.smile_model = "Gateway" smile.smile_name = "Adam" smile.connect.return_value = True @@ -157,7 +161,8 @@ def mock_smile_anna() -> Generator[None, MagicMock, None]: smile.smile_version = "4.0.15" smile.smile_type = "thermostat" smile.smile_hostname = "smile98765" - smile.smile_name = "Anna" + smile.smile_model = "Gateway" + smile.smile_name = "Smile Anna" smile.connect.return_value = True @@ -181,7 +186,8 @@ def mock_smile_anna_2() -> Generator[None, MagicMock, None]: smile.smile_version = "4.0.15" smile.smile_type = "thermostat" smile.smile_hostname = "smile98765" - smile.smile_name = "Anna" + smile.smile_model = "Gateway" + smile.smile_name = "Smile Anna" smile.connect.return_value = True @@ -205,7 +211,8 @@ def mock_smile_anna_3() -> Generator[None, MagicMock, None]: smile.smile_version = "4.0.15" smile.smile_type = "thermostat" smile.smile_hostname = "smile98765" - smile.smile_name = "Anna" + smile.smile_model = "Gateway" + smile.smile_name = "Smile Anna" smile.connect.return_value = True @@ -229,6 +236,7 @@ def mock_smile_p1() -> Generator[None, MagicMock, None]: smile.smile_version = "3.3.9" smile.smile_type = "power" smile.smile_hostname = "smile98765" + smile.smile_model = "Gateway" smile.smile_name = "Smile P1" smile.connect.return_value = True @@ -253,6 +261,7 @@ def mock_stretch() -> Generator[None, MagicMock, None]: smile.smile_version = "3.1.11" smile.smile_type = "stretch" smile.smile_hostname = "stretch98765" + smile.smile_model = "Gateway" smile.smile_name = "Stretch" smile.connect.return_value = True diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json index 08792347af0..d62ff0e249d 100644 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json +++ b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json @@ -26,7 +26,8 @@ "upper_bound": 99.9, "resolution": 0.01 }, - "preset_modes": ["home", "asleep", "away", "no_frost"], + "available": true, + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], "active_preset": "away", "available_schedules": [ "CV Roan", @@ -53,6 +54,7 @@ "name": "Floor kraan", "zigbee_mac_address": "ABCD012345670A02", "vendor": "Plugwise", + "available": true, "sensors": { "temperature": 26.0, "setpoint": 21.5, @@ -69,6 +71,7 @@ "name": "Bios Cv Thermostatic Radiator ", "zigbee_mac_address": "ABCD012345670A09", "vendor": "Plugwise", + "available": true, "sensors": { "temperature": 17.2, "setpoint": 13.0, @@ -92,7 +95,8 @@ "upper_bound": 99.9, "resolution": 0.01 }, - "preset_modes": ["home", "asleep", "away", "no_frost"], + "available": true, + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], "active_preset": "home", "available_schedules": [ "CV Roan", @@ -116,12 +120,11 @@ "hardware": "AME Smile 2.0 board", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", "mac_address": "012345670001", - "model": "Adam", + "model": "Gateway", "name": "Adam", "zigbee_mac_address": "ABCD012345670101", - "vendor": "Plugwise B.V.", + "vendor": "Plugwise", "regulation_mode": "heating", - "regulation_modes": [], "binary_sensors": { "plugwise_notification": true }, @@ -138,6 +141,7 @@ "name": "Thermostatic Radiator Jessie", "zigbee_mac_address": "ABCD012345670A10", "vendor": "Plugwise", + "available": true, "sensors": { "temperature": 17.1, "setpoint": 15.0, @@ -154,6 +158,7 @@ "name": "Playstation Smart Plug", "zigbee_mac_address": "ABCD012345670A12", "vendor": "Plugwise", + "available": true, "sensors": { "electricity_consumed": 82.6, "electricity_consumed_interval": 8.6, @@ -173,6 +178,7 @@ "name": "CV Pomp", "zigbee_mac_address": "ABCD012345670A05", "vendor": "Plugwise", + "available": true, "sensors": { "electricity_consumed": 35.6, "electricity_consumed_interval": 7.37, @@ -205,6 +211,7 @@ "name": "NAS", "zigbee_mac_address": "ABCD012345670A14", "vendor": "Plugwise", + "available": true, "sensors": { "electricity_consumed": 16.5, "electricity_consumed_interval": 0.5, @@ -224,6 +231,7 @@ "name": "USG Smart Plug", "zigbee_mac_address": "ABCD012345670A16", "vendor": "Plugwise", + "available": true, "sensors": { "electricity_consumed": 8.5, "electricity_consumed_interval": 0.0, @@ -243,6 +251,7 @@ "name": "NVR", "zigbee_mac_address": "ABCD012345670A15", "vendor": "Plugwise", + "available": true, "sensors": { "electricity_consumed": 34.0, "electricity_consumed_interval": 9.15, @@ -262,6 +271,7 @@ "name": "Fibaro HC2", "zigbee_mac_address": "ABCD012345670A13", "vendor": "Plugwise", + "available": true, "sensors": { "electricity_consumed": 12.5, "electricity_consumed_interval": 3.8, @@ -288,7 +298,8 @@ "upper_bound": 99.9, "resolution": 0.01 }, - "preset_modes": ["home", "asleep", "away", "no_frost"], + "available": true, + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], "active_preset": "asleep", "available_schedules": [ "CV Roan", @@ -315,6 +326,7 @@ "name": "Thermostatic Radiator Badkamer", "zigbee_mac_address": "ABCD012345670A17", "vendor": "Plugwise", + "available": true, "sensors": { "temperature": 19.1, "setpoint": 14.0, @@ -338,7 +350,8 @@ "upper_bound": 99.9, "resolution": 0.01 }, - "preset_modes": ["home", "asleep", "away", "no_frost"], + "available": true, + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], "active_preset": "away", "available_schedules": [ "CV Roan", @@ -364,6 +377,7 @@ "name": "Ziggo Modem", "zigbee_mac_address": "ABCD012345670A01", "vendor": "Plugwise", + "available": true, "sensors": { "electricity_consumed": 12.2, "electricity_consumed_interval": 2.97, @@ -390,7 +404,8 @@ "upper_bound": 100.0, "resolution": 0.01 }, - "preset_modes": ["home", "asleep", "away", "no_frost"], + "available": true, + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], "active_preset": "no_frost", "available_schedules": [ "CV Roan", diff --git a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json index 1cc94ca6347..be2dd05011b 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json @@ -1,6 +1,6 @@ [ { - "smile_name": "Smile", + "smile_name": "Smile Anna", "gateway_id": "015ae9ea3f964e668e490fa39da3870b", "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", "cooling_present": true, @@ -19,7 +19,7 @@ "upper_bound": 100.0, "resolution": 1.0 }, - "elga_cooling_enabled": true, + "available": true, "binary_sensors": { "dhw_state": false, "heating_state": true, @@ -30,6 +30,7 @@ }, "sensors": { "water_temperature": 29.1, + "dhw_temperature": 46.3, "intended_boiler_temperature": 0.0, "modulation_level": 52, "return_temperature": 25.1, @@ -46,9 +47,9 @@ "hardware": "AME Smile 2.0 board", "location": "a57efe5f145f498c9be62a9b63626fbf", "mac_address": "012345670001", - "model": "Smile", - "name": "Smile", - "vendor": "Plugwise B.V.", + "model": "Gateway", + "name": "Smile Anna", + "vendor": "Plugwise", "binary_sensors": { "plugwise_notification": false }, @@ -61,15 +62,18 @@ "firmware": "2018-02-08T11:15:53+01:00", "hardware": "6539-1301-5002", "location": "c784ee9fdab44e1395b8dee7d7a497d5", - "model": "Anna", + "model": "ThermoTouch", "name": "Anna", "vendor": "Plugwise", "thermostat": { + "setpoint_low": 20.5, + "setpoint_high": 24.0, "setpoint": 20.5, "lower_bound": 4.0, "upper_bound": 30.0, "resolution": 0.1 }, + "available": true, "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], "active_preset": "home", "available_schedules": ["standaard"], @@ -78,10 +82,11 @@ "mode": "auto", "sensors": { "temperature": 19.3, - "setpoint": 20.5, "illuminance": 86.0, "cooling_activation_outdoor_temperature": 21.0, - "cooling_deactivation_threshold": 4.0 + "cooling_deactivation_threshold": 4.0, + "setpoint_low": 20.5, + "setpoint_high": 24.0 } } } diff --git a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json index 1f7c82983d4..246ae5dff50 100644 --- a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json @@ -10,23 +10,31 @@ "ad4838d7d35c4d6ea796ee12ae5aedf8": { "dev_class": "thermostat", "location": "f2bf9048bef64cc5b6d5110154e33c81", - "model": "Anna", + "model": "ThermoTouch", "name": "Anna", - "vendor": "Plugwise B.V.", + "vendor": "Plugwise", "thermostat": { - "setpoint": 18.5, + "setpoint": 20.0, + "setpoint_low": 20.0, + "setpoint_high": 23.5, "lower_bound": 1.0, "upper_bound": 35.0, "resolution": 0.01 }, - "preset_modes": ["home", "asleep", "away", "no_frost"], + "available": true, + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], "active_preset": "asleep", "available_schedules": ["Weekschema", "Badkamer", "Test"], "selected_schedule": "None", "last_used": "Weekschema", "control_state": "cooling", "mode": "cool", - "sensors": { "temperature": 18.1, "setpoint": 18.5 } + "sensors": { + "temperature": 25.8, + "setpoint": 20.0, + "setpoint_low": 20.0, + "setpoint_high": 23.5 + } }, "1772a4ea304041adb83f357b751341ff": { "dev_class": "thermo_sensor", @@ -37,6 +45,7 @@ "name": "Tom Badkamer", "zigbee_mac_address": "ABCD012345670A01", "vendor": "Plugwise", + "available": true, "sensors": { "temperature": 21.6, "battery": 99, @@ -54,12 +63,15 @@ "zigbee_mac_address": "ABCD012345670A04", "vendor": "Plugwise", "thermostat": { - "setpoint": 15.0, + "setpoint": 19.0, + "setpoint_low": 19.0, + "setpoint_high": 25.0, "lower_bound": 0.0, "upper_bound": 99.9, "resolution": 0.01 }, - "preset_modes": ["home", "asleep", "away", "no_frost"], + "available": true, + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], "active_preset": "home", "available_schedules": ["Weekschema", "Badkamer", "Test"], "selected_schedule": "Badkamer", @@ -67,9 +79,11 @@ "control_state": "off", "mode": "auto", "sensors": { - "temperature": 17.9, + "temperature": 239, "battery": 56, - "setpoint": 15.0 + "setpoint": 20.0, + "setpoint_low": 20.0, + "setpoint_high": 23.5 } }, "da224107914542988a88561b4452b0f6": { @@ -78,10 +92,10 @@ "hardware": "AME Smile 2.0 board", "location": "bc93488efab249e5bc54fd7e175a6f91", "mac_address": "012345670001", - "model": "Adam", + "model": "Gateway", "name": "Adam", "zigbee_mac_address": "ABCD012345670101", - "vendor": "Plugwise B.V.", + "vendor": "Plugwise", "regulation_mode": "cooling", "regulation_modes": [ "cooling", @@ -94,7 +108,7 @@ "plugwise_notification": false }, "sensors": { - "outdoor_temperature": -1.25 + "outdoor_temperature": 29.65 } }, "056ee145a816487eaa69243c3280f8bf": { @@ -108,7 +122,7 @@ "upper_bound": 95.0, "resolution": 0.01 }, - "adam_cooling_enabled": true, + "available": true, "binary_sensors": { "cooling_state": true, "dhw_state": false, @@ -116,8 +130,8 @@ "flame_state": false }, "sensors": { - "water_temperature": 37.0, - "intended_boiler_temperature": 38.1 + "water_temperature": 19.0, + "intended_boiler_temperature": 17.5 }, "switches": { "dhw_cm_switch": false diff --git a/tests/components/plugwise/fixtures/m_adam_heating/all_data.json b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json index 0a00a5b7b1c..8ee3df544e5 100644 --- a/tests/components/plugwise/fixtures/m_adam_heating/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json @@ -10,23 +10,24 @@ "ad4838d7d35c4d6ea796ee12ae5aedf8": { "dev_class": "thermostat", "location": "f2bf9048bef64cc5b6d5110154e33c81", - "model": "Anna", + "model": "ThermoTouch", "name": "Anna", - "vendor": "Plugwise B.V.", + "vendor": "Plugwise", "thermostat": { - "setpoint": 18.5, + "setpoint": 20.0, "lower_bound": 1.0, "upper_bound": 35.0, "resolution": 0.01 }, - "preset_modes": ["home", "asleep", "away", "no_frost"], + "available": true, + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], "active_preset": "asleep", "available_schedules": ["Weekschema", "Badkamer", "Test"], "selected_schedule": "None", "last_used": "Weekschema", "control_state": "heating", "mode": "heat", - "sensors": { "temperature": 18.1, "setpoint": 18.5 } + "sensors": { "temperature": 19.1, "setpoint": 20.0 } }, "1772a4ea304041adb83f357b751341ff": { "dev_class": "thermo_sensor", @@ -37,8 +38,9 @@ "name": "Tom Badkamer", "zigbee_mac_address": "ABCD012345670A01", "vendor": "Plugwise", + "available": true, "sensors": { - "temperature": 21.6, + "temperature": 18.6, "battery": 99, "temperature_difference": 2.3, "valve_position": 0.0 @@ -59,7 +61,8 @@ "upper_bound": 99.9, "resolution": 0.01 }, - "preset_modes": ["home", "asleep", "away", "no_frost"], + "available": true, + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], "active_preset": "home", "available_schedules": ["Weekschema", "Badkamer", "Test"], "selected_schedule": "Badkamer", @@ -78,10 +81,10 @@ "hardware": "AME Smile 2.0 board", "location": "bc93488efab249e5bc54fd7e175a6f91", "mac_address": "012345670001", - "model": "Adam", + "model": "Gateway", "name": "Adam", "zigbee_mac_address": "ABCD012345670101", - "vendor": "Plugwise B.V.", + "vendor": "Plugwise", "regulation_mode": "heating", "regulation_modes": ["heating", "off", "bleeding_cold", "bleeding_hot"], "binary_sensors": { @@ -108,6 +111,7 @@ "upper_bound": 60.0, "resolution": 0.01 }, + "available": true, "binary_sensors": { "dhw_state": false, "heating_state": true, diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json index a9a92126265..d6d34801641 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json @@ -1,6 +1,6 @@ [ { - "smile_name": "Smile", + "smile_name": "Smile Anna", "gateway_id": "015ae9ea3f964e668e490fa39da3870b", "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", "cooling_present": true, @@ -19,7 +19,7 @@ "upper_bound": 100.0, "resolution": 1.0 }, - "elga_cooling_enabled": true, + "available": true, "binary_sensors": { "dhw_state": false, "heating_state": false, @@ -30,6 +30,7 @@ }, "sensors": { "water_temperature": 29.1, + "dhw_temperature": 46.3, "intended_boiler_temperature": 0.0, "modulation_level": 52, "return_temperature": 25.1, @@ -46,9 +47,9 @@ "hardware": "AME Smile 2.0 board", "location": "a57efe5f145f498c9be62a9b63626fbf", "mac_address": "012345670001", - "model": "Smile", - "name": "Smile", - "vendor": "Plugwise B.V.", + "model": "Gateway", + "name": "Smile Anna", + "vendor": "Plugwise", "binary_sensors": { "plugwise_notification": false }, @@ -61,15 +62,18 @@ "firmware": "2018-02-08T11:15:53+01:00", "hardware": "6539-1301-5002", "location": "c784ee9fdab44e1395b8dee7d7a497d5", - "model": "Anna", + "model": "ThermoTouch", "name": "Anna", "vendor": "Plugwise", "thermostat": { "setpoint": 24.0, + "setpoint_low": 20.5, + "setpoint_high": 24.0, "lower_bound": 4.0, "upper_bound": 30.0, "resolution": 0.1 }, + "available": true, "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], "active_preset": "home", "available_schedules": ["standaard"], @@ -78,10 +82,12 @@ "mode": "auto", "sensors": { "temperature": 26.3, - "setpoint": 24.0, "illuminance": 86.0, "cooling_activation_outdoor_temperature": 21.0, - "cooling_deactivation_threshold": 4.0 + "cooling_deactivation_threshold": 4.0, + "setpoint": 24.0, + "setpoint_low": 20.5, + "setpoint_high": 24.0 } } } diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json index 0c1fef1a171..ca9559ca073 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json @@ -1,6 +1,6 @@ [ { - "smile_name": "Smile", + "smile_name": "Smile Anna", "gateway_id": "015ae9ea3f964e668e490fa39da3870b", "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", "cooling_present": true, @@ -19,7 +19,7 @@ "upper_bound": 100.0, "resolution": 1.0 }, - "elga_cooling_enabled": true, + "available": true, "binary_sensors": { "dhw_state": false, "heating_state": false, @@ -29,12 +29,13 @@ "flame_state": false }, "sensors": { - "water_temperature": 29.1, - "intended_boiler_temperature": 0.0, - "modulation_level": 52, - "return_temperature": 25.1, + "water_temperature": 19.1, + "dhw_temperature": 46.3, + "intended_boiler_temperature": 18.0, + "modulation_level": 0, + "return_temperature": 22.0, "water_pressure": 1.57, - "outdoor_air_temperature": 3.0 + "outdoor_air_temperature": 28.2 }, "switches": { "dhw_cm_switch": false @@ -46,14 +47,14 @@ "hardware": "AME Smile 2.0 board", "location": "a57efe5f145f498c9be62a9b63626fbf", "mac_address": "012345670001", - "model": "Smile", - "name": "Smile", - "vendor": "Plugwise B.V.", + "model": "Gateway", + "name": "Smile Anna", + "vendor": "Plugwise", "binary_sensors": { "plugwise_notification": false }, "sensors": { - "outdoor_temperature": 20.2 + "outdoor_temperature": 28.2 } }, "3cb70739631c4d17a86b8b12e8a5161b": { @@ -61,15 +62,18 @@ "firmware": "2018-02-08T11:15:53+01:00", "hardware": "6539-1301-5002", "location": "c784ee9fdab44e1395b8dee7d7a497d5", - "model": "Anna", + "model": "ThermoTouch", "name": "Anna", "vendor": "Plugwise", "thermostat": { "setpoint": 20.5, + "setpoint_low": 20.5, + "setpoint_high": 24.0, "lower_bound": 4.0, "upper_bound": 30.0, "resolution": 0.1 }, + "available": true, "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], "active_preset": "home", "available_schedules": ["standaard"], @@ -77,11 +81,13 @@ "last_used": "standaard", "mode": "auto", "sensors": { - "temperature": 21.3, - "setpoint": 20.5, + "temperature": 23.0, "illuminance": 86.0, - "cooling_activation_outdoor_temperature": 21.0, - "cooling_deactivation_threshold": 4.0 + "cooling_activation_outdoor_temperature": 25.0, + "cooling_deactivation_threshold": 4.0, + "setpoint": 20.5, + "setpoint_low": 20.5, + "setpoint_high": 24.0 } } } diff --git a/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json b/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json index fbf5aa63a5f..c52f33e6323 100644 --- a/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json +++ b/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json @@ -1,19 +1,30 @@ [ { - "smile_name": "P1", - "gateway_id": "e950c7d5e1ee407a858e2a8b5016c8b3", + "smile_name": "Smile P1", + "gateway_id": "cd3e822288064775a7c4afcdd70bdda2", "notifications": {} }, { - "e950c7d5e1ee407a858e2a8b5016c8b3": { + "cd3e822288064775a7c4afcdd70bdda2": { "dev_class": "gateway", "firmware": "3.3.9", "hardware": "AME Smile 2.0 board", "location": "cd3e822288064775a7c4afcdd70bdda2", "mac_address": "012345670001", - "model": "P1", + "model": "Gateway", + "name": "Smile P1", + "vendor": "Plugwise", + "binary_sensors": { + "plugwise_notification": false + } + }, + "e950c7d5e1ee407a858e2a8b5016c8b3": { + "dev_class": "smartmeter", + "location": "cd3e822288064775a7c4afcdd70bdda2", + "model": "2M550E-1012", "name": "P1", - "vendor": "Plugwise B.V.", + "vendor": "ISKRAEMECO", + "available": true, "sensors": { "net_electricity_point": -2816, "electricity_consumed_peak_point": 0, diff --git a/tests/components/plugwise/fixtures/stretch_v31/all_data.json b/tests/components/plugwise/fixtures/stretch_v31/all_data.json index 1ff62e9e619..1ce34e376d7 100644 --- a/tests/components/plugwise/fixtures/stretch_v31/all_data.json +++ b/tests/components/plugwise/fixtures/stretch_v31/all_data.json @@ -10,10 +10,10 @@ "firmware": "3.1.11", "location": "0000aaaa0000aaaa0000aaaa0000aa00", "mac_address": "01:23:45:67:89:AB", - "model": "Stretch", + "model": "Gateway", "name": "Stretch", - "vendor": "Plugwise B.V.", - "zigbee_mac_address": "ABCD012345670101" + "zigbee_mac_address": "ABCD012345670101", + "vendor": "Plugwise" }, "5871317346d045bc9f6b987ef25ee638": { "dev_class": "water_heater_vessel", diff --git a/tests/components/plugwise/test_diagnostics.py b/tests/components/plugwise/test_diagnostics.py index 3e3b2259e15..7e8d574d5bd 100644 --- a/tests/components/plugwise/test_diagnostics.py +++ b/tests/components/plugwise/test_diagnostics.py @@ -46,7 +46,8 @@ async def test_diagnostics( "upper_bound": 99.9, "resolution": 0.01, }, - "preset_modes": ["home", "asleep", "away", "no_frost"], + "available": True, + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], "active_preset": "away", "available_schedules": [ "CV Roan", @@ -69,6 +70,7 @@ async def test_diagnostics( "name": "Floor kraan", "zigbee_mac_address": "ABCD012345670A02", "vendor": "Plugwise", + "available": True, "sensors": { "temperature": 26.0, "setpoint": 21.5, @@ -85,6 +87,7 @@ async def test_diagnostics( "name": "Bios Cv Thermostatic Radiator ", "zigbee_mac_address": "ABCD012345670A09", "vendor": "Plugwise", + "available": True, "sensors": { "temperature": 17.2, "setpoint": 13.0, @@ -108,7 +111,8 @@ async def test_diagnostics( "upper_bound": 99.9, "resolution": 0.01, }, - "preset_modes": ["home", "asleep", "away", "no_frost"], + "available": True, + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], "active_preset": "home", "available_schedules": [ "CV Roan", @@ -128,12 +132,11 @@ async def test_diagnostics( "hardware": "AME Smile 2.0 board", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", "mac_address": "012345670001", - "model": "Adam", + "model": "Gateway", "name": "Adam", "zigbee_mac_address": "ABCD012345670101", - "vendor": "Plugwise B.V.", + "vendor": "Plugwise", "regulation_mode": "heating", - "regulation_modes": [], "binary_sensors": {"plugwise_notification": True}, "sensors": {"outdoor_temperature": 7.81}, }, @@ -146,6 +149,7 @@ async def test_diagnostics( "name": "Thermostatic Radiator Jessie", "zigbee_mac_address": "ABCD012345670A10", "vendor": "Plugwise", + "available": True, "sensors": { "temperature": 17.1, "setpoint": 15.0, @@ -162,6 +166,7 @@ async def test_diagnostics( "name": "Playstation Smart Plug", "zigbee_mac_address": "ABCD012345670A12", "vendor": "Plugwise", + "available": True, "sensors": { "electricity_consumed": 82.6, "electricity_consumed_interval": 8.6, @@ -178,6 +183,7 @@ async def test_diagnostics( "name": "CV Pomp", "zigbee_mac_address": "ABCD012345670A05", "vendor": "Plugwise", + "available": True, "sensors": { "electricity_consumed": 35.6, "electricity_consumed_interval": 7.37, @@ -206,6 +212,7 @@ async def test_diagnostics( "name": "NAS", "zigbee_mac_address": "ABCD012345670A14", "vendor": "Plugwise", + "available": True, "sensors": { "electricity_consumed": 16.5, "electricity_consumed_interval": 0.5, @@ -222,6 +229,7 @@ async def test_diagnostics( "name": "USG Smart Plug", "zigbee_mac_address": "ABCD012345670A16", "vendor": "Plugwise", + "available": True, "sensors": { "electricity_consumed": 8.5, "electricity_consumed_interval": 0.0, @@ -238,6 +246,7 @@ async def test_diagnostics( "name": "NVR", "zigbee_mac_address": "ABCD012345670A15", "vendor": "Plugwise", + "available": True, "sensors": { "electricity_consumed": 34.0, "electricity_consumed_interval": 9.15, @@ -254,6 +263,7 @@ async def test_diagnostics( "name": "Fibaro HC2", "zigbee_mac_address": "ABCD012345670A13", "vendor": "Plugwise", + "available": True, "sensors": { "electricity_consumed": 12.5, "electricity_consumed_interval": 3.8, @@ -277,7 +287,8 @@ async def test_diagnostics( "upper_bound": 99.9, "resolution": 0.01, }, - "preset_modes": ["home", "asleep", "away", "no_frost"], + "available": True, + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], "active_preset": "asleep", "available_schedules": [ "CV Roan", @@ -300,6 +311,7 @@ async def test_diagnostics( "name": "Thermostatic Radiator Badkamer", "zigbee_mac_address": "ABCD012345670A17", "vendor": "Plugwise", + "available": True, "sensors": { "temperature": 19.1, "setpoint": 14.0, @@ -323,7 +335,8 @@ async def test_diagnostics( "upper_bound": 99.9, "resolution": 0.01, }, - "preset_modes": ["home", "asleep", "away", "no_frost"], + "available": True, + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], "active_preset": "away", "available_schedules": [ "CV Roan", @@ -345,6 +358,7 @@ async def test_diagnostics( "name": "Ziggo Modem", "zigbee_mac_address": "ABCD012345670A01", "vendor": "Plugwise", + "available": True, "sensors": { "electricity_consumed": 12.2, "electricity_consumed_interval": 2.97, @@ -368,7 +382,8 @@ async def test_diagnostics( "upper_bound": 100.0, "resolution": 0.01, }, - "preset_modes": ["home", "asleep", "away", "no_frost"], + "available": True, + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], "active_preset": "no_frost", "available_schedules": [ "CV Roan", From 83557ef762c8ba041642b050f3ecb86b8e420806 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Oct 2022 14:51:09 +0200 Subject: [PATCH 397/985] Add myself as codeowner to Alert (#80169) --- CODEOWNERS | 4 ++-- homeassistant/components/alert/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 08310e49520..5783cf31cab 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -57,8 +57,8 @@ build.json @home-assistant/supervisor /tests/components/aladdin_connect/ @mkmer /homeassistant/components/alarm_control_panel/ @home-assistant/core /tests/components/alarm_control_panel/ @home-assistant/core -/homeassistant/components/alert/ @home-assistant/core -/tests/components/alert/ @home-assistant/core +/homeassistant/components/alert/ @home-assistant/core @frenck +/tests/components/alert/ @home-assistant/core @frenck /homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy /tests/components/alexa/ @home-assistant/cloud @ochlocracy /homeassistant/components/almond/ @gcampax @balloob diff --git a/homeassistant/components/alert/manifest.json b/homeassistant/components/alert/manifest.json index bf9724ec2b9..c2cdf20f54b 100644 --- a/homeassistant/components/alert/manifest.json +++ b/homeassistant/components/alert/manifest.json @@ -3,7 +3,7 @@ "name": "Alert", "documentation": "https://www.home-assistant.io/integrations/alert", "after_dependencies": ["notify"], - "codeowners": ["@home-assistant/core"], + "codeowners": ["@home-assistant/core", "@frenck"], "quality_scale": "internal", "iot_class": "local_push" } From 577f7904b55619fb868cbfec7fe9db4dbded5150 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 12 Oct 2022 14:59:10 +0200 Subject: [PATCH 398/985] Minor improvements of recorder typing (#80165) * Minor improvements of recorder typing * Only allow specifying statistic_ids as lists --- homeassistant/components/recorder/core.py | 9 +++-- .../components/recorder/migration.py | 9 ++++- .../components/recorder/statistics.py | 38 ++++++++----------- homeassistant/components/sensor/recorder.py | 2 +- 4 files changed, 28 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 032f1ff1ec2..2530b303e15 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -610,8 +610,12 @@ class Recorder(threading.Thread): # wait for startup to complete. If its not live, we need to continue # on. self.hass.add_job(self.async_set_db_ready) - # If shutdown happened before Home Assistant finished starting + + # We wait to start a live migration until startup has finished + # since it can be cpu intensive and we do not want it to compete + # with startup which is also cpu intensive if self._wait_startup_or_shutdown() is SHUTDOWN_TASK: + # Shutdown happened before Home Assistant finished starting self.migration_in_progress = False # Make sure we cleanly close the run if # we restart before startup finishes @@ -619,9 +623,6 @@ class Recorder(threading.Thread): self.hass.add_job(self.async_set_db_ready) return - # We wait to start the migration until startup has finished - # since it can be cpu intensive and we do not want it to compete - # with startup which is also cpu intensive if not schema_is_current: if self._migrate_schema_and_setup_run(current_version): self.schema_version = SCHEMA_VERSION diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 0ebd761ca53..3482f9aa942 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -1,9 +1,11 @@ """Schema migration helpers.""" +from __future__ import annotations + from collections.abc import Callable, Iterable import contextlib from datetime import timedelta import logging -from typing import Any, cast +from typing import TYPE_CHECKING, cast import sqlalchemy from sqlalchemy import ForeignKeyConstraint, MetaData, Table, func, text @@ -40,6 +42,9 @@ from .statistics import ( ) from .util import session_scope +if TYPE_CHECKING: + from . import Recorder + LIVE_MIGRATION_MIN_SCHEMA_VERSION = 0 _LOGGER = logging.getLogger(__name__) @@ -86,7 +91,7 @@ def live_migration(current_version: int) -> bool: def migrate_schema( - instance: Any, + instance: Recorder, hass: HomeAssistant, engine: Engine, session_maker: Callable[[], Session], diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 7ba5c5f8c73..3ff438f50da 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -582,9 +582,7 @@ def _compile_hourly_statistics_summary_mean_stmt( ) -def compile_hourly_statistics( - instance: Recorder, session: Session, start: datetime -) -> None: +def _compile_hourly_statistics(session: Session, start: datetime) -> None: """Compile hourly statistics. This will summarize 5-minute statistics for one hour: @@ -700,7 +698,7 @@ def compile_statistics(instance: Recorder, start: datetime) -> bool: if start.minute == 55: # A full hour is ready, summarize it - compile_hourly_statistics(instance, session, start) + _compile_hourly_statistics(session, start) session.add(StatisticsRuns(start=start)) @@ -776,7 +774,7 @@ def _update_statistics( def _generate_get_metadata_stmt( - statistic_ids: list[str] | tuple[str] | None = None, + statistic_ids: list[str] | None = None, statistic_type: Literal["mean"] | Literal["sum"] | None = None, statistic_source: str | None = None, ) -> StatementLambdaElement: @@ -794,10 +792,9 @@ def _generate_get_metadata_stmt( def get_metadata_with_session( - hass: HomeAssistant, session: Session, *, - statistic_ids: list[str] | tuple[str] | None = None, + statistic_ids: list[str] | None = None, statistic_type: Literal["mean"] | Literal["sum"] | None = None, statistic_source: str | None = None, ) -> dict[str, tuple[int, StatisticMetaData]]: @@ -834,14 +831,13 @@ def get_metadata_with_session( def get_metadata( hass: HomeAssistant, *, - statistic_ids: list[str] | tuple[str] | None = None, + statistic_ids: list[str] | None = None, statistic_type: Literal["mean"] | Literal["sum"] | None = None, statistic_source: str | None = None, ) -> dict[str, tuple[int, StatisticMetaData]]: """Return metadata for statistic_ids.""" with session_scope(hass=hass) as session: return get_metadata_with_session( - hass, session, statistic_ids=statistic_ids, statistic_type=statistic_type, @@ -882,7 +878,7 @@ def update_statistics_metadata( def list_statistic_ids( hass: HomeAssistant, - statistic_ids: list[str] | tuple[str] | None = None, + statistic_ids: list[str] | None = None, statistic_type: Literal["mean"] | Literal["sum"] | None = None, ) -> list[dict]: """Return all statistic_ids (or filtered one) and unit of measurement. @@ -896,7 +892,7 @@ def list_statistic_ids( # Query the database with session_scope(hass=hass) as session: metadata = get_metadata_with_session( - hass, session, statistic_type=statistic_type, statistic_ids=statistic_ids + session, statistic_type=statistic_type, statistic_ids=statistic_ids ) result = { @@ -1105,7 +1101,7 @@ def statistics_during_period( metadata = None with session_scope(hass=hass) as session: # Fetch metadata for the given (or all) statistic_ids - metadata = get_metadata_with_session(hass, session, statistic_ids=statistic_ids) + metadata = get_metadata_with_session(session, statistic_ids=statistic_ids) if not metadata: return {} @@ -1196,7 +1192,7 @@ def _get_last_statistics( statistic_ids = [statistic_id] with session_scope(hass=hass) as session: # Fetch metadata for the given statistic_id - metadata = get_metadata_with_session(hass, session, statistic_ids=statistic_ids) + metadata = get_metadata_with_session(session, statistic_ids=statistic_ids) if not metadata: return {} metadata_id = metadata[statistic_id][0] @@ -1280,9 +1276,7 @@ def get_latest_short_term_statistics( with session_scope(hass=hass) as session: # Fetch metadata for the given statistic_ids if not metadata: - metadata = get_metadata_with_session( - hass, session, statistic_ids=statistic_ids - ) + metadata = get_metadata_with_session(session, statistic_ids=statistic_ids) if not metadata: return {} metadata_ids = [ @@ -1565,7 +1559,7 @@ def import_statistics( exception_filter=_filter_unique_constraint_integrity_error(instance), ) as session: old_metadata_dict = get_metadata_with_session( - instance.hass, session, statistic_ids=[metadata["statistic_id"]] + session, statistic_ids=[metadata["statistic_id"]] ) metadata_id = _update_or_add_metadata(session, metadata, old_metadata_dict) for stat in statistics: @@ -1590,9 +1584,7 @@ def adjust_statistics( """Process an add_statistics job.""" with session_scope(session=instance.get_session()) as session: - metadata = get_metadata_with_session( - instance.hass, session, statistic_ids=(statistic_id,) - ) + metadata = get_metadata_with_session(session, statistic_ids=[statistic_id]) if statistic_id not in metadata: return True @@ -1652,9 +1644,9 @@ def change_statistics_unit( ) -> None: """Change statistics unit for a statistic_id.""" with session_scope(session=instance.get_session()) as session: - metadata = get_metadata_with_session( - instance.hass, session, statistic_ids=(statistic_id,) - ).get(statistic_id) + metadata = get_metadata_with_session(session, statistic_ids=[statistic_id]).get( + statistic_id + ) # Guard against the statistics being removed or updated before the # change_statistics_unit job executes diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 7bb2a998b9e..6d5f08d6d56 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -387,7 +387,7 @@ def _compile_statistics( # noqa: C901 sensor_states = _get_sensor_states(hass) wanted_statistics = _wanted_statistics(sensor_states) old_metadatas = statistics.get_metadata_with_session( - hass, session, statistic_ids=[i.entity_id for i in sensor_states] + session, statistic_ids=[i.entity_id for i in sensor_states] ) # Get history between start and end From 69e10e59821f7e5ca1d4d305079f059774b67864 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 12 Oct 2022 13:35:09 +0200 Subject: [PATCH 399/985] Refactor recorder migration --- homeassistant/components/recorder/core.py | 39 ++++++---- .../components/recorder/migration.py | 72 ++++++++++++++----- tests/components/recorder/test_migrate.py | 12 ++-- 3 files changed, 84 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 2530b303e15..f7d2b774aeb 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -588,24 +588,31 @@ class Recorder(threading.Thread): def run(self) -> None: """Start processing events to save.""" - current_version = self._setup_recorder() + setup_result = self._setup_recorder() - if current_version is None: + if not setup_result: + # Give up if we could not connect self.hass.add_job(self.async_connection_failed) return - self.schema_version = current_version + schema_status = migration.validate_db_schema(self.hass, self.get_session) + if schema_status is None: + # Give up if we could not validate the schema + self.hass.add_job(self.async_connection_failed) + return + self.schema_version = schema_status.current_version - schema_is_current = migration.schema_is_current(current_version) - if schema_is_current: + schema_is_valid = migration.schema_is_valid(schema_status) + + if schema_is_valid: self._setup_run() else: self.migration_in_progress = True - self.migration_is_live = migration.live_migration(current_version) + self.migration_is_live = migration.live_migration(schema_status) self.hass.add_job(self.async_connection_success) - if self.migration_is_live or schema_is_current: + if self.migration_is_live or schema_is_valid: # If the migrate is live or the schema is current, we need to # wait for startup to complete. If its not live, we need to continue # on. @@ -623,8 +630,8 @@ class Recorder(threading.Thread): self.hass.add_job(self.async_set_db_ready) return - if not schema_is_current: - if self._migrate_schema_and_setup_run(current_version): + if not schema_is_valid: + if self._migrate_schema_and_setup_run(schema_status): self.schema_version = SCHEMA_VERSION if not self._event_listener: # If the schema migration takes so long that the end @@ -689,14 +696,14 @@ class Recorder(threading.Thread): # happens to rollback and recover self._reopen_event_session() - def _setup_recorder(self) -> None | int: - """Create connect to the database and get the schema version.""" + def _setup_recorder(self) -> bool: + """Create a connection to the database.""" tries = 1 while tries <= self.db_max_retries: try: self._setup_connection() - return migration.get_schema_version(self.get_session) + return True except UnsupportedDialect: break except Exception as err: # pylint: disable=broad-except @@ -708,14 +715,16 @@ class Recorder(threading.Thread): tries += 1 time.sleep(self.db_retry_wait) - return None + return False @callback def _async_migration_started(self) -> None: """Set the migration started event.""" self.async_migration_event.set() - def _migrate_schema_and_setup_run(self, current_version: int) -> bool: + def _migrate_schema_and_setup_run( + self, schema_status: migration.SchemaValidationStatus + ) -> bool: """Migrate schema to the latest version.""" persistent_notification.create( self.hass, @@ -727,7 +736,7 @@ class Recorder(threading.Thread): try: migration.migrate_schema( - self, self.hass, self.engine, self.get_session, current_version + self, self.hass, self.engine, self.get_session, schema_status ) except exc.DatabaseError as err: if self._handle_database_error(err): diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 3482f9aa942..227500aaf0f 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Iterable import contextlib +from dataclasses import dataclass from datetime import timedelta import logging from typing import TYPE_CHECKING, cast @@ -61,33 +62,65 @@ def raise_if_exception_missing_str(ex: Exception, match_substrs: Iterable[str]) raise ex -def get_schema_version(session_maker: Callable[[], Session]) -> int: +def get_schema_version(session_maker: Callable[[], Session]) -> int | None: """Get the schema version.""" - with session_scope(session=session_maker()) as session: - res = ( - session.query(SchemaChanges) - .order_by(SchemaChanges.change_id.desc()) - .first() - ) - current_version = getattr(res, "schema_version", None) - - if current_version is None: - current_version = _inspect_schema_version(session) - _LOGGER.debug( - "No schema version found. Inspected version: %s", current_version + try: + with session_scope(session=session_maker()) as session: + res = ( + session.query(SchemaChanges) + .order_by(SchemaChanges.change_id.desc()) + .first() ) + current_version = getattr(res, "schema_version", None) - return cast(int, current_version) + if current_version is None: + current_version = _inspect_schema_version(session) + _LOGGER.debug( + "No schema version found. Inspected version: %s", current_version + ) + + return cast(int, current_version) + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Error when determining DB schema version: %s", err) + return None -def schema_is_current(current_version: int) -> bool: +@dataclass +class SchemaValidationStatus: + """Store schema validation status.""" + + current_version: int + + +def _schema_is_current(current_version: int) -> bool: """Check if the schema is current.""" return current_version == SCHEMA_VERSION -def live_migration(current_version: int) -> bool: +def schema_is_valid(schema_status: SchemaValidationStatus) -> bool: + """Check if the schema is valid.""" + return _schema_is_current(schema_status.current_version) + + +def validate_db_schema( + hass: HomeAssistant, session_maker: Callable[[], Session] +) -> SchemaValidationStatus | None: + """Check if the schema is valid. + + This checks that the schema is the current version as well as for some common schema + errors caused by manual migration between database engines, for example importing an + SQLite database to MariaDB. + """ + current_version = get_schema_version(session_maker) + if current_version is None: + return None + + return SchemaValidationStatus(current_version) + + +def live_migration(schema_status: SchemaValidationStatus) -> bool: """Check if live migration is possible.""" - return current_version >= LIVE_MIGRATION_MIN_SCHEMA_VERSION + return schema_status.current_version >= LIVE_MIGRATION_MIN_SCHEMA_VERSION def migrate_schema( @@ -95,13 +128,14 @@ def migrate_schema( hass: HomeAssistant, engine: Engine, session_maker: Callable[[], Session], - current_version: int, + schema_status: SchemaValidationStatus, ) -> None: """Check if the schema needs to be upgraded.""" + current_version = schema_status.current_version _LOGGER.warning("Database is about to upgrade. Schema version: %s", current_version) db_ready = False for version in range(current_version, SCHEMA_VERSION): - if live_migration(version) and not db_ready: + if live_migration(SchemaValidationStatus(version)) and not db_ready: db_ready = True instance.migration_is_live = True hass.add_job(instance.async_set_db_ready) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 9e0609de5b6..45268ae819b 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -134,14 +134,16 @@ async def test_database_migration_encounters_corruption(hass): sqlite3_exception.__cause__ = sqlite3.DatabaseError() with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( - "homeassistant.components.recorder.migration.schema_is_current", - side_effect=[False, True], + "homeassistant.components.recorder.migration._schema_is_current", + side_effect=[False], ), patch( "homeassistant.components.recorder.migration.migrate_schema", side_effect=sqlite3_exception, ), patch( "homeassistant.components.recorder.core.move_away_broken_database" - ) as move_away: + ) as move_away, patch( + "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", + ): recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} @@ -159,8 +161,8 @@ async def test_database_migration_encounters_corruption_not_sqlite(hass): assert recorder.util.async_migration_in_progress(hass) is False with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( - "homeassistant.components.recorder.migration.schema_is_current", - side_effect=[False, True], + "homeassistant.components.recorder.migration._schema_is_current", + side_effect=[False], ), patch( "homeassistant.components.recorder.migration.migrate_schema", side_effect=DatabaseError("statement", {}, []), From 3a5b66fd60eb157047fd20716a57b6c75628ccfc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Oct 2022 15:02:47 +0200 Subject: [PATCH 400/985] Use percentage constant in components (#80173) --- homeassistant/components/amberelectric/sensor.py | 4 ++-- homeassistant/components/lyric/sensor.py | 3 ++- homeassistant/components/statistics/sensor.py | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/amberelectric/sensor.py b/homeassistant/components/amberelectric/sensor.py index 522cde2a95f..98aed91a941 100644 --- a/homeassistant/components/amberelectric/sensor.py +++ b/homeassistant/components/amberelectric/sensor.py @@ -20,7 +20,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CURRENCY_DOLLAR, ENERGY_KILO_WATT_HOUR +from homeassistant.const import CURRENCY_DOLLAR, ENERGY_KILO_WATT_HOUR, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -247,7 +247,7 @@ async def async_setup_entry( renewables_description = SensorEntityDescription( key="renewables", name=f"{entry.title} - Renewables", - native_unit_of_measurement="%", + native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, icon="mdi:solar-power", ) diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index d727b24eee4..4d132381d42 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -16,6 +16,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -114,7 +115,7 @@ async def async_setup_entry( name="Outdoor Humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement="%", + native_unit_of_measurement=PERCENTAGE, value=lambda device: device.displayedOutdoorHumidity, ), location, diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 3f33fa015b9..db50e4249f2 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -25,6 +25,7 @@ from homeassistant.const import ( CONF_ENTITY_ID, CONF_NAME, CONF_UNIQUE_ID, + PERCENTAGE, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -377,7 +378,7 @@ class StatisticsSensor(SensorEntity): base_unit: str | None = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) unit: str | None if self.is_binary and self._state_characteristic in STAT_BINARY_PERCENTAGE: - unit = "%" + unit = PERCENTAGE elif not base_unit: unit = None elif self._state_characteristic in STAT_NUMERIC_RETAIN_UNIT: From 4a1c40f09ba18876f31f6aef4b2ed3806fff5bf3 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 12 Oct 2022 15:12:12 +0200 Subject: [PATCH 401/985] Revert "Refactor recorder migration" This reverts commit 69e10e59821f7e5ca1d4d305079f059774b67864. --- homeassistant/components/recorder/core.py | 39 ++++------ .../components/recorder/migration.py | 72 +++++-------------- tests/components/recorder/test_migrate.py | 12 ++-- 3 files changed, 39 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index f7d2b774aeb..2530b303e15 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -588,31 +588,24 @@ class Recorder(threading.Thread): def run(self) -> None: """Start processing events to save.""" - setup_result = self._setup_recorder() + current_version = self._setup_recorder() - if not setup_result: - # Give up if we could not connect + if current_version is None: self.hass.add_job(self.async_connection_failed) return - schema_status = migration.validate_db_schema(self.hass, self.get_session) - if schema_status is None: - # Give up if we could not validate the schema - self.hass.add_job(self.async_connection_failed) - return - self.schema_version = schema_status.current_version + self.schema_version = current_version - schema_is_valid = migration.schema_is_valid(schema_status) - - if schema_is_valid: + schema_is_current = migration.schema_is_current(current_version) + if schema_is_current: self._setup_run() else: self.migration_in_progress = True - self.migration_is_live = migration.live_migration(schema_status) + self.migration_is_live = migration.live_migration(current_version) self.hass.add_job(self.async_connection_success) - if self.migration_is_live or schema_is_valid: + if self.migration_is_live or schema_is_current: # If the migrate is live or the schema is current, we need to # wait for startup to complete. If its not live, we need to continue # on. @@ -630,8 +623,8 @@ class Recorder(threading.Thread): self.hass.add_job(self.async_set_db_ready) return - if not schema_is_valid: - if self._migrate_schema_and_setup_run(schema_status): + if not schema_is_current: + if self._migrate_schema_and_setup_run(current_version): self.schema_version = SCHEMA_VERSION if not self._event_listener: # If the schema migration takes so long that the end @@ -696,14 +689,14 @@ class Recorder(threading.Thread): # happens to rollback and recover self._reopen_event_session() - def _setup_recorder(self) -> bool: - """Create a connection to the database.""" + def _setup_recorder(self) -> None | int: + """Create connect to the database and get the schema version.""" tries = 1 while tries <= self.db_max_retries: try: self._setup_connection() - return True + return migration.get_schema_version(self.get_session) except UnsupportedDialect: break except Exception as err: # pylint: disable=broad-except @@ -715,16 +708,14 @@ class Recorder(threading.Thread): tries += 1 time.sleep(self.db_retry_wait) - return False + return None @callback def _async_migration_started(self) -> None: """Set the migration started event.""" self.async_migration_event.set() - def _migrate_schema_and_setup_run( - self, schema_status: migration.SchemaValidationStatus - ) -> bool: + def _migrate_schema_and_setup_run(self, current_version: int) -> bool: """Migrate schema to the latest version.""" persistent_notification.create( self.hass, @@ -736,7 +727,7 @@ class Recorder(threading.Thread): try: migration.migrate_schema( - self, self.hass, self.engine, self.get_session, schema_status + self, self.hass, self.engine, self.get_session, current_version ) except exc.DatabaseError as err: if self._handle_database_error(err): diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 227500aaf0f..3482f9aa942 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Callable, Iterable import contextlib -from dataclasses import dataclass from datetime import timedelta import logging from typing import TYPE_CHECKING, cast @@ -62,65 +61,33 @@ def raise_if_exception_missing_str(ex: Exception, match_substrs: Iterable[str]) raise ex -def get_schema_version(session_maker: Callable[[], Session]) -> int | None: +def get_schema_version(session_maker: Callable[[], Session]) -> int: """Get the schema version.""" - try: - with session_scope(session=session_maker()) as session: - res = ( - session.query(SchemaChanges) - .order_by(SchemaChanges.change_id.desc()) - .first() + with session_scope(session=session_maker()) as session: + res = ( + session.query(SchemaChanges) + .order_by(SchemaChanges.change_id.desc()) + .first() + ) + current_version = getattr(res, "schema_version", None) + + if current_version is None: + current_version = _inspect_schema_version(session) + _LOGGER.debug( + "No schema version found. Inspected version: %s", current_version ) - current_version = getattr(res, "schema_version", None) - if current_version is None: - current_version = _inspect_schema_version(session) - _LOGGER.debug( - "No schema version found. Inspected version: %s", current_version - ) - - return cast(int, current_version) - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error when determining DB schema version: %s", err) - return None + return cast(int, current_version) -@dataclass -class SchemaValidationStatus: - """Store schema validation status.""" - - current_version: int - - -def _schema_is_current(current_version: int) -> bool: +def schema_is_current(current_version: int) -> bool: """Check if the schema is current.""" return current_version == SCHEMA_VERSION -def schema_is_valid(schema_status: SchemaValidationStatus) -> bool: - """Check if the schema is valid.""" - return _schema_is_current(schema_status.current_version) - - -def validate_db_schema( - hass: HomeAssistant, session_maker: Callable[[], Session] -) -> SchemaValidationStatus | None: - """Check if the schema is valid. - - This checks that the schema is the current version as well as for some common schema - errors caused by manual migration between database engines, for example importing an - SQLite database to MariaDB. - """ - current_version = get_schema_version(session_maker) - if current_version is None: - return None - - return SchemaValidationStatus(current_version) - - -def live_migration(schema_status: SchemaValidationStatus) -> bool: +def live_migration(current_version: int) -> bool: """Check if live migration is possible.""" - return schema_status.current_version >= LIVE_MIGRATION_MIN_SCHEMA_VERSION + return current_version >= LIVE_MIGRATION_MIN_SCHEMA_VERSION def migrate_schema( @@ -128,14 +95,13 @@ def migrate_schema( hass: HomeAssistant, engine: Engine, session_maker: Callable[[], Session], - schema_status: SchemaValidationStatus, + current_version: int, ) -> None: """Check if the schema needs to be upgraded.""" - current_version = schema_status.current_version _LOGGER.warning("Database is about to upgrade. Schema version: %s", current_version) db_ready = False for version in range(current_version, SCHEMA_VERSION): - if live_migration(SchemaValidationStatus(version)) and not db_ready: + if live_migration(version) and not db_ready: db_ready = True instance.migration_is_live = True hass.add_job(instance.async_set_db_ready) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 45268ae819b..9e0609de5b6 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -134,16 +134,14 @@ async def test_database_migration_encounters_corruption(hass): sqlite3_exception.__cause__ = sqlite3.DatabaseError() with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( - "homeassistant.components.recorder.migration._schema_is_current", - side_effect=[False], + "homeassistant.components.recorder.migration.schema_is_current", + side_effect=[False, True], ), patch( "homeassistant.components.recorder.migration.migrate_schema", side_effect=sqlite3_exception, ), patch( "homeassistant.components.recorder.core.move_away_broken_database" - ) as move_away, patch( - "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", - ): + ) as move_away: recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} @@ -161,8 +159,8 @@ async def test_database_migration_encounters_corruption_not_sqlite(hass): assert recorder.util.async_migration_in_progress(hass) is False with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( - "homeassistant.components.recorder.migration._schema_is_current", - side_effect=[False], + "homeassistant.components.recorder.migration.schema_is_current", + side_effect=[False, True], ), patch( "homeassistant.components.recorder.migration.migrate_schema", side_effect=DatabaseError("statement", {}, []), From 30920c3da7a714c5fef85fba4bd84191a9e5f44d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Oct 2022 15:52:09 +0200 Subject: [PATCH 402/985] Code quality improvements for Fully Kiosk (#80168) --- .../components/fully_kiosk/manifest.json | 1 - .../components/fully_kiosk/strings.json | 1 - .../components/fully_kiosk/switch.py | 6 +-- .../fully_kiosk/translations/en.json | 1 - tests/components/fully_kiosk/test_button.py | 42 +++++++++++++------ 5 files changed, 33 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/fully_kiosk/manifest.json b/homeassistant/components/fully_kiosk/manifest.json index 8918ce28062..5601cb074f0 100644 --- a/homeassistant/components/fully_kiosk/manifest.json +++ b/homeassistant/components/fully_kiosk/manifest.json @@ -4,7 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fullykiosk", "requirements": ["python-fullykiosk==0.0.11"], - "dependencies": [], "codeowners": ["@cgarwood"], "iot_class": "local_polling", "dhcp": [{ "registered_devices": true }] diff --git a/homeassistant/components/fully_kiosk/strings.json b/homeassistant/components/fully_kiosk/strings.json index 05b9e067962..873ebc661fb 100644 --- a/homeassistant/components/fully_kiosk/strings.json +++ b/homeassistant/components/fully_kiosk/strings.json @@ -10,7 +10,6 @@ }, "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": { diff --git a/homeassistant/components/fully_kiosk/switch.py b/homeassistant/components/fully_kiosk/switch.py index 581700c87d6..28407a66da1 100644 --- a/homeassistant/components/fully_kiosk/switch.py +++ b/homeassistant/components/fully_kiosk/switch.py @@ -109,9 +109,9 @@ class FullySwitchEntity(FullyKioskEntity, SwitchEntity): @property def is_on(self) -> bool | None: """Return true if the entity is on.""" - if self.entity_description.is_on_fn(self.coordinator.data) is not None: - return bool(self.entity_description.is_on_fn(self.coordinator.data)) - return None + if (is_on := self.entity_description.is_on_fn(self.coordinator.data)) is None: + return None + return bool(is_on) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" diff --git a/homeassistant/components/fully_kiosk/translations/en.json b/homeassistant/components/fully_kiosk/translations/en.json index 338c50514fb..24823d68a60 100644 --- a/homeassistant/components/fully_kiosk/translations/en.json +++ b/homeassistant/components/fully_kiosk/translations/en.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, "step": { diff --git a/tests/components/fully_kiosk/test_button.py b/tests/components/fully_kiosk/test_button.py index 8616d7107f7..1c839e57dfd 100644 --- a/tests/components/fully_kiosk/test_button.py +++ b/tests/components/fully_kiosk/test_button.py @@ -22,31 +22,56 @@ async def test_buttons( entry = entity_registry.async_get("button.amazon_fire_restart_browser") assert entry assert entry.unique_id == "abcdef-123456-restartApp" - await call_service(hass, "press", "button.amazon_fire_restart_browser") + assert await hass.services.async_call( + button.DOMAIN, + button.SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.amazon_fire_restart_browser"}, + blocking=True, + ) assert len(mock_fully_kiosk.restartApp.mock_calls) == 1 entry = entity_registry.async_get("button.amazon_fire_reboot_device") assert entry assert entry.unique_id == "abcdef-123456-rebootDevice" - await call_service(hass, "press", "button.amazon_fire_reboot_device") + assert await hass.services.async_call( + button.DOMAIN, + button.SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.amazon_fire_reboot_device"}, + blocking=True, + ) assert len(mock_fully_kiosk.rebootDevice.mock_calls) == 1 entry = entity_registry.async_get("button.amazon_fire_bring_to_foreground") assert entry assert entry.unique_id == "abcdef-123456-toForeground" - await call_service(hass, "press", "button.amazon_fire_bring_to_foreground") + assert await hass.services.async_call( + button.DOMAIN, + button.SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.amazon_fire_bring_to_foreground"}, + blocking=True, + ) assert len(mock_fully_kiosk.toForeground.mock_calls) == 1 entry = entity_registry.async_get("button.amazon_fire_send_to_background") assert entry assert entry.unique_id == "abcdef-123456-toBackground" - await call_service(hass, "press", "button.amazon_fire_send_to_background") + assert await hass.services.async_call( + button.DOMAIN, + button.SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.amazon_fire_send_to_background"}, + blocking=True, + ) assert len(mock_fully_kiosk.toBackground.mock_calls) == 1 entry = entity_registry.async_get("button.amazon_fire_load_start_url") assert entry assert entry.unique_id == "abcdef-123456-loadStartUrl" - await call_service(hass, "press", "button.amazon_fire_load_start_url") + assert await hass.services.async_call( + button.DOMAIN, + button.SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.amazon_fire_load_start_url"}, + blocking=True, + ) assert len(mock_fully_kiosk.loadStartUrl.mock_calls) == 1 assert entry.device_id @@ -60,10 +85,3 @@ async def test_buttons( assert device_entry.model == "KFDOWI" assert device_entry.name == "Amazon Fire" assert device_entry.sw_version == "1.42.5" - - -def call_service(hass, service, entity_id): - """Call any service on entity.""" - return hass.services.async_call( - button.DOMAIN, service, {ATTR_ENTITY_ID: entity_id}, blocking=True - ) From 1c86a12233861a5e7b8f1cd7a71c34254e44bfef Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 12 Oct 2022 16:43:55 +0200 Subject: [PATCH 403/985] Correct units for sensors in nibe heatpump (#80140) --- homeassistant/components/nibe_heatpump/sensor.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nibe_heatpump/sensor.py b/homeassistant/components/nibe_heatpump/sensor.py index b0bc816dad6..66c66aaabe1 100644 --- a/homeassistant/components/nibe_heatpump/sensor.py +++ b/homeassistant/components/nibe_heatpump/sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, + POWER_WATT, TEMP_CELSIUS, TEMP_FAHRENHEIT, TIME_HOURS, @@ -72,24 +73,31 @@ UNIT_DESCRIPTIONS = { state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_MILLIVOLT, ), + "W": SensorEntityDescription( + key="W", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + ), "Wh": SensorEntityDescription( key="Wh", entity_category=EntityCategory.DIAGNOSTIC, - device_class=SensorDeviceClass.POWER, + device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_WATT_HOUR, ), "kWh": SensorEntityDescription( key="kWh", entity_category=EntityCategory.DIAGNOSTIC, - device_class=SensorDeviceClass.POWER, + device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), "MWh": SensorEntityDescription( key="MWh", entity_category=EntityCategory.DIAGNOSTIC, - device_class=SensorDeviceClass.POWER, + device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_MEGA_WATT_HOUR, ), @@ -97,6 +105,7 @@ UNIT_DESCRIPTIONS = { key="h", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=TIME_HOURS, ), } @@ -133,6 +142,7 @@ class Sensor(CoilEntity, SensorEntity): self.entity_description = entity_description else: self._attr_native_unit_of_measurement = coil.unit + self._attr_entity_category = EntityCategory.DIAGNOSTIC def _async_read_coil(self, coil: Coil): self._attr_native_value = coil.value From 54587e96d4fa3664b23e14c04c9584feafba6153 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Oct 2022 17:03:36 +0200 Subject: [PATCH 404/985] Drop unused unit_system from bmw (#80176) --- homeassistant/components/bmw_connected_drive/sensor.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 7de0f40e86b..c797de99859 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -19,7 +19,6 @@ from homeassistant.const import LENGTH, PERCENTAGE, VOLUME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.util.unit_system import UnitSystem from . import BMWBaseEntity from .const import DOMAIN, UNIT_MAP @@ -137,7 +136,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the MyBMW sensors from config entry.""" - unit_system = hass.config.units coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] entities: list[BMWSensor] = [] @@ -145,7 +143,7 @@ async def async_setup_entry( for vehicle in coordinator.account.vehicles: entities.extend( [ - BMWSensor(coordinator, vehicle, description, unit_system) + BMWSensor(coordinator, vehicle, description) for attribute_name in vehicle.available_attributes if (description := SENSOR_TYPES.get(attribute_name)) ] @@ -164,7 +162,6 @@ class BMWSensor(BMWBaseEntity, SensorEntity): coordinator: BMWDataUpdateCoordinator, vehicle: MyBMWVehicle, description: BMWSensorEntityDescription, - unit_system: UnitSystem, ) -> None: """Initialize BMW vehicle sensor.""" super().__init__(coordinator, vehicle) From 690556a5f14653facf73b70ea6a8bad6d9a71db9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Oct 2022 17:17:28 +0200 Subject: [PATCH 405/985] CI: Do not trigger full suite for alert integration (#80174) --- .core_files.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.core_files.yaml b/.core_files.yaml index df69df45cb6..4082c016d8f 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -46,7 +46,6 @@ base_platforms: &base_platforms # Extra components that trigger the full suite components: &components - - homeassistant/components/alert/** - homeassistant/components/alexa/** - homeassistant/components/application_credentials/** - homeassistant/components/auth/** From ad6c3d1cde5406cfdb4d333a50c5bd76a904251f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Oct 2022 17:17:48 +0200 Subject: [PATCH 406/985] Move alert constants into const module (#80170) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/alert/__init__.py | 44 ++++++------- homeassistant/components/alert/const.py | 19 ++++++ .../components/alert/reproduce_state.py | 9 +-- tests/components/alert/test_init.py | 65 +++++++++++-------- 4 files changed, 80 insertions(+), 57 deletions(-) create mode 100644 homeassistant/components/alert/const.py diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 20f3eaf30ca..1c9a98d5b34 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Callable from datetime import timedelta -import logging from typing import Any, final import voluptuous as vol @@ -39,20 +38,19 @@ from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType from homeassistant.util.dt import now -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "alert" - -CONF_CAN_ACK = "can_acknowledge" -CONF_NOTIFIERS = "notifiers" -CONF_SKIP_FIRST = "skip_first" -CONF_ALERT_MESSAGE = "message" -CONF_DONE_MESSAGE = "done_message" -CONF_TITLE = "title" -CONF_DATA = "data" - -DEFAULT_CAN_ACK = True -DEFAULT_SKIP_FIRST = False +from .const import ( + CONF_ALERT_MESSAGE, + CONF_CAN_ACK, + CONF_DATA, + CONF_DONE_MESSAGE, + CONF_NOTIFIERS, + CONF_SKIP_FIRST, + CONF_TITLE, + DEFAULT_CAN_ACK, + DEFAULT_SKIP_FIRST, + DOMAIN, + LOGGER, +) ALERT_SCHEMA = vol.Schema( { @@ -242,7 +240,7 @@ class Alert(ToggleEntity): """Determine if the alert should start or stop.""" if (to_state := event.data.get("new_state")) is None: return - _LOGGER.debug("Watched entity (%s) has changed", event.data.get("entity_id")) + LOGGER.debug("Watched entity (%s) has changed", event.data.get("entity_id")) if to_state.state == self._alert_state and not self._firing: await self.begin_alerting() if to_state.state != self._alert_state and self._firing: @@ -250,7 +248,7 @@ class Alert(ToggleEntity): async def begin_alerting(self) -> None: """Begin the alert procedures.""" - _LOGGER.debug("Beginning Alert: %s", self._attr_name) + LOGGER.debug("Beginning Alert: %s", self._attr_name) self._ack = False self._firing = True self._next_delay = 0 @@ -264,7 +262,7 @@ class Alert(ToggleEntity): async def end_alerting(self) -> None: """End the alert procedures.""" - _LOGGER.debug("Ending Alert: %s", self._attr_name) + LOGGER.debug("Ending Alert: %s", self._attr_name) if self._cancel is not None: self._cancel() self._cancel = None @@ -288,7 +286,7 @@ class Alert(ToggleEntity): return if not self._ack: - _LOGGER.info("Alerting: %s", self._attr_name) + LOGGER.info("Alerting: %s", self._attr_name) self._send_done_message = True if self._message_template is not None: @@ -301,7 +299,7 @@ class Alert(ToggleEntity): async def _notify_done_message(self) -> None: """Send notification of complete alert.""" - _LOGGER.info("Alerting: %s", self._done_message_template) + LOGGER.info("Alerting: %s", self._done_message_template) self._send_done_message = False if self._done_message_template is None: @@ -321,7 +319,7 @@ class Alert(ToggleEntity): if self._data: msg_payload[ATTR_DATA] = self._data - _LOGGER.debug(msg_payload) + LOGGER.debug(msg_payload) for target in self._notifiers: await self.hass.services.async_call( @@ -330,13 +328,13 @@ class Alert(ToggleEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Async Unacknowledge alert.""" - _LOGGER.debug("Reset Alert: %s", self._attr_name) + LOGGER.debug("Reset Alert: %s", self._attr_name) self._ack = False self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Async Acknowledge alert.""" - _LOGGER.debug("Acknowledged Alert: %s", self._attr_name) + LOGGER.debug("Acknowledged Alert: %s", self._attr_name) self._ack = True self.async_write_ha_state() diff --git a/homeassistant/components/alert/const.py b/homeassistant/components/alert/const.py new file mode 100644 index 00000000000..e8afd5ab452 --- /dev/null +++ b/homeassistant/components/alert/const.py @@ -0,0 +1,19 @@ +"""Constants for the Alert integration.""" + +import logging +from typing import Final + +DOMAIN: Final = "alert" + +LOGGER = logging.getLogger(__package__) + +CONF_CAN_ACK = "can_acknowledge" +CONF_NOTIFIERS = "notifiers" +CONF_SKIP_FIRST = "skip_first" +CONF_ALERT_MESSAGE = "message" +CONF_DONE_MESSAGE = "done_message" +CONF_TITLE = "title" +CONF_DATA = "data" + +DEFAULT_CAN_ACK = True +DEFAULT_SKIP_FIRST = False diff --git a/homeassistant/components/alert/reproduce_state.py b/homeassistant/components/alert/reproduce_state.py index 49658ab2495..1e813768b3a 100644 --- a/homeassistant/components/alert/reproduce_state.py +++ b/homeassistant/components/alert/reproduce_state.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from collections.abc import Iterable -import logging from typing import Any from homeassistant.const import ( @@ -15,9 +14,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, HomeAssistant, State -from . import DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN, LOGGER VALID_STATES = {STATE_ON, STATE_OFF} @@ -31,11 +28,11 @@ async def _async_reproduce_state( ) -> None: """Reproduce a single state.""" if (cur_state := hass.states.get(state.entity_id)) is None: - _LOGGER.warning("Unable to find entity %s", state.entity_id) + LOGGER.warning("Unable to find entity %s", state.entity_id) return if state.state not in VALID_STATES: - _LOGGER.warning( + LOGGER.warning( "Invalid state specified for %s: %s", state.entity_id, state.state ) return diff --git a/tests/components/alert/test_init.py b/tests/components/alert/test_init.py index ef21b463a12..8f6d3e41343 100644 --- a/tests/components/alert/test_init.py +++ b/tests/components/alert/test_init.py @@ -5,12 +5,21 @@ from copy import deepcopy import pytest import homeassistant.components.alert as alert -from homeassistant.components.alert import DOMAIN +from homeassistant.components.alert.const import ( + CONF_ALERT_MESSAGE, + CONF_DATA, + CONF_DONE_MESSAGE, + CONF_NOTIFIERS, + CONF_SKIP_FIRST, + CONF_TITLE, + DOMAIN, +) import homeassistant.components.notify as notify from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITY_ID, CONF_NAME, + CONF_REPEAT, CONF_STATE, SERVICE_TOGGLE, SERVICE_TURN_OFF, @@ -31,17 +40,17 @@ TITLE = "{{ states.sensor.test.entity_id }}" TEST_TITLE = "sensor.test" TEST_DATA = {"data": {"inline_keyboard": ["Close garage:/close_garage"]}} TEST_CONFIG = { - alert.DOMAIN: { + DOMAIN: { NAME: { CONF_NAME: NAME, - alert.CONF_DONE_MESSAGE: DONE_MESSAGE, + CONF_DONE_MESSAGE: DONE_MESSAGE, CONF_ENTITY_ID: TEST_ENTITY, CONF_STATE: STATE_ON, - alert.CONF_REPEAT: 30, - alert.CONF_SKIP_FIRST: False, - alert.CONF_NOTIFIERS: [NOTIFIER], - alert.CONF_TITLE: TITLE, - alert.CONF_DATA: {}, + CONF_REPEAT: 30, + CONF_SKIP_FIRST: False, + CONF_NOTIFIERS: [NOTIFIER], + CONF_TITLE: TITLE, + CONF_DATA: {}, } } } @@ -59,7 +68,7 @@ TEST_NOACK = [ None, None, ] -ENTITY_ID = f"{alert.DOMAIN}.{NAME}" +ENTITY_ID = f"{DOMAIN}.{NAME}" @callback @@ -119,13 +128,13 @@ async def test_is_on(hass): async def test_setup(hass): """Test setup method.""" - assert await async_setup_component(hass, alert.DOMAIN, TEST_CONFIG) + assert await async_setup_component(hass, DOMAIN, TEST_CONFIG) assert hass.states.get(ENTITY_ID).state == STATE_IDLE async def test_fire(hass, mock_notifier): """Test the alert firing.""" - assert await async_setup_component(hass, alert.DOMAIN, TEST_CONFIG) + assert await async_setup_component(hass, DOMAIN, TEST_CONFIG) hass.states.async_set("sensor.test", STATE_ON) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ON @@ -133,7 +142,7 @@ async def test_fire(hass, mock_notifier): async def test_silence(hass, mock_notifier): """Test silencing the alert.""" - assert await async_setup_component(hass, alert.DOMAIN, TEST_CONFIG) + assert await async_setup_component(hass, DOMAIN, TEST_CONFIG) hass.states.async_set("sensor.test", STATE_ON) await hass.async_block_till_done() async_turn_off(hass, ENTITY_ID) @@ -151,7 +160,7 @@ async def test_silence(hass, mock_notifier): async def test_reset(hass, mock_notifier): """Test resetting the alert.""" - assert await async_setup_component(hass, alert.DOMAIN, TEST_CONFIG) + assert await async_setup_component(hass, DOMAIN, TEST_CONFIG) hass.states.async_set("sensor.test", STATE_ON) await hass.async_block_till_done() async_turn_off(hass, ENTITY_ID) @@ -164,7 +173,7 @@ async def test_reset(hass, mock_notifier): async def test_toggle(hass, mock_notifier): """Test toggling alert.""" - assert await async_setup_component(hass, alert.DOMAIN, TEST_CONFIG) + assert await async_setup_component(hass, DOMAIN, TEST_CONFIG) hass.states.async_set("sensor.test", STATE_ON) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ON @@ -180,7 +189,7 @@ async def test_notification_no_done_message(hass): """Test notifications.""" events = [] config = deepcopy(TEST_CONFIG) - del config[alert.DOMAIN][NAME][alert.CONF_DONE_MESSAGE] + del config[DOMAIN][NAME][CONF_DONE_MESSAGE] @callback def record_event(event): @@ -189,7 +198,7 @@ async def test_notification_no_done_message(hass): hass.services.async_register(notify.DOMAIN, NOTIFIER, record_event) - assert await async_setup_component(hass, alert.DOMAIN, config) + assert await async_setup_component(hass, DOMAIN, config) assert len(events) == 0 hass.states.async_set("sensor.test", STATE_ON) @@ -212,7 +221,7 @@ async def test_notification(hass): hass.services.async_register(notify.DOMAIN, NOTIFIER, record_event) - assert await async_setup_component(hass, alert.DOMAIN, TEST_CONFIG) + assert await async_setup_component(hass, DOMAIN, TEST_CONFIG) assert len(events) == 0 hass.states.async_set("sensor.test", STATE_ON) @@ -226,7 +235,7 @@ async def test_notification(hass): async def test_sending_non_templated_notification(hass, mock_notifier): """Test notifications.""" - assert await async_setup_component(hass, alert.DOMAIN, TEST_CONFIG) + assert await async_setup_component(hass, DOMAIN, TEST_CONFIG) hass.states.async_set(TEST_ENTITY, STATE_ON) await hass.async_block_till_done() @@ -238,8 +247,8 @@ async def test_sending_non_templated_notification(hass, mock_notifier): async def test_sending_templated_notification(hass, mock_notifier): """Test templated notification.""" config = deepcopy(TEST_CONFIG) - config[alert.DOMAIN][NAME][alert.CONF_ALERT_MESSAGE] = TEMPLATE - assert await async_setup_component(hass, alert.DOMAIN, config) + config[DOMAIN][NAME][CONF_ALERT_MESSAGE] = TEMPLATE + assert await async_setup_component(hass, DOMAIN, config) hass.states.async_set(TEST_ENTITY, STATE_ON) await hass.async_block_till_done() @@ -251,8 +260,8 @@ async def test_sending_templated_notification(hass, mock_notifier): async def test_sending_templated_done_notification(hass, mock_notifier): """Test templated notification.""" config = deepcopy(TEST_CONFIG) - config[alert.DOMAIN][NAME][alert.CONF_DONE_MESSAGE] = TEMPLATE - assert await async_setup_component(hass, alert.DOMAIN, config) + config[DOMAIN][NAME][CONF_DONE_MESSAGE] = TEMPLATE + assert await async_setup_component(hass, DOMAIN, config) hass.states.async_set(TEST_ENTITY, STATE_ON) await hass.async_block_till_done() @@ -266,8 +275,8 @@ async def test_sending_templated_done_notification(hass, mock_notifier): async def test_sending_titled_notification(hass, mock_notifier): """Test notifications.""" config = deepcopy(TEST_CONFIG) - config[alert.DOMAIN][NAME][alert.CONF_TITLE] = TITLE - assert await async_setup_component(hass, alert.DOMAIN, config) + config[DOMAIN][NAME][CONF_TITLE] = TITLE + assert await async_setup_component(hass, DOMAIN, config) hass.states.async_set(TEST_ENTITY, STATE_ON) await hass.async_block_till_done() @@ -279,8 +288,8 @@ async def test_sending_titled_notification(hass, mock_notifier): async def test_sending_data_notification(hass, mock_notifier): """Test notifications.""" config = deepcopy(TEST_CONFIG) - config[alert.DOMAIN][NAME][alert.CONF_DATA] = TEST_DATA - assert await async_setup_component(hass, alert.DOMAIN, config) + config[DOMAIN][NAME][CONF_DATA] = TEST_DATA + assert await async_setup_component(hass, DOMAIN, config) hass.states.async_set(TEST_ENTITY, STATE_ON) await hass.async_block_till_done() @@ -292,7 +301,7 @@ async def test_sending_data_notification(hass, mock_notifier): async def test_skipfirst(hass): """Test skipping first notification.""" config = deepcopy(TEST_CONFIG) - config[alert.DOMAIN][NAME][alert.CONF_SKIP_FIRST] = True + config[DOMAIN][NAME][CONF_SKIP_FIRST] = True events = [] @callback @@ -302,7 +311,7 @@ async def test_skipfirst(hass): hass.services.async_register(notify.DOMAIN, NOTIFIER, record_event) - assert await async_setup_component(hass, alert.DOMAIN, config) + assert await async_setup_component(hass, DOMAIN, config) assert len(events) == 0 hass.states.async_set("sensor.test", STATE_ON) From 37a5a09910af223fd93e1f8d88bb653fb261b6fe Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Oct 2022 20:10:03 +0200 Subject: [PATCH 407/985] Remove unused is_on helper function from Alert (#80190) --- homeassistant/components/alert/__init__.py | 5 ----- tests/components/alert/test_init.py | 10 ---------- 2 files changed, 15 deletions(-) diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 1c9a98d5b34..737c17d6cef 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -80,11 +80,6 @@ CONFIG_SCHEMA = vol.Schema( ALERT_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids}) -def is_on(hass: HomeAssistant, entity_id: str) -> bool: - """Return if the alert is firing and not acknowledged.""" - return hass.states.is_state(entity_id, STATE_ON) - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Alert component.""" entities: list[Alert] = [] diff --git a/tests/components/alert/test_init.py b/tests/components/alert/test_init.py index 8f6d3e41343..e4f90a1a989 100644 --- a/tests/components/alert/test_init.py +++ b/tests/components/alert/test_init.py @@ -116,16 +116,6 @@ def mock_notifier(hass): return events -async def test_is_on(hass): - """Test is_on method.""" - hass.states.async_set(ENTITY_ID, STATE_ON) - await hass.async_block_till_done() - assert alert.is_on(hass, ENTITY_ID) - hass.states.async_set(ENTITY_ID, STATE_OFF) - await hass.async_block_till_done() - assert not alert.is_on(hass, ENTITY_ID) - - async def test_setup(hass): """Test setup method.""" assert await async_setup_component(hass, DOMAIN, TEST_CONFIG) From c6340856e9b8da129aa03149d31544976eadda90 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Oct 2022 20:10:38 +0200 Subject: [PATCH 408/985] Fix schema for the Alert integration (#80189) Schema fixes for the Alert integration --- homeassistant/components/alert/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 737c17d6cef..4c67dff7368 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -56,15 +56,15 @@ ALERT_SCHEMA = vol.Schema( { vol.Required(CONF_NAME): cv.string, vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_STATE, default=STATE_ON): cv.string, + vol.Optional(CONF_STATE, default=STATE_ON): cv.string, vol.Required(CONF_REPEAT): vol.All( cv.ensure_list, [vol.Coerce(float)], # Minimum delay is 1 second = 0.016 minutes [vol.Range(min=0.016)], ), - vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean, - vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean, + vol.Optional(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean, + vol.Optional(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean, vol.Optional(CONF_ALERT_MESSAGE): cv.template, vol.Optional(CONF_DONE_MESSAGE): cv.template, vol.Optional(CONF_TITLE): cv.template, From fc32071562de406c32e75410cd87920f82153856 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Oct 2022 20:13:05 +0200 Subject: [PATCH 409/985] Remove ToggleEntity inheritance from Alert (#80185) --- homeassistant/components/alert/__init__.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 4c67dff7368..53fb89b19e6 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from datetime import timedelta -from typing import Any, final +from typing import Any import voluptuous as vol @@ -29,7 +29,7 @@ from homeassistant.const import ( from homeassistant.core import Event, HomeAssistant, ServiceCall from homeassistant.helpers import service import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import ( async_track_point_in_time, async_track_state_change_event, @@ -164,7 +164,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -class Alert(ToggleEntity): +class Alert(Entity): """Representation of an alert.""" _attr_should_poll = False @@ -220,10 +220,8 @@ class Alert(ToggleEntity): hass, [watched_entity_id], self.watched_entity_change ) - @final # type: ignore[misc] @property - # pylint: disable=overridden-final-method - def state(self) -> str: # type: ignore[override] + def state(self) -> str: """Return the alert status.""" if self._firing: if self._ack: From 0daa5b55b5a97d2c9be72e2f17b67d33f12ecb7d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 12 Oct 2022 21:02:25 +0200 Subject: [PATCH 410/985] Add missing type for CoordinatorEntity in Brother sensor platform (#80197) Add missing type for CoordinatorEntity --- homeassistant/components/brother/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 2b82ac0cdb8..adb17d4283d 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -395,7 +395,9 @@ async def async_setup_entry( async_add_entities(sensors, False) -class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): +class BrotherPrinterSensor( + CoordinatorEntity[BrotherDataUpdateCoordinator], SensorEntity +): """Define an Brother Printer sensor.""" _attr_has_entity_name = True From 503434e538af4b708f01cee9ca20bfa8426cec94 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Oct 2022 21:33:38 +0200 Subject: [PATCH 411/985] Use DistanceConverter in components (#80182) * Use DistanceConverter in components * Adjust for METRIC_SYSTEM --- homeassistant/components/gdacs/__init__.py | 5 +++-- homeassistant/components/geonetnz_quakes/__init__.py | 5 +++-- homeassistant/components/geonetnz_volcano/__init__.py | 5 +++-- homeassistant/components/geonetnz_volcano/sensor.py | 7 +++++-- homeassistant/components/here_travel_time/__init__.py | 7 +++++-- homeassistant/components/nissan_leaf/sensor.py | 4 ++-- homeassistant/components/waze_travel_time/sensor.py | 7 +++++-- 7 files changed, 26 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/gdacs/__init__.py b/homeassistant/components/gdacs/__init__.py index 56f17adc992..d4fe4177aed 100644 --- a/homeassistant/components/gdacs/__init__.py +++ b/homeassistant/components/gdacs/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_RADIUS, CONF_SCAN_INTERVAL, CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_KILOMETERS, LENGTH_MILES, ) from homeassistant.core import HomeAssistant, callback @@ -19,7 +20,7 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType -from homeassistant.util.unit_system import METRIC_SYSTEM +from homeassistant.util.unit_conversion import DistanceConverter from .const import ( CONF_CATEGORIES, @@ -88,7 +89,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b radius = config_entry.data[CONF_RADIUS] if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - radius = METRIC_SYSTEM.length(radius, LENGTH_MILES) + radius = DistanceConverter.convert(radius, LENGTH_MILES, LENGTH_KILOMETERS) # Create feed entity manager for all platforms. manager = GdacsFeedEntityManager(hass, config_entry, radius) feeds[config_entry.entry_id] = manager diff --git a/homeassistant/components/geonetnz_quakes/__init__.py b/homeassistant/components/geonetnz_quakes/__init__.py index 6c091e71f05..e4d766fa979 100644 --- a/homeassistant/components/geonetnz_quakes/__init__.py +++ b/homeassistant/components/geonetnz_quakes/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_RADIUS, CONF_SCAN_INTERVAL, CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_KILOMETERS, LENGTH_MILES, ) from homeassistant.core import HomeAssistant, callback @@ -19,7 +20,7 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType -from homeassistant.util.unit_system import METRIC_SYSTEM +from homeassistant.util.unit_conversion import DistanceConverter from .const import ( CONF_MINIMUM_MAGNITUDE, @@ -95,7 +96,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b radius = config_entry.data[CONF_RADIUS] if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - radius = METRIC_SYSTEM.length(radius, LENGTH_MILES) + radius = DistanceConverter.convert(radius, LENGTH_MILES, LENGTH_KILOMETERS) # Create feed entity manager for all platforms. manager = GeonetnzQuakesFeedEntityManager(hass, config_entry, radius) feeds[config_entry.entry_id] = manager diff --git a/homeassistant/components/geonetnz_volcano/__init__.py b/homeassistant/components/geonetnz_volcano/__init__.py index a1b6368c8ef..e4bf2d2cb8c 100644 --- a/homeassistant/components/geonetnz_volcano/__init__.py +++ b/homeassistant/components/geonetnz_volcano/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_KILOMETERS, LENGTH_MILES, ) from homeassistant.core import HomeAssistant, callback @@ -22,7 +23,7 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType -from homeassistant.util.unit_system import METRIC_SYSTEM +from homeassistant.util.unit_conversion import DistanceConverter from .config_flow import configured_instances from .const import DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN, FEED, PLATFORMS @@ -85,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b radius = config_entry.data[CONF_RADIUS] unit_system = config_entry.data[CONF_UNIT_SYSTEM] if unit_system == CONF_UNIT_SYSTEM_IMPERIAL: - radius = METRIC_SYSTEM.length(radius, LENGTH_MILES) + radius = DistanceConverter.convert(radius, LENGTH_MILES, LENGTH_KILOMETERS) # Create feed entity manager for all platforms. manager = GeonetnzVolcanoFeedEntityManager(hass, config_entry, radius, unit_system) hass.data[DOMAIN][FEED][config_entry.entry_id] = manager diff --git a/homeassistant/components/geonetnz_volcano/sensor.py b/homeassistant/components/geonetnz_volcano/sensor.py index add35bfbcd7..51bca3e467a 100644 --- a/homeassistant/components/geonetnz_volcano/sensor.py +++ b/homeassistant/components/geonetnz_volcano/sensor.py @@ -11,12 +11,13 @@ from homeassistant.const import ( ATTR_LONGITUDE, CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, + LENGTH_MILES, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt -from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from homeassistant.util.unit_conversion import DistanceConverter from .const import ( ATTR_ACTIVITY, @@ -114,7 +115,9 @@ class GeonetnzVolcanoSensor(SensorEntity): # Convert distance if not metric system. if self._unit_system == CONF_UNIT_SYSTEM_IMPERIAL: self._distance = round( - IMPERIAL_SYSTEM.length(feed_entry.distance_to_home, LENGTH_KILOMETERS), + DistanceConverter.convert( + feed_entry.distance_to_home, LENGTH_KILOMETERS, LENGTH_MILES + ), 1, ) else: diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index 8f63060b683..ddfdc34ff69 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_METERS, + LENGTH_MILES, Platform, ) from homeassistant.core import HomeAssistant @@ -23,7 +24,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.location import find_coordinates from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt -from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from homeassistant.util.unit_conversion import DistanceConverter from .const import ( ATTR_DESTINATION, @@ -179,7 +180,9 @@ class HereTravelTimeDataUpdateCoordinator(DataUpdateCoordinator): traffic_time = summary["trafficTime"] if self.config.units == CONF_UNIT_SYSTEM_IMPERIAL: # Convert to miles. - distance = IMPERIAL_SYSTEM.length(distance, LENGTH_METERS) + distance = DistanceConverter.convert( + distance, LENGTH_METERS, LENGTH_MILES + ) else: # Convert to kilometers distance = distance / 1000 diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py index 64847e3fa5c..0e7cc0c00cd 100644 --- a/homeassistant/components/nissan_leaf/sensor.py +++ b/homeassistant/components/nissan_leaf/sensor.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM +from homeassistant.util.unit_conversion import DistanceConverter from . import LeafEntity from .const import ( @@ -123,7 +123,7 @@ class LeafRangeSensor(LeafEntity, SensorEntity): return None if not self.car.hass.config.units.is_metric or self.car.force_miles: - ret = IMPERIAL_SYSTEM.length(ret, METRIC_SYSTEM.length_unit) + ret = DistanceConverter.convert(ret, LENGTH_KILOMETERS, LENGTH_MILES) return round(ret) diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 153ada11349..85b6acdc19a 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, EVENT_HOMEASSISTANT_STARTED, LENGTH_KILOMETERS, + LENGTH_MILES, TIME_MINUTES, ) from homeassistant.core import CoreState, HomeAssistant @@ -26,7 +27,7 @@ from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.location import find_coordinates -from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from homeassistant.util.unit_conversion import DistanceConverter from .const import ( CONF_AVOID_FERRIES, @@ -245,7 +246,9 @@ class WazeTravelTimeData: if units == CONF_UNIT_SYSTEM_IMPERIAL: # Convert to miles. - self.distance = IMPERIAL_SYSTEM.length(distance, LENGTH_KILOMETERS) + self.distance = DistanceConverter.convert( + distance, LENGTH_KILOMETERS, LENGTH_MILES + ) else: self.distance = distance From a396e35c21fcf43dbb2471af438b1e24201f5ed6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 12 Oct 2022 21:56:07 +0200 Subject: [PATCH 412/985] Use DistanceConverter in components (#80207) --- homeassistant/components/gdacs/geo_location.py | 6 +++--- homeassistant/components/geonetnz_quakes/geo_location.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py index 715ac779668..7e48cf0aa3a 100644 --- a/homeassistant/components/gdacs/geo_location.py +++ b/homeassistant/components/gdacs/geo_location.py @@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from homeassistant.util.unit_conversion import DistanceConverter from . import GdacsFeedEntityManager from .const import DEFAULT_ICON, DOMAIN, FEED @@ -153,8 +153,8 @@ class GdacsEvent(GeolocationEvent): self._attr_name = f"{feed_entry.event_type}: {event_name}" # Convert distance if not metric system. if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - self._attr_distance = IMPERIAL_SYSTEM.length( - feed_entry.distance_to_home, LENGTH_KILOMETERS + self._attr_distance = DistanceConverter.convert( + feed_entry.distance_to_home, LENGTH_KILOMETERS, LENGTH_MILES ) else: self._attr_distance = feed_entry.distance_to_home diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py index 26ad780d098..c6c872ba828 100644 --- a/homeassistant/components/geonetnz_quakes/geo_location.py +++ b/homeassistant/components/geonetnz_quakes/geo_location.py @@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from homeassistant.util.unit_conversion import DistanceConverter from . import GeonetnzQuakesFeedEntityManager from .const import DOMAIN, FEED @@ -141,8 +141,8 @@ class GeonetnzQuakesEvent(GeolocationEvent): self._attr_name = feed_entry.title # Convert distance if not metric system. if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - self._attr_distance = IMPERIAL_SYSTEM.length( - feed_entry.distance_to_home, LENGTH_KILOMETERS + self._attr_distance = DistanceConverter.convert( + feed_entry.distance_to_home, LENGTH_KILOMETERS, LENGTH_MILES ) else: self._attr_distance = feed_entry.distance_to_home From 82322e3804af9cac55c6bea106f4bb0faff4c298 Mon Sep 17 00:00:00 2001 From: Kevin Addeman Date: Wed, 12 Oct 2022 16:29:28 -0400 Subject: [PATCH 413/985] Add button entities for Lutron Caseta/RA3/HWQSX (#79963) Co-authored-by: J. Nick Koston --- .../components/lutron_caseta/__init__.py | 48 ++++----- .../components/lutron_caseta/button.py | 98 +++++++++++++++++++ .../components/lutron_caseta/models.py | 4 +- .../components/lutron_caseta/strings.json | 3 + .../components/lutron_caseta/switch.py | 16 +++ .../lutron_caseta/translations/en.json | 3 + tests/components/lutron_caseta/__init__.py | 70 ++++++++++++- tests/components/lutron_caseta/test_button.py | 50 ++++++++++ .../lutron_caseta/test_diagnostics.py | 56 ++++++++++- 9 files changed, 323 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/lutron_caseta/button.py create mode 100644 tests/components/lutron_caseta/test_button.py diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 9638f769919..5ef195514d3 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -79,6 +79,7 @@ PLATFORMS = [ Platform.LIGHT, Platform.SCENE, Platform.SWITCH, + Platform.BUTTON, ] @@ -213,14 +214,14 @@ def _async_register_bridge_device( def _async_register_button_devices( hass: HomeAssistant, config_entry_id: str, - bridge, - bridge_device, + bridge: Smartbridge, + bridge_device: dict[str, Any], button_devices_by_id: dict[int, dict], -) -> tuple[dict[str, dict], dict[int, dict[str, Any]]]: +) -> tuple[dict[str, dict], dict[int, DeviceInfo]]: """Register button devices (Pico Remotes) in the device registry.""" device_registry = dr.async_get(hass) button_devices_by_dr_id: dict[str, dict] = {} - device_info_by_device_id: dict[int, dict[str, Any]] = {} + device_info_by_device_id: dict[int, DeviceInfo] = {} seen: set[str] = set() bridge_devices = bridge.get_devices() @@ -241,10 +242,9 @@ def _async_register_button_devices( seen.add(ha_device_serial) area, name = _area_and_name_from_name(ha_device["name"]) - device_args: dict[str, Any] = { + device_args: DeviceInfo = { "name": f"{area} {name}", "manufacturer": MANUFACTURER, - "config_entry_id": config_entry_id, "identifiers": {(DOMAIN, ha_device_serial)}, "model": f"{ha_device['model']} ({ha_device['type']})", "via_device": (DOMAIN, bridge_device["serial"]), @@ -252,7 +252,9 @@ def _async_register_button_devices( if area != UNASSIGNED_AREA: device_args["suggested_area"] = area - dr_device = device_registry.async_get_or_create(**device_args) + dr_device = device_registry.async_get_or_create( + **device_args, config_entry_id=config_entry_id + ) button_devices_by_dr_id[dr_device.id] = ha_device device_info_by_device_id.setdefault(ha_device["device_id"], device_args) @@ -358,7 +360,7 @@ class LutronCasetaDevice(Entity): _attr_should_poll = False - def __init__(self, device, data): + def __init__(self, device: dict[str, Any], data: LutronCasetaData) -> None: """Set up the base class. [:param]device the device metadata @@ -372,22 +374,19 @@ class LutronCasetaDevice(Entity): if "serial" not in self._device: return - if "parent_device" in device and ( - parent_device_info := data.device_info_by_device_id.get( - device["parent_device"] - ) - ): - # Append the child device name to the end of the parent keypad name to create the entity name - self._attr_name = f'{parent_device_info["name"]} {device["device_name"]}' - # Set the device_info to the same as the Parent Keypad - # The entities will be nested inside the keypad device - self._attr_device_info = parent_device_info + if "parent_device" in device: + # This is a child entity, handle the naming in button.py and switch.py return area, name = _area_and_name_from_name(device["name"]) self._attr_name = full_name = f"{area} {name}" info = DeviceInfo( - identifiers={(DOMAIN, self._handle_none_serial(self.serial))}, + # Historically we used the device serial number for the identifier + # but the serial is usually an integer and a string is expected + # here. Since it would be a breaking change to change the identifier + # we are ignoring the type error here until it can be migrated to + # a string in a future release. + identifiers={(DOMAIN, self._handle_none_serial(self.serial))}, # type: ignore[arg-type] manufacturer=MANUFACTURER, model=f"{device['model']} ({device['type']})", name=full_name, @@ -402,7 +401,7 @@ class LutronCasetaDevice(Entity): """Register callbacks.""" self._smartbridge.add_subscriber(self.device_id, self.async_write_ha_state) - def _handle_none_serial(self, serial: str | None) -> str | int: + def _handle_none_serial(self, serial: str | int | None) -> str | int: """Handle None serial returned by RA3 and QSX processors.""" if serial is None: return f"{self._bridge_unique_id}_{self.device_id}" @@ -414,7 +413,7 @@ class LutronCasetaDevice(Entity): return self._device["device_id"] @property - def serial(self): + def serial(self) -> int | None: """Return the serial number of the device.""" return self._device["serial"] @@ -426,7 +425,12 @@ class LutronCasetaDevice(Entity): @property def extra_state_attributes(self): """Return the state attributes.""" - return {"device_id": self.device_id, "zone_id": self._device["zone"]} + attributes = { + "device_id": self.device_id, + } + if zone := self._device.get("zone"): + attributes["zone_id"] = zone + return attributes class LutronCasetaDeviceUpdatableEntity(LutronCasetaDevice): diff --git a/homeassistant/components/lutron_caseta/button.py b/homeassistant/components/lutron_caseta/button.py new file mode 100644 index 00000000000..713c711f675 --- /dev/null +++ b/homeassistant/components/lutron_caseta/button.py @@ -0,0 +1,98 @@ +"""Support for pico and keypad buttons.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import LutronCasetaDevice +from .const import DOMAIN as CASETA_DOMAIN +from .device_trigger import ( + LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP, + _lutron_model_to_device_type, +) +from .models import LutronCasetaData + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Lutron pico and keypad buttons.""" + data: LutronCasetaData = hass.data[CASETA_DOMAIN][config_entry.entry_id] + bridge = data.bridge + button_devices = bridge.get_buttons() + all_devices = data.bridge.get_devices() + device_info_by_device_id = data.device_info_by_device_id + entities: list[LutronCasetaButton] = [] + + for device in button_devices.values(): + + parent_device_info = device_info_by_device_id[device["parent_device"]] + + enabled_default = True + if not (device_name := device.get("device_name")): + # device name (button name) is missing, probably a caseta pico + # try to get the name using the button number from the triggers + # disable the button by default + enabled_default = False + keypad_device = all_devices[device["parent_device"]] + button_numbers = LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP.get( + _lutron_model_to_device_type( + keypad_device["model"], keypad_device["type"] + ), + {}, + ) + device_name = ( + button_numbers.get( + int(device["button_number"]), + f"button {device['button_number']}", + ) + .replace("_", " ") + .title() + ) + + # Append the child device name to the end of the parent keypad name to create the entity name + full_name = f'{parent_device_info.get("name")} {device_name}' + # Set the device_info to the same as the Parent Keypad + # The entities will be nested inside the keypad device + entities.append( + LutronCasetaButton( + device, data, full_name, enabled_default, parent_device_info + ), + ) + + if entities: + async_add_entities(entities) + + +class LutronCasetaButton(LutronCasetaDevice, ButtonEntity): + """Representation of a Lutron pico and keypad button.""" + + def __init__( + self, + device: dict[str, Any], + data: LutronCasetaData, + full_name: str, + enabled_default: bool, + device_info: DeviceInfo, + ) -> None: + """Init a button entity.""" + super().__init__(device, data) + self._attr_entity_registry_enabled_default = enabled_default + self._attr_name = full_name + self._attr_device_info = device_info + + async def async_press(self) -> None: + """Send a button press event.""" + await self._smartbridge.tap_button(self.device_id) + + @property + def serial(self): + """Buttons shouldn't have serial numbers, Return None.""" + return None diff --git a/homeassistant/components/lutron_caseta/models.py b/homeassistant/components/lutron_caseta/models.py index d0e59c25438..e7fb8a2f2b8 100644 --- a/homeassistant/components/lutron_caseta/models.py +++ b/homeassistant/components/lutron_caseta/models.py @@ -6,6 +6,8 @@ from typing import Any from pylutron_caseta.smartbridge import Smartbridge +from homeassistant.helpers.entity import DeviceInfo + @dataclass class LutronCasetaData: @@ -14,4 +16,4 @@ class LutronCasetaData: bridge: Smartbridge bridge_device: dict[str, Any] button_devices: dict[str, dict] - device_info_by_device_id: dict[int, dict[str, Any]] + device_info_by_device_id: dict[int, DeviceInfo] diff --git a/homeassistant/components/lutron_caseta/strings.json b/homeassistant/components/lutron_caseta/strings.json index a89b0c4bbce..0c6ec06005c 100644 --- a/homeassistant/components/lutron_caseta/strings.json +++ b/homeassistant/components/lutron_caseta/strings.json @@ -33,6 +33,9 @@ "button_2": "Second button", "button_3": "Third button", "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button", + "button_7": "Seventh button", "group_1_button_1": "First Group first button", "group_1_button_2": "First Group second button", "group_2_button_1": "Second Group first button", diff --git a/homeassistant/components/lutron_caseta/switch.py b/homeassistant/components/lutron_caseta/switch.py index d87fd4c3bfa..50c01e6a31f 100644 --- a/homeassistant/components/lutron_caseta/switch.py +++ b/homeassistant/components/lutron_caseta/switch.py @@ -33,6 +33,22 @@ async def async_setup_entry( class LutronCasetaLight(LutronCasetaDeviceUpdatableEntity, SwitchEntity): """Representation of a Lutron Caseta switch.""" + def __init__(self, device, data): + """Init a button entity.""" + + super().__init__(device, data) + self._enabled_default = True + + if "parent_device" not in device: + return + + parent_device_info = data.device_info_by_device_id.get(device["parent_device"]) + # Append the child device name to the end of the parent keypad name to create the entity name + self._attr_name = f'{parent_device_info["name"]} {device["device_name"]}' + # Set the device_info to the same as the Parent Keypad + # The entities will be nested inside the keypad device + self._attr_device_info = parent_device_info + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self._smartbridge.turn_on(self.device_id) diff --git a/homeassistant/components/lutron_caseta/translations/en.json b/homeassistant/components/lutron_caseta/translations/en.json index d5245dae2a4..b0ddf459194 100644 --- a/homeassistant/components/lutron_caseta/translations/en.json +++ b/homeassistant/components/lutron_caseta/translations/en.json @@ -33,6 +33,9 @@ "button_2": "Second button", "button_3": "Third button", "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button", + "button_7": "Seventh button", "close_1": "Close 1", "close_2": "Close 2", "close_3": "Close 3", diff --git a/tests/components/lutron_caseta/__init__.py b/tests/components/lutron_caseta/__init__.py index 91ddfe26fb5..f6af22034a7 100644 --- a/tests/components/lutron_caseta/__init__.py +++ b/tests/components/lutron_caseta/__init__.py @@ -105,11 +105,11 @@ class MockBridge: """Initialize MockBridge instance with configured mock connectivity.""" self.can_connect = can_connect self.is_currently_connected = False - self.buttons = {} self.areas = {} self.occupancy_groups = {} self.scenes = self.get_scenes() self.devices = self.load_devices() + self.buttons = self.load_buttons() async def connect(self): """Connect the mock bridge.""" @@ -119,6 +119,9 @@ class MockBridge: def add_subscriber(self, device_id: str, callback_): """Mock a listener to be notified of state changes.""" + def add_button_subscriber(self, button_id: str, callback_): + """Mock a listener for button presses.""" + def is_connected(self): """Return whether the mock bridge is connected.""" return self.is_currently_connected @@ -187,6 +190,64 @@ class MockBridge: "serial": 5442321, "tilt": None, }, + "9": { + "device_id": "9", + "current_state": -1, + "fan_speed": None, + "tilt": None, + "zone": None, + "name": "Dining Room_Pico", + "button_groups": ["4"], + "occupancy_sensors": None, + "type": "Pico3ButtonRaiseLower", + "model": "PJ2-3BRL-GXX-X01", + "serial": 68551522, + "device_name": "Pico", + "area": "6", + }, + "1355": { + "device_id": "1355", + "current_state": -1, + "fan_speed": None, + "zone": None, + "name": "Hallway_Main Stairs Position 1 Keypad", + "button_groups": ["1363"], + "type": "SunnataKeypad", + "model": "RRST-W3RL-XX", + "serial": 66286451, + "control_station_name": "Main Stairs", + "device_name": "Position 1", + "area": "1205", + }, + } + + def load_buttons(self): + """Load mock buttons into self.buttons.""" + return { + "111": { + "device_id": "111", + "current_state": "Release", + "button_number": 0, + "name": "Dining Room_Pico", + "type": "Pico3ButtonRaiseLower", + "model": "PJ2-3BRL-GXX-X01", + "serial": 68551522, + "parent_device": "9", + }, + "1372": { + "device_id": "1372", + "current_state": "Release", + "button_number": 3, + "button_group": "1363", + "name": "Hallway_Main Stairs Position 1 Keypad", + "type": "SunnataKeypad", + "model": "RRST-W3RL-XX", + "serial": 66286451, + "button_name": "Kitchen Pendants", + "button_led": "1362", + "device_name": "Kitchen Pendants", + "parent_device": "1355", + }, } def get_devices(self) -> dict[str, dict]: @@ -228,6 +289,13 @@ class MockBridge: """Return scenes on the bridge.""" return {} + def get_buttons(self): + """Will return all known buttons connected to the bridge/processor.""" + return self.buttons + + def tap_button(self, button_id: str): + """Mock a button press and release message for the given button ID.""" + async def close(self): """Close the mock bridge connection.""" self.is_currently_connected = False diff --git a/tests/components/lutron_caseta/test_button.py b/tests/components/lutron_caseta/test_button.py new file mode 100644 index 00000000000..767d9a59df4 --- /dev/null +++ b/tests/components/lutron_caseta/test_button.py @@ -0,0 +1,50 @@ +"""Tests for the Lutron Caseta integration.""" + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import MockBridge, async_setup_integration + + +async def test_button_unique_id(hass: HomeAssistant) -> None: + """Test a button unique id.""" + await async_setup_integration(hass, MockBridge) + + ra3_button_entity_id = ( + "button.hallway_main_stairs_position_1_keypad_kitchen_pendants" + ) + caseta_button_entity_id = "button.dining_room_pico_on" + + entity_registry = er.async_get(hass) + + # Assert that Caseta buttons will have the bridge serial hash and the zone id as the uniqueID + assert entity_registry.async_get(ra3_button_entity_id).unique_id == "000004d2_1372" + assert ( + entity_registry.async_get(caseta_button_entity_id).unique_id == "000004d2_111" + ) + + +async def test_button_press(hass: HomeAssistant) -> None: + """Test a button press.""" + await async_setup_integration(hass, MockBridge) + + ra3_button_entity_id = ( + "button.hallway_main_stairs_position_1_keypad_kitchen_pendants" + ) + + state = hass.states.get(ra3_button_entity_id) + assert state + assert state.state == STATE_UNKNOWN + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: ra3_button_entity_id}, + blocking=False, + ) + await hass.async_block_till_done() + + state = hass.states.get(ra3_button_entity_id) + assert state diff --git a/tests/components/lutron_caseta/test_diagnostics.py b/tests/components/lutron_caseta/test_diagnostics.py index 42fc1dac5c1..b0d6aae1058 100644 --- a/tests/components/lutron_caseta/test_diagnostics.py +++ b/tests/components/lutron_caseta/test_diagnostics.py @@ -41,7 +41,32 @@ async def test_diagnostics(hass, hass_client) -> None: assert diag == { "data": { "areas": {}, - "buttons": {}, + "buttons": { + "111": { + "device_id": "111", + "current_state": "Release", + "button_number": 0, + "name": "Dining Room_Pico", + "type": "Pico3ButtonRaiseLower", + "model": "PJ2-3BRL-GXX-X01", + "serial": 68551522, + "parent_device": "9", + }, + "1372": { + "device_id": "1372", + "current_state": "Release", + "button_number": 3, + "button_group": "1363", + "name": "Hallway_Main Stairs Position 1 Keypad", + "type": "SunnataKeypad", + "model": "RRST-W3RL-XX", + "serial": 66286451, + "button_name": "Kitchen Pendants", + "button_led": "1362", + "device_name": "Kitchen Pendants", + "parent_device": "1355", + }, + }, "devices": { "1": { "model": "model", @@ -109,6 +134,35 @@ async def test_diagnostics(hass, hass_client) -> None: "serial": 5442321, "tilt": None, }, + "9": { + "device_id": "9", + "current_state": -1, + "fan_speed": None, + "tilt": None, + "zone": None, + "name": "Dining Room_Pico", + "button_groups": ["4"], + "occupancy_sensors": None, + "type": "Pico3ButtonRaiseLower", + "model": "PJ2-3BRL-GXX-X01", + "serial": 68551522, + "device_name": "Pico", + "area": "6", + }, + "1355": { + "device_id": "1355", + "current_state": -1, + "fan_speed": None, + "zone": None, + "name": "Hallway_Main Stairs Position 1 Keypad", + "button_groups": ["1363"], + "type": "SunnataKeypad", + "model": "RRST-W3RL-XX", + "serial": 66286451, + "control_station_name": "Main Stairs", + "device_name": "Position 1", + "area": "1205", + }, }, "occupancy_groups": {}, "scenes": {}, From f5868f00a0a9857bf8a26ae96e0f7b357570c909 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Thu, 13 Oct 2022 07:30:51 +1100 Subject: [PATCH 414/985] Powerview rename blackout to opaque (#80163) --- .../hunterdouglas_powerview/cover.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 0082c68e26e..347b2c3af03 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -731,7 +731,7 @@ class PowerViewShadeTDBUTop(PowerViewShadeDualRailBase): class PowerViewShadeDualOverlappedBase(PowerViewShade): - """Represent a shade that has a front sheer and rear blackout panel. + """Represent a shade that has a front sheer and rear opaque panel. This equates to two shades being controlled by one motor """ @@ -744,7 +744,7 @@ class PowerViewShadeDualOverlappedBase(PowerViewShade): # 51 - 100 is equiv to 1-100 on other shades - one motor, two shades primary = (hd_position_to_hass(self.positions.primary, MAX_POSITION) / 2) + 50 # poskind 2 represents the shade first half of the shade in hass - # rear (blackout) must be fully open before front can move + # rear (opaque) must be fully open before front can move # 51 - 100 is equiv to 1-100 on other shades - one motor, two shades secondary = hd_position_to_hass(self.positions.secondary, MAX_POSITION) / 2 return ceil(primary + secondary) @@ -773,7 +773,7 @@ class PowerViewShadeDualOverlappedBase(PowerViewShade): class PowerViewShadeDualOverlappedCombined(PowerViewShadeDualOverlappedBase): - """Represent a shade that has a front sheer and rear blackout panel. + """Represent a shade that has a front sheer and rear opaque panel. This equates to two shades being controlled by one motor. The front shade must be completely down before the rear shade will move. @@ -842,7 +842,7 @@ class PowerViewShadeDualOverlappedCombined(PowerViewShadeDualOverlappedBase): class PowerViewShadeDualOverlappedFront(PowerViewShadeDualOverlappedBase): - """Represent the shade front panel - These have a blackout panel too. + """Represent the shade front panel - These have a opaque panel too. This equates to two shades being controlled by one motor. The front shade must be completely down before the rear shade will move. @@ -850,7 +850,7 @@ class PowerViewShadeDualOverlappedFront(PowerViewShadeDualOverlappedBase): API Class: ShadeDualOverlapped + ShadeDualOverlappedTilt90 + ShadeDualOverlappedTilt180 Type 8 - Duolite (front and rear shades) - Type 9 - Duolite with 90° Tilt (front bottom up shade that also tilts plus a rear blackout (non-tilting) shade) + Type 9 - Duolite with 90° Tilt (front bottom up shade that also tilts plus a rear opaque (non-tilting) shade) Type 10 - Duolite with 180° Tilt """ @@ -903,7 +903,7 @@ class PowerViewShadeDualOverlappedFront(PowerViewShadeDualOverlappedBase): class PowerViewShadeDualOverlappedRear(PowerViewShadeDualOverlappedBase): - """Represent the shade front panel - These have a blackout panel too. + """Represent the shade front panel - These have a opaque panel too. This equates to two shades being controlled by one motor. The front shade must be completely down before the rear shade will move. @@ -911,7 +911,7 @@ class PowerViewShadeDualOverlappedRear(PowerViewShadeDualOverlappedBase): API Class: ShadeDualOverlapped + ShadeDualOverlappedTilt90 + ShadeDualOverlappedTilt180 Type 8 - Duolite (front and rear shades) - Type 9 - Duolite with 90° Tilt (front bottom up shade that also tilts plus a rear blackout (non-tilting) shade) + Type 9 - Duolite with 90° Tilt (front bottom up shade that also tilts plus a rear opaque (non-tilting) shade) Type 10 - Duolite with 180° Tilt """ @@ -975,7 +975,7 @@ class PowerViewShadeDualOverlappedRear(PowerViewShadeDualOverlappedBase): class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombined): - """Represent a shade that has a front sheer and rear blackout panel. + """Represent a shade that has a front sheer and rear opaque panel. This equates to two shades being controlled by one motor. The front shade must be completely down before the rear shade will move. @@ -984,7 +984,7 @@ class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombi Sibling Class: PowerViewShadeDualOverlappedFront, PowerViewShadeDualOverlappedRear API Class: ShadeDualOverlappedTilt90 + ShadeDualOverlappedTilt180 - Type 9 - Duolite with 90° Tilt (front bottom up shade that also tilts plus a rear blackout (non-tilting) shade) + Type 9 - Duolite with 90° Tilt (front bottom up shade that also tilts plus a rear opaque (non-tilting) shade) Type 10 - Duolite with 180° Tilt """ @@ -1016,7 +1016,7 @@ class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombi # 51 - 100 is equiv to 1-100 on other shades - one motor, two shades primary = (hd_position_to_hass(self.positions.primary, MAX_POSITION) / 2) + 50 # poskind 2 represents the shade first half of the shade in hass - # rear (blackout) must be fully open before front can move + # rear (opaque) must be fully open before front can move # 51 - 100 is equiv to 1-100 on other shades - one motor, two shades secondary = hd_position_to_hass(self.positions.secondary, MAX_POSITION) / 2 vane = hd_position_to_hass(self.positions.vane, self._max_tilt) From 4cf0f9b19711cca20b476ea999ce223c5c6c09b0 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 13 Oct 2022 00:06:23 +0200 Subject: [PATCH 415/985] Fix incorrect deprecation year for conversion utils (#80195) Fix incorrect depr year --- homeassistant/util/distance.py | 2 +- homeassistant/util/pressure.py | 2 +- homeassistant/util/speed.py | 2 +- homeassistant/util/temperature.py | 2 +- homeassistant/util/volume.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/util/distance.py b/homeassistant/util/distance.py index f5dbeaf42d5..719379d4c61 100644 --- a/homeassistant/util/distance.py +++ b/homeassistant/util/distance.py @@ -48,7 +48,7 @@ def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" report( "uses distance utility. This is deprecated since 2022.10 and will " - "stop working in Home Assistant 2022.4, it should be updated to use " + "stop working in Home Assistant 2023.4, it should be updated to use " "unit_conversion.DistanceConverter instead", error_if_core=False, ) diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index 2a8e20ed025..d6d0c79741f 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -27,7 +27,7 @@ def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" report( "uses pressure utility. This is deprecated since 2022.10 and will " - "stop working in Home Assistant 2022.4, it should be updated to use " + "stop working in Home Assistant 2023.4, it should be updated to use " "unit_conversion.PressureConverter instead", error_if_core=False, ) diff --git a/homeassistant/util/speed.py b/homeassistant/util/speed.py index 76ea873d7fe..f531e2d78f7 100644 --- a/homeassistant/util/speed.py +++ b/homeassistant/util/speed.py @@ -34,7 +34,7 @@ def convert(value: float, from_unit: str, to_unit: str) -> float: """Convert one unit of measurement to another.""" report( "uses speed utility. This is deprecated since 2022.10 and will " - "stop working in Home Assistant 2022.4, it should be updated to use " + "stop working in Home Assistant 2023.4, it should be updated to use " "unit_conversion.SpeedConverter instead", error_if_core=False, ) diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index 9173fbc5eee..0c2608eb4b5 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -39,7 +39,7 @@ def convert( """Convert a temperature from one unit to another.""" report( "uses temperature utility. This is deprecated since 2022.10 and will " - "stop working in Home Assistant 2022.4, it should be updated to use " + "stop working in Home Assistant 2023.4, it should be updated to use " "unit_conversion.TemperatureConverter instead", error_if_core=False, ) diff --git a/homeassistant/util/volume.py b/homeassistant/util/volume.py index b468b9e6e0d..e21cebd2982 100644 --- a/homeassistant/util/volume.py +++ b/homeassistant/util/volume.py @@ -42,7 +42,7 @@ def convert(volume: float, from_unit: str, to_unit: str) -> float: """Convert a volume from one unit to another.""" report( "uses volume utility. This is deprecated since 2022.10 and will " - "stop working in Home Assistant 2022.4, it should be updated to use " + "stop working in Home Assistant 2023.4, it should be updated to use " "unit_conversion.VolumeConverter instead", error_if_core=False, ) From 01c66aa7c1f9624095fe472ff02d9613423d7853 Mon Sep 17 00:00:00 2001 From: Kevin Addeman Date: Wed, 12 Oct 2022 20:26:54 -0400 Subject: [PATCH 416/985] Add support for area field from pylutron_caseta (#80221) --- .../components/lutron_caseta/__init__.py | 58 ++++++++++++------- .../components/lutron_caseta/binary_sensor.py | 10 +++- .../components/lutron_caseta/scene.py | 3 +- tests/components/lutron_caseta/__init__.py | 29 +++++++++- .../lutron_caseta/test_diagnostics.py | 18 +++++- 5 files changed, 88 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 5ef195514d3..dae83a045a2 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -177,7 +177,7 @@ async def async_setup_entry( ) buttons = bridge.buttons - _async_register_bridge_device(hass, entry_id, bridge_device) + _async_register_bridge_device(hass, entry_id, bridge_device, bridge) button_devices, device_info_by_device_id = _async_register_button_devices( hass, entry_id, bridge, bridge_device, buttons ) @@ -196,18 +196,25 @@ async def async_setup_entry( @callback def _async_register_bridge_device( - hass: HomeAssistant, config_entry_id: str, bridge_device: dict + hass: HomeAssistant, config_entry_id: str, bridge_device: dict, bridge: Smartbridge ) -> None: """Register the bridge device in the device registry.""" device_registry = dr.async_get(hass) - device_registry.async_get_or_create( - name=bridge_device["name"], - manufacturer=MANUFACTURER, - config_entry_id=config_entry_id, - identifiers={(DOMAIN, bridge_device["serial"])}, - model=f"{bridge_device['model']} ({bridge_device['type']})", - configuration_url="https://device-login.lutron.com", - ) + + device_args: DeviceInfo = { + "name": bridge_device["name"], + "manufacturer": MANUFACTURER, + "identifiers": {(DOMAIN, bridge_device["serial"])}, + "model": f"{bridge_device['model']} ({bridge_device['type']})", + "via_device": (DOMAIN, bridge_device["serial"]), + "configuration_url": "https://device-login.lutron.com", + } + + area = _area_name_from_id(bridge.areas, bridge_device["area"]) + if area != UNASSIGNED_AREA: + device_args["suggested_area"] = area + + device_registry.async_get_or_create(**device_args, config_entry_id=config_entry_id) @callback @@ -241,7 +248,10 @@ def _async_register_button_devices( continue seen.add(ha_device_serial) - area, name = _area_and_name_from_name(ha_device["name"]) + area = _area_name_from_id(bridge.areas, ha_device["area"]) + # name field is still a combination of area and name from pylytron-caseta + # extract the name portion only. + name = ha_device["name"].split("_")[-1] device_args: DeviceInfo = { "name": f"{area} {name}", "manufacturer": MANUFACTURER, @@ -265,12 +275,19 @@ def _handle_none_keypad_serial(keypad_device: dict, bridge_serial: int) -> str: return keypad_device["serial"] or f"{bridge_serial}_{keypad_device['device_id']}" -def _area_and_name_from_name(device_name: str) -> tuple[str, str]: - """Return the area and name from the devices internal name.""" - if "_" in device_name: - area_device_name = device_name.split("_", 1) - return area_device_name[0], area_device_name[1] - return UNASSIGNED_AREA, device_name +def _area_name_from_id(areas: dict[str, dict], area_id: str) -> str: + """Return the full area name including parent(s).""" + + if area_id is None: + return UNASSIGNED_AREA + + area = areas[area_id] + if "parent_id" in area: + parent_area = area["parent_id"] + if parent_area is not None: + return f"{_area_name_from_id(areas, parent_area)} {area['name']}" + + return area["name"] @callback @@ -316,7 +333,8 @@ def _async_subscribe_pico_remote_events( ) type_ = _lutron_model_to_device_type(ha_device["model"], ha_device["type"]) - area, name = _area_and_name_from_name(ha_device["name"]) + area = _area_name_from_id(bridge_device.areas, ha_device["area"]) + name = ha_device["name"].split("_")[-1] leap_button_number = device["button_number"] lip_button_number = async_get_lip_button(type_, leap_button_number) hass_device = dev_reg.async_get_device({(DOMAIN, ha_device_serial)}) @@ -377,8 +395,8 @@ class LutronCasetaDevice(Entity): if "parent_device" in device: # This is a child entity, handle the naming in button.py and switch.py return - - area, name = _area_and_name_from_name(device["name"]) + area = _area_name_from_id(self._smartbridge.areas, device["area"]) + name = device["name"].split("_")[-1] self._attr_name = full_name = f"{area} {name}" info = DeviceInfo( # Historically we used the device serial number for the identifier diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index 6df1125f7e9..29e59c426b5 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -6,13 +6,14 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_SUGGESTED_AREA from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice, _area_and_name_from_name -from .const import CONFIG_URL, MANUFACTURER +from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice, _area_name_from_id +from .const import CONFIG_URL, MANUFACTURER, UNASSIGNED_AREA from .models import LutronCasetaData @@ -43,7 +44,8 @@ class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): def __init__(self, device, data): """Init an occupancy sensor.""" super().__init__(device, data) - _, name = _area_and_name_from_name(device["name"]) + area = _area_name_from_id(self._smartbridge.areas, device["area"]) + name = f"{area} {device['device_name']}" self._attr_name = name self._attr_device_info = DeviceInfo( identifiers={(CASETA_DOMAIN, self.unique_id)}, @@ -54,6 +56,8 @@ class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): configuration_url=CONFIG_URL, entry_type=DeviceEntryType.SERVICE, ) + if area != UNASSIGNED_AREA: + self._attr_device_info[ATTR_SUGGESTED_AREA] = area @property def is_on(self): diff --git a/homeassistant/components/lutron_caseta/scene.py b/homeassistant/components/lutron_caseta/scene.py index cc3be8a6479..997397c5b6c 100644 --- a/homeassistant/components/lutron_caseta/scene.py +++ b/homeassistant/components/lutron_caseta/scene.py @@ -9,7 +9,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import _area_and_name_from_name from .const import DOMAIN as CASETA_DOMAIN from .models import LutronCasetaData from .util import serial_to_unique_id @@ -42,7 +41,7 @@ class LutronCasetaScene(Scene): self._attr_device_info = DeviceInfo( identifiers={(CASETA_DOMAIN, data.bridge_device["serial"])}, ) - self._attr_name = _area_and_name_from_name(scene["name"])[1] + self._attr_name = scene["name"] self._attr_unique_id = f"scene_{bridge_unique_id}_{self._scene_id}" async def async_activate(self, **kwargs: Any) -> None: diff --git a/tests/components/lutron_caseta/__init__.py b/tests/components/lutron_caseta/__init__.py index f6af22034a7..cc6818dab14 100644 --- a/tests/components/lutron_caseta/__init__.py +++ b/tests/components/lutron_caseta/__init__.py @@ -105,7 +105,7 @@ class MockBridge: """Initialize MockBridge instance with configured mock connectivity.""" self.can_connect = can_connect self.is_currently_connected = False - self.areas = {} + self.areas = self.load_areas() self.occupancy_groups = {} self.scenes = self.get_scenes() self.devices = self.load_devices() @@ -126,10 +126,28 @@ class MockBridge: """Return whether the mock bridge is connected.""" return self.is_currently_connected + def load_areas(self): + """Loak mock areas into self.areas.""" + return { + "898": {"id": "898", "name": "Basement", "parent_id": None}, + "822": {"id": "822", "name": "Bedroom", "parent_id": "898"}, + "910": {"id": "910", "name": "Bathroom", "parent_id": "898"}, + "1024": {"id": "1024", "name": "Master Bedroom", "parent_id": None}, + "1025": {"id": "1025", "name": "Kitchen", "parent_id": None}, + "1026": {"id": "1026", "name": "Dining Room", "parent_id": None}, + "1205": {"id": "1205", "name": "Hallway", "parent_id": None}, + } + def load_devices(self): """Load mock devices into self.devices.""" return { - "1": {"serial": 1234, "name": "bridge", "model": "model", "type": "type"}, + "1": { + "serial": 1234, + "name": "bridge", + "model": "model", + "type": "type", + "area": "1205", + }, "801": { "device_id": "801", "current_state": 100, @@ -141,6 +159,7 @@ class MockBridge: "model": None, "serial": None, "tilt": None, + "area": "822", }, "802": { "device_id": "802", @@ -153,6 +172,7 @@ class MockBridge: "model": None, "serial": None, "tilt": None, + "area": "822", }, "803": { "device_id": "803", @@ -165,6 +185,7 @@ class MockBridge: "model": None, "serial": None, "tilt": None, + "area": "910", }, "804": { "device_id": "804", @@ -177,6 +198,7 @@ class MockBridge: "model": None, "serial": None, "tilt": None, + "area": "1024", }, "901": { "device_id": "901", @@ -189,6 +211,7 @@ class MockBridge: "model": None, "serial": 5442321, "tilt": None, + "area": "1025", }, "9": { "device_id": "9", @@ -203,7 +226,7 @@ class MockBridge: "model": "PJ2-3BRL-GXX-X01", "serial": 68551522, "device_name": "Pico", - "area": "6", + "area": "1026", }, "1355": { "device_id": "1355", diff --git a/tests/components/lutron_caseta/test_diagnostics.py b/tests/components/lutron_caseta/test_diagnostics.py index b0d6aae1058..8fa293c19d6 100644 --- a/tests/components/lutron_caseta/test_diagnostics.py +++ b/tests/components/lutron_caseta/test_diagnostics.py @@ -40,7 +40,15 @@ async def test_diagnostics(hass, hass_client) -> None: diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) assert diag == { "data": { - "areas": {}, + "areas": { + "898": {"id": "898", "name": "Basement", "parent_id": None}, + "822": {"id": "822", "name": "Bedroom", "parent_id": "898"}, + "910": {"id": "910", "name": "Bathroom", "parent_id": "898"}, + "1024": {"id": "1024", "name": "Master Bedroom", "parent_id": None}, + "1025": {"id": "1025", "name": "Kitchen", "parent_id": None}, + "1026": {"id": "1026", "name": "Dining Room", "parent_id": None}, + "1205": {"id": "1205", "name": "Hallway", "parent_id": None}, + }, "buttons": { "111": { "device_id": "111", @@ -73,6 +81,7 @@ async def test_diagnostics(hass, hass_client) -> None: "name": "bridge", "serial": 1234, "type": "type", + "area": "1205", }, "801": { "device_id": "801", @@ -85,6 +94,7 @@ async def test_diagnostics(hass, hass_client) -> None: "model": None, "serial": None, "tilt": None, + "area": "822", }, "802": { "device_id": "802", @@ -97,6 +107,7 @@ async def test_diagnostics(hass, hass_client) -> None: "model": None, "serial": None, "tilt": None, + "area": "822", }, "803": { "device_id": "803", @@ -109,6 +120,7 @@ async def test_diagnostics(hass, hass_client) -> None: "model": None, "serial": None, "tilt": None, + "area": "910", }, "804": { "device_id": "804", @@ -121,6 +133,7 @@ async def test_diagnostics(hass, hass_client) -> None: "model": None, "serial": None, "tilt": None, + "area": "1024", }, "901": { "device_id": "901", @@ -133,6 +146,7 @@ async def test_diagnostics(hass, hass_client) -> None: "model": None, "serial": 5442321, "tilt": None, + "area": "1025", }, "9": { "device_id": "9", @@ -147,7 +161,7 @@ async def test_diagnostics(hass, hass_client) -> None: "model": "PJ2-3BRL-GXX-X01", "serial": 68551522, "device_name": "Pico", - "area": "6", + "area": "1026", }, "1355": { "device_id": "1355", From ca4c4774ca9cf4efc9578cd9b2a9d9bd39ed9b31 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 13 Oct 2022 00:33:41 +0000 Subject: [PATCH 417/985] [ci skip] Translation update --- .../devolo_home_network/translations/bg.json | 8 +++++++- .../devolo_home_network/translations/ru.json | 8 +++++++- .../devolo_home_network/translations/sv.json | 8 +++++++- .../fully_kiosk/translations/en.json | 1 + .../lametric/translations/select.bg.json | 8 ++++++++ .../lutron_caseta/translations/es.json | 3 +++ .../components/snooz/translations/bg.json | 20 +++++++++++++++++++ .../components/snooz/translations/ru.json | 19 +++++++++++++++++- .../components/snooz/translations/sv.json | 18 +++++++++++++++++ .../components/zwave_js/translations/et.json | 3 ++- .../components/zwave_js/translations/ru.json | 3 ++- .../zwave_js/translations/zh-Hant.json | 3 ++- 12 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/lametric/translations/select.bg.json create mode 100644 homeassistant/components/snooz/translations/bg.json create mode 100644 homeassistant/components/snooz/translations/sv.json diff --git a/homeassistant/components/devolo_home_network/translations/bg.json b/homeassistant/components/devolo_home_network/translations/bg.json index c1dc13fe2d7..a90c099889a 100644 --- a/homeassistant/components/devolo_home_network/translations/bg.json +++ b/homeassistant/components/devolo_home_network/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", @@ -9,6 +10,11 @@ }, "flow_title": "{product} ({name})", "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + }, "user": { "data": { "ip_address": "IP \u0430\u0434\u0440\u0435\u0441" diff --git a/homeassistant/components/devolo_home_network/translations/ru.json b/homeassistant/components/devolo_home_network/translations/ru.json index 4cc909b8816..3149b2951c7 100644 --- a/homeassistant/components/devolo_home_network/translations/ru.json +++ b/homeassistant/components/devolo_home_network/translations/ru.json @@ -2,7 +2,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.", - "home_control": "\u0426\u0435\u043d\u0442\u0440\u0430\u043b\u044c\u043d\u044b\u0439 \u0431\u043b\u043e\u043a \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f devolo Home Control \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441 \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439." + "home_control": "\u0426\u0435\u043d\u0442\u0440\u0430\u043b\u044c\u043d\u044b\u0439 \u0431\u043b\u043e\u043a \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f devolo Home Control \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441 \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439.", + "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.", @@ -10,6 +11,11 @@ }, "flow_title": "{product} ({name})", "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + }, "user": { "data": { "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441" diff --git a/homeassistant/components/devolo_home_network/translations/sv.json b/homeassistant/components/devolo_home_network/translations/sv.json index 097e9d826b9..9ea453fadde 100644 --- a/homeassistant/components/devolo_home_network/translations/sv.json +++ b/homeassistant/components/devolo_home_network/translations/sv.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad", - "home_control": "Devolo Home Control Central Unit fungerar inte med denna integration." + "home_control": "Devolo Home Control Central Unit fungerar inte med denna integration.", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta.", @@ -10,6 +11,11 @@ }, "flow_title": "{product} ({name})", "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + } + }, "user": { "data": { "ip_address": "IP-adress" diff --git a/homeassistant/components/fully_kiosk/translations/en.json b/homeassistant/components/fully_kiosk/translations/en.json index 24823d68a60..338c50514fb 100644 --- a/homeassistant/components/fully_kiosk/translations/en.json +++ b/homeassistant/components/fully_kiosk/translations/en.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, "step": { diff --git a/homeassistant/components/lametric/translations/select.bg.json b/homeassistant/components/lametric/translations/select.bg.json new file mode 100644 index 00000000000..94363e744b4 --- /dev/null +++ b/homeassistant/components/lametric/translations/select.bg.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e", + "manual": "\u0420\u044a\u0447\u043do" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/es.json b/homeassistant/components/lutron_caseta/translations/es.json index 95559e14baf..c17b0212332 100644 --- a/homeassistant/components/lutron_caseta/translations/es.json +++ b/homeassistant/components/lutron_caseta/translations/es.json @@ -33,6 +33,9 @@ "button_2": "Segundo bot\u00f3n", "button_3": "Tercer bot\u00f3n", "button_4": "Cuarto bot\u00f3n", + "button_5": "Quinto bot\u00f3n", + "button_6": "Sexto bot\u00f3n", + "button_7": "S\u00e9ptimo bot\u00f3n", "close_1": "Cerrar 1", "close_2": "Cerrar 2", "close_3": "Cerrar 3", diff --git a/homeassistant/components/snooz/translations/bg.json b/homeassistant/components/snooz/translations/bg.json new file mode 100644 index 00000000000..a61dac839ad --- /dev/null +++ b/homeassistant/components/snooz/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0437\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/snooz/translations/ru.json b/homeassistant/components/snooz/translations/ru.json index 3488392c218..13b6c954ad9 100644 --- a/homeassistant/components/snooz/translations/ru.json +++ b/homeassistant/components/snooz/translations/ru.json @@ -5,6 +5,23 @@ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." }, - "flow_title": "{name}" + "flow_title": "{name}", + "progress": { + "wait_for_pairing_mode": "\u0427\u0442\u043e\u0431\u044b \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443, \u043f\u0435\u0440\u0435\u0432\u0435\u0434\u0438\u0442\u0435 \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432 \u0440\u0435\u0436\u0438\u043c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f. \n\n### \u041a\u0430\u043a \u0432\u043e\u0439\u0442\u0438 \u0432 \u0440\u0435\u0436\u0438\u043c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f\n1. \u041f\u0440\u0438\u043d\u0443\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0437\u0430\u043a\u0440\u043e\u0439\u0442\u0435 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f SNOOZ.\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0438 \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0439\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u043f\u0438\u0442\u0430\u043d\u0438\u044f \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435. \u041e\u0442\u043f\u0443\u0441\u0442\u0438\u0442\u0435, \u043a\u043e\u0433\u0434\u0430 \u0438\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440\u044b \u043d\u0430\u0447\u043d\u0443\u0442 \u043c\u0438\u0433\u0430\u0442\u044c (\u043f\u0440\u0438\u043c\u0435\u0440\u043d\u043e 5 \u0441\u0435\u043a\u0443\u043d\u0434)." + }, + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "pairing_timeout": { + "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u0435\u0440\u0435\u0448\u043b\u043e \u0432 \u0440\u0435\u0436\u0438\u043c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u00ab\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c\u00bb, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u044c \u043f\u043e\u043f\u044b\u0442\u043a\u0443. \n\n### \u0418\u0441\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\n1. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u043c\u0443 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044e.\n2. \u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043e\u0442 \u0441\u0435\u0442\u0438 \u043d\u0430 5 \u0441\u0435\u043a\u0443\u043d\u0434, \u0437\u0430\u0442\u0435\u043c \u0441\u043d\u043e\u0432\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0435." + }, + "user": { + "data": { + "address": "\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 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/snooz/translations/sv.json b/homeassistant/components/snooz/translations/sv.json new file mode 100644 index 00000000000..ab14fbbddb6 --- /dev/null +++ b/homeassistant/components/snooz/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att konfigurera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/et.json b/homeassistant/components/zwave_js/translations/et.json index e74301c8ce4..df15c31b8c2 100644 --- a/homeassistant/components/zwave_js/translations/et.json +++ b/homeassistant/components/zwave_js/translations/et.json @@ -10,7 +10,8 @@ "already_in_progress": "Seadistamine on juba k\u00e4imas", "cannot_connect": "\u00dchendamine nurjus", "discovery_requires_supervisor": "Avastamine n\u00f5uab supervisorit.", - "not_zwave_device": "Avastatud seade ei ole Z-Wave seade." + "not_zwave_device": "Avastatud seade ei ole Z-Wave seade.", + "not_zwave_js_addon": "Avastatud lisandmoodul ei ole ametlik Z-Wave JS-i lisandmoodul." }, "error": { "addon_start_failed": "Z-Wave JS lisandmooduli k\u00e4ivitamine nurjus. Kontrolli seadistusi.", diff --git a/homeassistant/components/zwave_js/translations/ru.json b/homeassistant/components/zwave_js/translations/ru.json index bbf816046df..3ffe43abb6f 100644 --- a/homeassistant/components/zwave_js/translations/ru.json +++ b/homeassistant/components/zwave_js/translations/ru.json @@ -10,7 +10,8 @@ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \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.", "discovery_requires_supervisor": "\u0414\u043b\u044f \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f Supervisor.", - "not_zwave_device": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Z-Wave." + "not_zwave_device": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Z-Wave.", + "not_zwave_js_addon": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u043d\u0435\u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS." }, "error": { "addon_start_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.", diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json index 3c5f898324a..c970bae125e 100644 --- a/homeassistant/components/zwave_js/translations/zh-Hant.json +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -10,7 +10,8 @@ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557", "discovery_requires_supervisor": "\u641c\u7d22\u529f\u80fd\u9700\u8981 Supervisor \u6b0a\u9650\u3002", - "not_zwave_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Z-Wave \u88dd\u7f6e" + "not_zwave_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Z-Wave \u88dd\u7f6e", + "not_zwave_js_addon": "\u767c\u73fe\u4e4b\u9644\u52a0\u5143\u4ef6\u4e26\u975e\u5b98\u65b9 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u3002" }, "error": { "addon_start_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u52d5\u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u3002", From 466c4656cab32e3939bb61f90c52aff44180a5fb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 13 Oct 2022 08:11:54 +0200 Subject: [PATCH 418/985] Refactor recorder migration (#80175) * Refactor recorder migration * Improve test coverage --- homeassistant/components/recorder/core.py | 39 ++++++---- .../components/recorder/migration.py | 72 ++++++++++++++----- tests/components/recorder/test_init.py | 17 +++++ tests/components/recorder/test_migrate.py | 12 ++-- 4 files changed, 101 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 2530b303e15..f7d2b774aeb 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -588,24 +588,31 @@ class Recorder(threading.Thread): def run(self) -> None: """Start processing events to save.""" - current_version = self._setup_recorder() + setup_result = self._setup_recorder() - if current_version is None: + if not setup_result: + # Give up if we could not connect self.hass.add_job(self.async_connection_failed) return - self.schema_version = current_version + schema_status = migration.validate_db_schema(self.hass, self.get_session) + if schema_status is None: + # Give up if we could not validate the schema + self.hass.add_job(self.async_connection_failed) + return + self.schema_version = schema_status.current_version - schema_is_current = migration.schema_is_current(current_version) - if schema_is_current: + schema_is_valid = migration.schema_is_valid(schema_status) + + if schema_is_valid: self._setup_run() else: self.migration_in_progress = True - self.migration_is_live = migration.live_migration(current_version) + self.migration_is_live = migration.live_migration(schema_status) self.hass.add_job(self.async_connection_success) - if self.migration_is_live or schema_is_current: + if self.migration_is_live or schema_is_valid: # If the migrate is live or the schema is current, we need to # wait for startup to complete. If its not live, we need to continue # on. @@ -623,8 +630,8 @@ class Recorder(threading.Thread): self.hass.add_job(self.async_set_db_ready) return - if not schema_is_current: - if self._migrate_schema_and_setup_run(current_version): + if not schema_is_valid: + if self._migrate_schema_and_setup_run(schema_status): self.schema_version = SCHEMA_VERSION if not self._event_listener: # If the schema migration takes so long that the end @@ -689,14 +696,14 @@ class Recorder(threading.Thread): # happens to rollback and recover self._reopen_event_session() - def _setup_recorder(self) -> None | int: - """Create connect to the database and get the schema version.""" + def _setup_recorder(self) -> bool: + """Create a connection to the database.""" tries = 1 while tries <= self.db_max_retries: try: self._setup_connection() - return migration.get_schema_version(self.get_session) + return True except UnsupportedDialect: break except Exception as err: # pylint: disable=broad-except @@ -708,14 +715,16 @@ class Recorder(threading.Thread): tries += 1 time.sleep(self.db_retry_wait) - return None + return False @callback def _async_migration_started(self) -> None: """Set the migration started event.""" self.async_migration_event.set() - def _migrate_schema_and_setup_run(self, current_version: int) -> bool: + def _migrate_schema_and_setup_run( + self, schema_status: migration.SchemaValidationStatus + ) -> bool: """Migrate schema to the latest version.""" persistent_notification.create( self.hass, @@ -727,7 +736,7 @@ class Recorder(threading.Thread): try: migration.migrate_schema( - self, self.hass, self.engine, self.get_session, current_version + self, self.hass, self.engine, self.get_session, schema_status ) except exc.DatabaseError as err: if self._handle_database_error(err): diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 3482f9aa942..227500aaf0f 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Iterable import contextlib +from dataclasses import dataclass from datetime import timedelta import logging from typing import TYPE_CHECKING, cast @@ -61,33 +62,65 @@ def raise_if_exception_missing_str(ex: Exception, match_substrs: Iterable[str]) raise ex -def get_schema_version(session_maker: Callable[[], Session]) -> int: +def get_schema_version(session_maker: Callable[[], Session]) -> int | None: """Get the schema version.""" - with session_scope(session=session_maker()) as session: - res = ( - session.query(SchemaChanges) - .order_by(SchemaChanges.change_id.desc()) - .first() - ) - current_version = getattr(res, "schema_version", None) - - if current_version is None: - current_version = _inspect_schema_version(session) - _LOGGER.debug( - "No schema version found. Inspected version: %s", current_version + try: + with session_scope(session=session_maker()) as session: + res = ( + session.query(SchemaChanges) + .order_by(SchemaChanges.change_id.desc()) + .first() ) + current_version = getattr(res, "schema_version", None) - return cast(int, current_version) + if current_version is None: + current_version = _inspect_schema_version(session) + _LOGGER.debug( + "No schema version found. Inspected version: %s", current_version + ) + + return cast(int, current_version) + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Error when determining DB schema version: %s", err) + return None -def schema_is_current(current_version: int) -> bool: +@dataclass +class SchemaValidationStatus: + """Store schema validation status.""" + + current_version: int + + +def _schema_is_current(current_version: int) -> bool: """Check if the schema is current.""" return current_version == SCHEMA_VERSION -def live_migration(current_version: int) -> bool: +def schema_is_valid(schema_status: SchemaValidationStatus) -> bool: + """Check if the schema is valid.""" + return _schema_is_current(schema_status.current_version) + + +def validate_db_schema( + hass: HomeAssistant, session_maker: Callable[[], Session] +) -> SchemaValidationStatus | None: + """Check if the schema is valid. + + This checks that the schema is the current version as well as for some common schema + errors caused by manual migration between database engines, for example importing an + SQLite database to MariaDB. + """ + current_version = get_schema_version(session_maker) + if current_version is None: + return None + + return SchemaValidationStatus(current_version) + + +def live_migration(schema_status: SchemaValidationStatus) -> bool: """Check if live migration is possible.""" - return current_version >= LIVE_MIGRATION_MIN_SCHEMA_VERSION + return schema_status.current_version >= LIVE_MIGRATION_MIN_SCHEMA_VERSION def migrate_schema( @@ -95,13 +128,14 @@ def migrate_schema( hass: HomeAssistant, engine: Engine, session_maker: Callable[[], Session], - current_version: int, + schema_status: SchemaValidationStatus, ) -> None: """Check if the schema needs to be upgraded.""" + current_version = schema_status.current_version _LOGGER.warning("Database is about to upgrade. Schema version: %s", current_version) db_ready = False for version in range(current_version, SCHEMA_VERSION): - if live_migration(version) and not db_ready: + if live_migration(SchemaValidationStatus(version)) and not db_ready: db_ready = True instance.migration_is_live = True hass.add_job(instance.async_set_db_ready) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 815af89198d..977e32e9a71 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -665,6 +665,23 @@ def test_recorder_setup_failure(hass): hass.stop() +def test_recorder_validate_schema_failure(hass): + """Test some exceptions.""" + recorder_helper.async_initialize_recorder(hass) + with patch( + "homeassistant.components.recorder.migration._inspect_schema_version" + ) as inspect_schema_version, patch( + "homeassistant.components.recorder.core.time.sleep" + ): + inspect_schema_version.side_effect = ImportError("driver not found") + rec = _default_recorder(hass) + rec.async_initialize() + rec.start() + rec.join() + + hass.stop() + + def test_recorder_setup_failure_without_event_listener(hass): """Test recorder setup failure when the event listener is not setup.""" recorder_helper.async_initialize_recorder(hass) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 9e0609de5b6..45268ae819b 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -134,14 +134,16 @@ async def test_database_migration_encounters_corruption(hass): sqlite3_exception.__cause__ = sqlite3.DatabaseError() with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( - "homeassistant.components.recorder.migration.schema_is_current", - side_effect=[False, True], + "homeassistant.components.recorder.migration._schema_is_current", + side_effect=[False], ), patch( "homeassistant.components.recorder.migration.migrate_schema", side_effect=sqlite3_exception, ), patch( "homeassistant.components.recorder.core.move_away_broken_database" - ) as move_away: + ) as move_away, patch( + "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", + ): recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} @@ -159,8 +161,8 @@ async def test_database_migration_encounters_corruption_not_sqlite(hass): assert recorder.util.async_migration_in_progress(hass) is False with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( - "homeassistant.components.recorder.migration.schema_is_current", - side_effect=[False, True], + "homeassistant.components.recorder.migration._schema_is_current", + side_effect=[False], ), patch( "homeassistant.components.recorder.migration.migrate_schema", side_effect=DatabaseError("statement", {}, []), From 1e75c3829e72531eabd96afa1bad7f09e9530b04 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 13 Oct 2022 09:04:24 +0200 Subject: [PATCH 419/985] Register Alert services as entity services (#80213) --- homeassistant/components/alert/__init__.py | 51 ++++------------------ 1 file changed, 8 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 53fb89b19e6..460b418201f 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -14,7 +14,6 @@ from homeassistant.components.notify import ( DOMAIN as DOMAIN_NOTIFY, ) from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_ENTITY_ID, CONF_NAME, CONF_REPEAT, @@ -26,10 +25,10 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import Event, HomeAssistant, ServiceCall -from homeassistant.helpers import service +from homeassistant.core import Event, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import ( async_track_point_in_time, async_track_state_change_event, @@ -77,11 +76,11 @@ CONFIG_SCHEMA = vol.Schema( {DOMAIN: cv.schema_with_slug_keys(ALERT_SCHEMA)}, extra=vol.ALLOW_EXTRA ) -ALERT_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids}) - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Alert component.""" + component = EntityComponent[Alert](LOGGER, DOMAIN, hass) + entities: list[Alert] = [] for object_id, cfg in config[DOMAIN].items(): @@ -121,45 +120,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if not entities: return False - async def async_handle_alert_service(service_call: ServiceCall) -> None: - """Handle calls to alert services.""" - alert_ids = await service.async_extract_entity_ids(hass, service_call) + component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") + component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") - for alert_id in alert_ids: - for alert in entities: - if alert.entity_id != alert_id: - continue - - alert.async_set_context(service_call.context) - if service_call.service == SERVICE_TURN_ON: - await alert.async_turn_on() - elif service_call.service == SERVICE_TOGGLE: - await alert.async_toggle() - else: - await alert.async_turn_off() - - # Setup service calls - hass.services.async_register( - DOMAIN, - SERVICE_TURN_OFF, - async_handle_alert_service, - schema=ALERT_SERVICE_SCHEMA, - ) - hass.services.async_register( - DOMAIN, - SERVICE_TURN_ON, - async_handle_alert_service, - schema=ALERT_SERVICE_SCHEMA, - ) - hass.services.async_register( - DOMAIN, - SERVICE_TOGGLE, - async_handle_alert_service, - schema=ALERT_SERVICE_SCHEMA, - ) - - for alert in entities: - alert.async_write_ha_state() + await component.async_add_entities(entities) return True From ea6368775b6612cf0c7b3f92f74658fabf64fa97 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 13 Oct 2022 09:04:36 +0200 Subject: [PATCH 420/985] Make notifiers of Alert optional (#80209) --- homeassistant/components/alert/__init__.py | 7 +++- tests/components/alert/test_init.py | 38 +++++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 460b418201f..4f8f1dade93 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -68,7 +68,9 @@ ALERT_SCHEMA = vol.Schema( vol.Optional(CONF_DONE_MESSAGE): cv.template, vol.Optional(CONF_TITLE): cv.template, vol.Optional(CONF_DATA): dict, - vol.Required(CONF_NOTIFIERS): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_NOTIFIERS, default=list): vol.All( + cv.ensure_list, [cv.string] + ), } ) @@ -269,6 +271,9 @@ class Alert(Entity): async def _send_notification_message(self, message: Any) -> None: + if not self._notifiers: + return + msg_payload = {ATTR_MESSAGE: message} if self._title_template is not None: diff --git a/tests/components/alert/test_init.py b/tests/components/alert/test_init.py index e4f90a1a989..3d2067a9ed9 100644 --- a/tests/components/alert/test_init.py +++ b/tests/components/alert/test_init.py @@ -28,7 +28,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component NAME = "alert_test" @@ -223,6 +223,42 @@ async def test_notification(hass): assert len(events) == 2 +async def test_no_notifiers(hass: HomeAssistant) -> None: + """Test we send no notifications when there are not no.""" + events = [] + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + hass.services.async_register(notify.DOMAIN, NOTIFIER, record_event) + + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + NAME: { + CONF_NAME: NAME, + CONF_ENTITY_ID: TEST_ENTITY, + CONF_STATE: STATE_ON, + CONF_REPEAT: 30, + } + } + }, + ) + assert len(events) == 0 + + hass.states.async_set("sensor.test", STATE_ON) + await hass.async_block_till_done() + assert len(events) == 0 + + hass.states.async_set("sensor.test", STATE_OFF) + await hass.async_block_till_done() + assert len(events) == 0 + + async def test_sending_non_templated_notification(hass, mock_notifier): """Test notifications.""" assert await async_setup_component(hass, DOMAIN, TEST_CONFIG) From 937aa286b7ec8ae5a8112b96e0b847cf2218c19d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Thu, 13 Oct 2022 09:24:14 +0200 Subject: [PATCH 421/985] Plugwise: implement device availability for non-legacy devices (#80191) Co-authored-by: Franck Nijhof --- homeassistant/components/plugwise/entity.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plugwise/entity.py b/homeassistant/components/plugwise/entity.py index 694f6e5817c..4d5f78f5202 100644 --- a/homeassistant/components/plugwise/entity.py +++ b/homeassistant/components/plugwise/entity.py @@ -65,7 +65,11 @@ class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]): @property def available(self) -> bool: """Return if entity is available.""" - return super().available and self._dev_id in self.coordinator.data.devices + return ( + self._dev_id in self.coordinator.data.devices + and ("available" not in self.device or self.device["available"]) + and super().available + ) @property def device(self) -> dict[str, Any]: From 3c125c4b65f22ed8979063690a162dd33de8543e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Oct 2022 09:29:47 +0200 Subject: [PATCH 422/985] Bump docker/login-action from 2.0.0 to 2.1.0 (#80227) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index a0ae1552f34..2f844314eaa 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -146,13 +146,13 @@ jobs: echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE - name: Login to DockerHub - uses: docker/login-action@v2.0.0 + uses: docker/login-action@v2.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v2.0.0 + uses: docker/login-action@v2.1.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -212,13 +212,13 @@ jobs: fi - name: Login to DockerHub - uses: docker/login-action@v2.0.0 + uses: docker/login-action@v2.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v2.0.0 + uses: docker/login-action@v2.1.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -284,14 +284,14 @@ jobs: - name: Login to DockerHub if: matrix.registry == 'homeassistant' - uses: docker/login-action@v2.0.0 + uses: docker/login-action@v2.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry if: matrix.registry == 'ghcr.io/home-assistant' - uses: docker/login-action@v2.0.0 + uses: docker/login-action@v2.1.0 with: registry: ghcr.io username: ${{ github.repository_owner }} From 394246ababae49ee4b666a4c94ef4f901f3c62d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Oct 2022 09:57:07 +0200 Subject: [PATCH 423/985] Bump dorny/paths-filter from 2.11.0 to 2.11.1 (#80228) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 098f4f590f7..c564acf9b8c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -70,7 +70,7 @@ jobs: echo "::set-output name=key::pre-commit-${{ env.CACHE_VERSION }}-${{ hashFiles('.pre-commit-config.yaml') }}" - name: Filter for core changes - uses: dorny/paths-filter@v2.11.0 + uses: dorny/paths-filter@v2.11.1 id: core with: filters: .core_files.yaml @@ -85,7 +85,7 @@ jobs: echo "Result:" cat .integration_paths.yaml - name: Filter for integration changes - uses: dorny/paths-filter@v2.11.0 + uses: dorny/paths-filter@v2.11.1 id: integrations with: filters: .integration_paths.yaml From b0ef1e3315906b4930806800aa0229995d5b36bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Matheson=20Wergeland?= Date: Thu, 13 Oct 2022 11:40:47 +0200 Subject: [PATCH 424/985] Fix nobo_hub presenting temperature in zone with one decimal (#79743) Fix presenting temperature in zone with one decimal. Fix stepping the target temperatur without decimals. --- homeassistant/components/nobo_hub/climate.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nobo_hub/climate.py b/homeassistant/components/nobo_hub/climate.py index a465bfa77ab..ba38e0b1530 100644 --- a/homeassistant/components/nobo_hub/climate.py +++ b/homeassistant/components/nobo_hub/climate.py @@ -1,7 +1,6 @@ """Python Control of Nobø Hub - Nobø Energy Control.""" from __future__ import annotations -import logging from typing import Any from pynobo import nobo @@ -24,7 +23,7 @@ from homeassistant.const import ( ATTR_NAME, ATTR_SUGGESTED_AREA, ATTR_VIA_DEVICE, - PRECISION_WHOLE, + PRECISION_TENTHS, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback @@ -52,8 +51,6 @@ PRESET_MODES = [PRESET_NONE, PRESET_COMFORT, PRESET_ECO, PRESET_AWAY] MIN_TEMPERATURE = 7 MAX_TEMPERATURE = 40 -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, @@ -87,11 +84,12 @@ class NoboZone(ClimateEntity): _attr_max_temp = MAX_TEMPERATURE _attr_min_temp = MIN_TEMPERATURE - _attr_precision = PRECISION_WHOLE + _attr_precision = PRECISION_TENTHS _attr_preset_modes = PRESET_MODES - # Need to poll to get preset change when in HVACMode.AUTO. _attr_supported_features = SUPPORT_FLAGS _attr_temperature_unit = TEMP_CELSIUS + _attr_target_temperature_step = 1 + # Need to poll to get preset change when in HVACMode.AUTO, so can't set _attr_should_poll = False def __init__(self, zone_id, hub: nobo, override_type): """Initialize the climate device.""" From 4462f2fc46957cf7b47c595c8125cfdf85291aee Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 13 Oct 2022 11:44:48 +0200 Subject: [PATCH 425/985] Fix recorder tests related to mysql (#80238) --- tests/components/recorder/test_init.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 977e32e9a71..54e82516373 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1649,7 +1649,7 @@ async def test_disable_echo(hass, db_url, echo, caplog): @pytest.mark.parametrize( - "config_url, connect_args", + "config_url, expected_connect_args", ( ( "mariadb://user:password@SERVER_IP/DB_NAME", @@ -1677,15 +1677,15 @@ async def test_disable_echo(hass, db_url, echo, caplog): ), ( "postgresql://blabla", - None, + {}, ), ( "sqlite://blabla", - None, + {}, ), ), ) -async def test_mysql_missing_utf8mb4(hass, config_url, connect_args): +async def test_mysql_missing_utf8mb4(hass, config_url, expected_connect_args): """Test recorder fails to setup if charset=utf8mb4 is missing from db_url.""" recorder_helper.async_initialize_recorder(hass) @@ -1701,7 +1701,10 @@ async def test_mysql_missing_utf8mb4(hass, config_url, connect_args): ): await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: config_url}}) create_engine_mock.assert_called_once() - assert create_engine_mock.mock_calls[0][2].get("connect_args") == connect_args + + connect_args = create_engine_mock.mock_calls[0][2].get("connect_args", {}) + for key, value in expected_connect_args.items(): + assert connect_args[key] == value @pytest.mark.parametrize( @@ -1771,4 +1774,4 @@ async def test_connect_args_priority(hass, config_url): } }, ) - assert connect_params == [{"charset": "utf8mb4"}] + assert connect_params[0]["charset"] == "utf8mb4" From acb147767366e73402a6ad4f9e8a053d26959110 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 13 Oct 2022 11:51:27 +0200 Subject: [PATCH 426/985] Avoid time traveling in recorder tests (#80247) --- tests/components/recorder/common.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index 8d1929c7362..0ddc76e4423 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import datetime import time from typing import Any, cast @@ -21,8 +21,6 @@ from homeassistant.util import dt as dt_util from . import db_schema_0 -from tests.common import async_fire_time_changed, fire_time_changed - DEFAULT_PURGE_TASKS = 3 @@ -69,9 +67,7 @@ def wait_recording_done(hass: HomeAssistant) -> None: def trigger_db_commit(hass: HomeAssistant) -> None: """Force the recorder to commit.""" - for _ in range(recorder.DEFAULT_COMMIT_INTERVAL): - # We only commit on time change - fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) + recorder.get_instance(hass)._async_commit(dt_util.utcnow()) async def async_wait_recording_done(hass: HomeAssistant) -> None: @@ -100,8 +96,7 @@ async def async_wait_purge_done(hass: HomeAssistant, max: int = None) -> None: @ha.callback def async_trigger_db_commit(hass: HomeAssistant) -> None: """Force the recorder to commit. Async friendly.""" - for _ in range(recorder.DEFAULT_COMMIT_INTERVAL): - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) + recorder.get_instance(hass)._async_commit(dt_util.utcnow()) async def async_recorder_block_till_done(hass: HomeAssistant) -> None: From 04cc2ae264c6ed43482e24ccf3cae24d191122f9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 13 Oct 2022 13:01:27 +0200 Subject: [PATCH 427/985] Correct initialization of new databases (#80234) --- homeassistant/components/recorder/core.py | 2 +- .../components/recorder/migration.py | 46 +++++++++++-------- tests/components/recorder/test_init.py | 2 +- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index f7d2b774aeb..d5e095d8104 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -703,7 +703,7 @@ class Recorder(threading.Thread): while tries <= self.db_max_retries: try: self._setup_connection() - return True + return migration.initialize_database(self.get_session) except UnsupportedDialect: break except Exception as err: # pylint: disable=broad-except diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 227500aaf0f..22a3b382c7d 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -6,7 +6,7 @@ import contextlib from dataclasses import dataclass from datetime import timedelta import logging -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING import sqlalchemy from sqlalchemy import ForeignKeyConstraint, MetaData, Table, func, text @@ -62,24 +62,17 @@ def raise_if_exception_missing_str(ex: Exception, match_substrs: Iterable[str]) raise ex +def _get_schema_version(session: Session) -> int | None: + """Get the schema version.""" + res = session.query(SchemaChanges).order_by(SchemaChanges.change_id.desc()).first() + return getattr(res, "schema_version", None) + + def get_schema_version(session_maker: Callable[[], Session]) -> int | None: """Get the schema version.""" try: with session_scope(session=session_maker()) as session: - res = ( - session.query(SchemaChanges) - .order_by(SchemaChanges.change_id.desc()) - .first() - ) - current_version = getattr(res, "schema_version", None) - - if current_version is None: - current_version = _inspect_schema_version(session) - _LOGGER.debug( - "No schema version found. Inspected version: %s", current_version - ) - - return cast(int, current_version) + return _get_schema_version(session) except Exception as err: # pylint: disable=broad-except _LOGGER.exception("Error when determining DB schema version: %s", err) return None @@ -797,8 +790,10 @@ def _apply_update( # noqa: C901 raise ValueError(f"No schema migration defined for version {new_version}") -def _inspect_schema_version(session: Session) -> int: - """Determine the schema version by inspecting the db structure. +def _initialize_database(session: Session) -> bool: + """Initialize a new database, or a database created before introducing schema changes. + + The function determines the schema version by inspecting the db structure. When the schema version is not present in the db, either db was just created with the correct schema, or this is a db created before schema @@ -814,9 +809,22 @@ def _inspect_schema_version(session: Session) -> int: # Schema addition from version 1 detected. New DB. session.add(StatisticsRuns(start=get_start_time())) session.add(SchemaChanges(schema_version=SCHEMA_VERSION)) - return SCHEMA_VERSION + return True # Version 1 schema changes not found, this db needs to be migrated. current_version = SchemaChanges(schema_version=0) session.add(current_version) - return cast(int, current_version.schema_version) + return True + + +def initialize_database(session_maker: Callable[[], Session]) -> bool: + """Initialize a new database, or a database created before introducing schema changes.""" + try: + with session_scope(session=session_maker()) as session: + if _get_schema_version(session) is not None: + return True + return _initialize_database(session) + + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Error when initialise database: %s", err) + return False diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 54e82516373..9939fc7fb46 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -669,7 +669,7 @@ def test_recorder_validate_schema_failure(hass): """Test some exceptions.""" recorder_helper.async_initialize_recorder(hass) with patch( - "homeassistant.components.recorder.migration._inspect_schema_version" + "homeassistant.components.recorder.migration._get_schema_version" ) as inspect_schema_version, patch( "homeassistant.components.recorder.core.time.sleep" ): From d80c0ddb5f1ed0265d10314b587bf7caa9823468 Mon Sep 17 00:00:00 2001 From: rappenze Date: Thu, 13 Oct 2022 13:51:30 +0200 Subject: [PATCH 428/985] Fix armed state in fibaro integration (#80218) * Fix armed state in fibaro integration * Update homeassistant/components/fibaro/__init__.py Co-authored-by: Joakim Plate Co-authored-by: Joakim Plate --- homeassistant/components/fibaro/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 08ee4658107..3661721810b 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -651,7 +651,13 @@ class FibaroDevice(Entity): self.fibaro_device.properties.batteryLevel ) if "armed" in self.fibaro_device.properties: - attr[ATTR_ARMED] = self.fibaro_device.properties.armed.lower() == "true" + armed = self.fibaro_device.properties.armed + if isinstance(armed, bool): + attr[ATTR_ARMED] = armed + elif isinstance(armed, str) and armed.lower() in ("true", "false"): + attr[ATTR_ARMED] = armed.lower() == "true" + else: + attr[ATTR_ARMED] = None except (ValueError, KeyError): pass From eae96eb4c22936b94e67b95eaa3a0e5083793b54 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 13 Oct 2022 07:31:33 -0600 Subject: [PATCH 429/985] Add diagnostics to AirNow (#79904) --- .../components/airnow/diagnostics.py | 53 +++++++++++++++++++ tests/components/airnow/test_diagnostics.py | 43 +++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 homeassistant/components/airnow/diagnostics.py create mode 100644 tests/components/airnow/test_diagnostics.py diff --git a/homeassistant/components/airnow/diagnostics.py b/homeassistant/components/airnow/diagnostics.py new file mode 100644 index 00000000000..284fd65013b --- /dev/null +++ b/homeassistant/components/airnow/diagnostics.py @@ -0,0 +1,53 @@ +"""Diagnostics support for AirNow.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_UNIQUE_ID, +) +from homeassistant.core import HomeAssistant + +from . import AirNowDataUpdateCoordinator +from .const import DOMAIN + +ATTR_LATITUDE_CAP = "Latitude" +ATTR_LONGITUDE_CAP = "Longitude" +ATTR_REPORTING_AREA = "ReportingArea" +ATTR_STATE_CODE = "StateCode" + +CONF_TITLE = "title" + +TO_REDACT = { + ATTR_LATITUDE_CAP, + ATTR_LONGITUDE_CAP, + ATTR_REPORTING_AREA, + ATTR_STATE_CODE, + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + # The config entry title has latitude/longitude: + CONF_TITLE, + # The config entry unique ID has latitude/longitude: + CONF_UNIQUE_ID, +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: AirNowDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + return async_redact_data( + { + "entry": entry.as_dict(), + "data": coordinator.data, + }, + TO_REDACT, + ) diff --git a/tests/components/airnow/test_diagnostics.py b/tests/components/airnow/test_diagnostics.py new file mode 100644 index 00000000000..76a8a1dc0b2 --- /dev/null +++ b/tests/components/airnow/test_diagnostics.py @@ -0,0 +1,43 @@ +"""Test AirNow diagnostics.""" +from homeassistant.components.diagnostics import REDACTED + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_entry_diagnostics(hass, config_entry, hass_client, setup_airnow): + """Test config entry diagnostics.""" + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { + "entry": { + "entry_id": config_entry.entry_id, + "version": 1, + "domain": "airnow", + "title": REDACTED, + "data": { + "api_key": REDACTED, + "latitude": REDACTED, + "longitude": REDACTED, + "radius": 75, + }, + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": REDACTED, + "disabled_by": None, + }, + "data": { + "O3": 0.048, + "PM2.5": 8.9, + "HourObserved": 15, + "DateObserved": "2020-12-20", + "StateCode": REDACTED, + "ReportingArea": REDACTED, + "Latitude": REDACTED, + "Longitude": REDACTED, + "PM10": 12, + "AQI": 44, + "Category.Number": 1, + "Category.Name": "Good", + "Pollutant": "O3", + }, + } From 50207a8ca80e63ed4572021e5a0d0c17d931e6a5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Oct 2022 17:00:44 +0200 Subject: [PATCH 430/985] Adjust temperature unit check in rainmachine (#80237) * Adjust temperature unit check in rainmachine * Use system compare * Use is not == --- homeassistant/components/rainmachine/select.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rainmachine/select.py b/homeassistant/components/rainmachine/select.py index 33a0a38ed15..12860e59e79 100644 --- a/homeassistant/components/rainmachine/select.py +++ b/homeassistant/components/rainmachine/select.py @@ -7,11 +7,11 @@ from regenmaschine.errors import RainMachineError from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, UnitSystem from . import RainMachineData, RainMachineEntity from .const import DATA_RESTRICTIONS_UNIVERSAL, DOMAIN @@ -101,7 +101,7 @@ async def async_setup_entry( } async_add_entities( - entity_map[description.key](entry, data, description, hass.config.units.name) + entity_map[description.key](entry, data, description, hass.config.units) for description in SELECT_DESCRIPTIONS if ( (coordinator := data.coordinators[description.api_category]) is not None @@ -121,7 +121,7 @@ class FreezeProtectionTemperatureSelect(RainMachineEntity, SelectEntity): entry: ConfigEntry, data: RainMachineData, description: FreezeProtectionSelectDescription, - unit_system: str, + unit_system: UnitSystem, ) -> None: """Initialize.""" super().__init__(entry, data, description) @@ -130,7 +130,7 @@ class FreezeProtectionTemperatureSelect(RainMachineEntity, SelectEntity): self._label_to_api_value_map = {} for option in description.extended_options: - if unit_system == CONF_UNIT_SYSTEM_IMPERIAL: + if unit_system is IMPERIAL_SYSTEM: label = option.imperial_label else: label = option.metric_label From e1ac8acf87144ece16020d925f5b363422740b91 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 13 Oct 2022 17:32:53 +0200 Subject: [PATCH 431/985] Bump CI cache version (#80265) --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c564acf9b8c..1ee60c6b029 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,8 +20,8 @@ on: type: boolean env: - CACHE_VERSION: 1 - PIP_CACHE_VERSION: 1 + CACHE_VERSION: 2 + PIP_CACHE_VERSION: 2 HA_SHORT_VERSION: 2022.11 DEFAULT_PYTHON: 3.9 ALL_PYTHON_VERSIONS: "['3.9', '3.10']" From e852c9b012f2f949cc08e9498b8a051f362669e9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 13 Oct 2022 17:34:45 +0200 Subject: [PATCH 432/985] Fix logbook tests (#80264) * Fix logbook tests * Correct tests * Improve tests --- .../components/logbook/test_websocket_api.py | 95 +++++++++++-------- 1 file changed, 55 insertions(+), 40 deletions(-) diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index a7bd28f0e4d..ec27b1baceb 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -528,7 +528,6 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities( }, ) await hass.async_block_till_done() - init_count = sum(hass.bus.async_listeners().values()) hass.states.async_set("light.exc", STATE_ON) hass.states.async_set("light.exc", STATE_OFF) @@ -544,6 +543,7 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities( await async_wait_recording_done(hass) websocket_client = await hass_ws_client() + init_listeners = hass.bus.async_listeners() await websocket_client.send_json( {"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()} ) @@ -684,7 +684,7 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities( assert msg["success"] # Check our listener got unsubscribed - assert sum(hass.bus.async_listeners().values()) == init_count + assert hass.bus.async_listeners() == init_listeners @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -722,7 +722,6 @@ async def test_subscribe_unsubscribe_logbook_stream_included_entities( }, ) await hass.async_block_till_done() - init_count = sum(hass.bus.async_listeners().values()) for entity_id in test_entities: hass.states.async_set(entity_id, STATE_ON) @@ -732,6 +731,7 @@ async def test_subscribe_unsubscribe_logbook_stream_included_entities( await async_wait_recording_done(hass) websocket_client = await hass_ws_client() + init_listeners = hass.bus.async_listeners() await websocket_client.send_json( {"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()} ) @@ -892,7 +892,7 @@ async def test_subscribe_unsubscribe_logbook_stream_included_entities( assert msg["success"] # Check our listener got unsubscribed - assert sum(hass.bus.async_listeners().values()) == init_count + assert hass.bus.async_listeners() == init_listeners @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -926,7 +926,6 @@ async def test_logbook_stream_excluded_entities_inherits_filters_from_recorder( }, ) await hass.async_block_till_done() - init_count = sum(hass.bus.async_listeners().values()) hass.states.async_set("light.exc", STATE_ON) hass.states.async_set("light.exc", STATE_OFF) @@ -943,6 +942,7 @@ async def test_logbook_stream_excluded_entities_inherits_filters_from_recorder( await async_wait_recording_done(hass) websocket_client = await hass_ws_client() + init_listeners = hass.bus.async_listeners() await websocket_client.send_json( {"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()} ) @@ -1083,7 +1083,7 @@ async def test_logbook_stream_excluded_entities_inherits_filters_from_recorder( assert msg["success"] # Check our listener got unsubscribed - assert sum(hass.bus.async_listeners().values()) == init_count + assert hass.bus.async_listeners() == init_listeners @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1100,7 +1100,6 @@ async def test_subscribe_unsubscribe_logbook_stream( ) await hass.async_block_till_done() - init_count = sum(hass.bus.async_listeners().values()) hass.states.async_set("binary_sensor.is_light", STATE_ON) hass.states.async_set("binary_sensor.is_light", STATE_OFF) @@ -1109,6 +1108,7 @@ async def test_subscribe_unsubscribe_logbook_stream( await async_wait_recording_done(hass) websocket_client = await hass_ws_client() + init_listeners = hass.bus.async_listeners() await websocket_client.send_json( {"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()} ) @@ -1386,7 +1386,7 @@ async def test_subscribe_unsubscribe_logbook_stream( assert msg["success"] # Check our listener got unsubscribed - assert sum(hass.bus.async_listeners().values()) == init_count + assert hass.bus.async_listeners() == init_listeners @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1403,7 +1403,6 @@ async def test_subscribe_unsubscribe_logbook_stream_entities( ) await hass.async_block_till_done() - init_count = sum(hass.bus.async_listeners().values()) hass.states.async_set("light.small", STATE_ON) hass.states.async_set("binary_sensor.is_light", STATE_ON) hass.states.async_set("binary_sensor.is_light", STATE_OFF) @@ -1412,6 +1411,7 @@ async def test_subscribe_unsubscribe_logbook_stream_entities( await async_wait_recording_done(hass) websocket_client = await hass_ws_client() + init_listeners = hass.bus.async_listeners() await websocket_client.send_json( { "id": 7, @@ -1484,7 +1484,7 @@ async def test_subscribe_unsubscribe_logbook_stream_entities( assert msg["success"] # Check our listener got unsubscribed - assert sum(hass.bus.async_listeners().values()) == init_count + assert hass.bus.async_listeners() == init_listeners @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1501,7 +1501,6 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_with_end_time( ) await hass.async_block_till_done() - init_count = sum(hass.bus.async_listeners().values()) hass.states.async_set("light.small", STATE_ON) hass.states.async_set("binary_sensor.is_light", STATE_ON) hass.states.async_set("binary_sensor.is_light", STATE_OFF) @@ -1510,6 +1509,7 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_with_end_time( await async_wait_recording_done(hass) websocket_client = await hass_ws_client() + init_listeners = hass.bus.async_listeners() await websocket_client.send_json( { "id": 7, @@ -1586,7 +1586,12 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_with_end_time( assert msg["success"] # Check our listener got unsubscribed - assert sum(hass.bus.async_listeners().values()) <= init_count + listeners = hass.bus.async_listeners() + # The async_fire_time_changed above triggers unsubscribe from + # homeassistant_final_write, don't worry about those + init_listeners.pop("homeassistant_final_write") + listeners.pop("homeassistant_final_write") + assert listeners == init_listeners @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1603,7 +1608,6 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_past_only( ) await hass.async_block_till_done() - init_count = sum(hass.bus.async_listeners().values()) hass.states.async_set("light.small", STATE_ON) hass.states.async_set("binary_sensor.is_light", STATE_ON) hass.states.async_set("binary_sensor.is_light", STATE_OFF) @@ -1612,6 +1616,7 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_past_only( await async_wait_recording_done(hass) websocket_client = await hass_ws_client() + init_listeners = hass.bus.async_listeners() await websocket_client.send_json( { "id": 7, @@ -1654,7 +1659,7 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_past_only( assert msg["success"] # Check our listener got unsubscribed - assert sum(hass.bus.async_listeners().values()) == init_count + assert hass.bus.async_listeners() == init_listeners @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1675,7 +1680,6 @@ async def test_subscribe_unsubscribe_logbook_stream_big_query( ) await hass.async_block_till_done() - init_count = sum(hass.bus.async_listeners().values()) four_days_ago = now - timedelta(days=4) five_days_ago = now - timedelta(days=5) @@ -1699,6 +1703,7 @@ async def test_subscribe_unsubscribe_logbook_stream_big_query( await async_wait_recording_done(hass) websocket_client = await hass_ws_client() + init_listeners = hass.bus.async_listeners() await websocket_client.send_json( { "id": 7, @@ -1754,7 +1759,7 @@ async def test_subscribe_unsubscribe_logbook_stream_big_query( assert msg["success"] # Check our listener got unsubscribed - assert sum(hass.bus.async_listeners().values()) == init_count + assert hass.bus.async_listeners() == init_listeners @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1774,10 +1779,10 @@ async def test_subscribe_unsubscribe_logbook_stream_device( device2 = devices[1] await hass.async_block_till_done() - init_count = sum(hass.bus.async_listeners().values()) await async_wait_recording_done(hass) websocket_client = await hass_ws_client() + init_listeners = hass.bus.async_listeners() await websocket_client.send_json( { "id": 7, @@ -1848,7 +1853,7 @@ async def test_subscribe_unsubscribe_logbook_stream_device( assert msg["success"] # Check our listener got unsubscribed - assert sum(hass.bus.async_listeners().values()) == init_count + assert hass.bus.async_listeners() == init_listeners async def test_event_stream_bad_start_time(hass, hass_ws_client, recorder_mock): @@ -1886,10 +1891,10 @@ async def test_logbook_stream_match_multiple_entities( hass.states.async_set(entity_id, STATE_ON) await hass.async_block_till_done() - init_count = sum(hass.bus.async_listeners().values()) await async_wait_recording_done(hass) websocket_client = await hass_ws_client() + init_listeners = hass.bus.async_listeners() await websocket_client.send_json( { "id": 7, @@ -1963,7 +1968,7 @@ async def test_logbook_stream_match_multiple_entities( assert msg["success"] # Check our listener got unsubscribed - assert sum(hass.bus.async_listeners().values()) == init_count + assert hass.bus.async_listeners() == init_listeners async def test_event_stream_bad_end_time(hass, hass_ws_client, recorder_mock): @@ -2017,7 +2022,6 @@ async def test_live_stream_with_one_second_commit_interval( device = devices[0] await hass.async_block_till_done() - init_count = sum(hass.bus.async_listeners().values()) hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "1"}) @@ -2030,6 +2034,7 @@ async def test_live_stream_with_one_second_commit_interval( hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "3"}) websocket_client = await hass_ws_client() + init_listeners = hass.bus.async_listeners() await websocket_client.send_json( { "id": 7, @@ -2086,7 +2091,7 @@ async def test_live_stream_with_one_second_commit_interval( assert msg["success"] # Check our listener got unsubscribed - assert sum(hass.bus.async_listeners().values()) == init_count + assert hass.bus.async_listeners() == init_listeners @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2101,7 +2106,6 @@ async def test_subscribe_disconnected(hass, recorder_mock, hass_ws_client): ) await async_wait_recording_done(hass) - init_count = sum(hass.bus.async_listeners().values()) hass.states.async_set("light.small", STATE_ON) hass.states.async_set("binary_sensor.is_light", STATE_ON) hass.states.async_set("binary_sensor.is_light", STATE_OFF) @@ -2109,6 +2113,9 @@ async def test_subscribe_disconnected(hass, recorder_mock, hass_ws_client): await hass.async_block_till_done() await async_wait_recording_done(hass) + # We will compare event subscriptions after closing the websocket connection, + # count the listeners before setting it up + init_listeners = hass.bus.async_listeners() websocket_client = await hass_ws_client() await websocket_client.send_json( { @@ -2139,7 +2146,7 @@ async def test_subscribe_disconnected(hass, recorder_mock, hass_ws_client): await hass.async_block_till_done() # Check our listener got unsubscribed - assert sum(hass.bus.async_listeners().values()) == init_count + assert hass.bus.async_listeners() == init_listeners @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2153,7 +2160,7 @@ async def test_stream_consumer_stop_processing(hass, recorder_mock, hass_ws_clie ] ) await async_wait_recording_done(hass) - init_count = sum(hass.bus.async_listeners().values()) + init_listeners = hass.bus.async_listeners() hass.states.async_set("light.small", STATE_ON) hass.states.async_set("binary_sensor.is_light", STATE_ON) hass.states.async_set("binary_sensor.is_light", STATE_OFF) @@ -2162,7 +2169,7 @@ async def test_stream_consumer_stop_processing(hass, recorder_mock, hass_ws_clie await async_wait_recording_done(hass) websocket_client = await hass_ws_client() - after_ws_created_count = sum(hass.bus.async_listeners().values()) + after_ws_created_listeners = hass.bus.async_listeners() with patch.object(websocket_api, "MAX_PENDING_LOGBOOK_EVENTS", 5), patch.object( websocket_api, "_async_events_consumer" @@ -2182,7 +2189,7 @@ async def test_stream_consumer_stop_processing(hass, recorder_mock, hass_ws_clie assert msg["type"] == TYPE_RESULT assert msg["success"] - assert sum(hass.bus.async_listeners().values()) != init_count + assert hass.bus.async_listeners() != init_listeners for _ in range(5): hass.states.async_set("binary_sensor.is_light", STATE_ON) hass.states.async_set("binary_sensor.is_light", STATE_OFF) @@ -2190,9 +2197,9 @@ async def test_stream_consumer_stop_processing(hass, recorder_mock, hass_ws_clie # Check our listener got unsubscribed because # the queue got full and the overload safety tripped - assert sum(hass.bus.async_listeners().values()) == after_ws_created_count + assert hass.bus.async_listeners() == after_ws_created_listeners await websocket_client.close() - assert sum(hass.bus.async_listeners().values()) == init_count + assert hass.bus.async_listeners() == init_listeners @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2294,7 +2301,9 @@ async def test_subscribe_all_entities_are_continuous( hass.states.async_set("counter.any", state) hass.states.async_set("proximity.any", state) - init_count = sum(hass.bus.async_listeners().values()) + # We will compare event subscriptions after closing the websocket connection, + # count the listeners before setting it up + init_listeners = hass.bus.async_listeners() _cycle_entities() await async_wait_recording_done(hass) @@ -2323,7 +2332,7 @@ async def test_subscribe_all_entities_are_continuous( await hass.async_block_till_done() # Check our listener got unsubscribed - assert sum(hass.bus.async_listeners().values()) == init_count + assert hass.bus.async_listeners() == init_listeners @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2348,7 +2357,9 @@ async def test_subscribe_all_entities_have_uom_multiple( entity_id, state, {ATTR_UNIT_OF_MEASUREMENT: "any"} ) - init_count = sum(hass.bus.async_listeners().values()) + # We will compare event subscriptions after closing the websocket connection, + # count the listeners before setting it up + init_listeners = hass.bus.async_listeners() _cycle_entities() await async_wait_recording_done(hass) @@ -2378,14 +2389,14 @@ async def test_subscribe_all_entities_have_uom_multiple( await hass.async_block_till_done() # Check our listener got unsubscribed - assert sum(hass.bus.async_listeners().values()) == init_count + assert hass.bus.async_listeners() == init_listeners @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) async def test_subscribe_entities_some_have_uom_multiple( hass, recorder_mock, hass_ws_client ): - """Test logbook stream with uom filtered entities and non-fitlered entities.""" + """Test logbook stream with uom filtered entities and non-filtered entities.""" now = dt_util.utcnow() await asyncio.gather( *[ @@ -2407,7 +2418,9 @@ async def test_subscribe_entities_some_have_uom_multiple( for state in (STATE_ON, STATE_OFF): hass.states.async_set(entity_id, state) - init_count = sum(hass.bus.async_listeners().values()) + # We will compare event subscriptions after closing the websocket connection, + # count the listeners before setting it up + init_listeners = hass.bus.async_listeners() _cycle_entities() await async_wait_recording_done(hass) @@ -2481,7 +2494,7 @@ async def test_subscribe_entities_some_have_uom_multiple( await hass.async_block_till_done() # Check our listener got unsubscribed - assert sum(hass.bus.async_listeners().values()) == init_count + assert hass.bus.async_listeners() == init_listeners @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2498,7 +2511,6 @@ async def test_logbook_stream_ignores_forced_updates( ) await hass.async_block_till_done() - init_count = sum(hass.bus.async_listeners().values()) hass.states.async_set("binary_sensor.is_light", STATE_ON) hass.states.async_set("binary_sensor.is_light", STATE_OFF) @@ -2507,6 +2519,7 @@ async def test_logbook_stream_ignores_forced_updates( await async_wait_recording_done(hass) websocket_client = await hass_ws_client() + init_listeners = hass.bus.async_listeners() await websocket_client.send_json( {"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()} ) @@ -2595,7 +2608,7 @@ async def test_logbook_stream_ignores_forced_updates( assert msg["success"] # Check our listener got unsubscribed - assert sum(hass.bus.async_listeners().values()) == init_count + assert hass.bus.async_listeners() == init_listeners @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2628,7 +2641,9 @@ async def test_subscribe_all_entities_are_continuous_with_device( hass.bus.async_fire("mock_event", {"device_id": device.id}) hass.bus.async_fire("mock_event", {"device_id": device2.id}) - init_count = sum(hass.bus.async_listeners().values()) + # We will compare event subscriptions after closing the websocket connection, + # count the listeners before setting it up + init_listeners = hass.bus.async_listeners() _create_events() await async_wait_recording_done(hass) @@ -2688,4 +2703,4 @@ async def test_subscribe_all_entities_are_continuous_with_device( await hass.async_block_till_done() # Check our listener got unsubscribed - assert sum(hass.bus.async_listeners().values()) == init_count + assert hass.bus.async_listeners() == init_listeners From 757df213e0b7c0c727132fb95f5ebe3ac074ea5c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 13 Oct 2022 18:50:00 +0200 Subject: [PATCH 433/985] Drop use of `is_metric` in tomorrowio (#80271) --- homeassistant/components/tomorrowio/sensor.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py index b2179cd60f5..1f3bb74b686 100644 --- a/homeassistant/components/tomorrowio/sensor.py +++ b/homeassistant/components/tomorrowio/sensor.py @@ -39,6 +39,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify from homeassistant.util.unit_conversion import DistanceConverter, SpeedConverter +from homeassistant.util.unit_system import IMPERIAL_SYSTEM from . import TomorrowioDataUpdateCoordinator, TomorrowioEntity from .const import ( @@ -327,11 +328,9 @@ class BaseTomorrowioSensorEntity(TomorrowioEntity, SensorEntity): ) self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: self.attribution} if self.entity_description.native_unit_of_measurement is None: - self._attr_native_unit_of_measurement = ( - description.unit_metric - if hass.config.units.is_metric - else description.unit_imperial - ) + self._attr_native_unit_of_measurement = description.unit_metric + if hass.config.units is IMPERIAL_SYSTEM: + self._attr_native_unit_of_measurement = description.unit_imperial @property @abstractmethod @@ -359,7 +358,7 @@ class BaseTomorrowioSensorEntity(TomorrowioEntity, SensorEntity): desc.imperial_conversion and desc.unit_imperial is not None and desc.unit_imperial != desc.unit_metric - and not self.hass.config.units.is_metric + and self.hass.config.units is IMPERIAL_SYSTEM ): return handle_conversion(state, desc.imperial_conversion) From 3379e144177cd3cca02fda5360177d66f6aa84e3 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 13 Oct 2022 19:31:59 +0200 Subject: [PATCH 434/985] =?UTF-8?q?Bump=20M=C3=A9t=C3=A9o-France=20to=201.?= =?UTF-8?q?1.0=20(#80255)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/meteo_france/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/pip_check | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/meteo_france/manifest.json b/homeassistant/components/meteo_france/manifest.json index cfdd62933c0..5a88275ba6a 100644 --- a/homeassistant/components/meteo_france/manifest.json +++ b/homeassistant/components/meteo_france/manifest.json @@ -3,7 +3,7 @@ "name": "M\u00e9t\u00e9o-France", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/meteo_france", - "requirements": ["meteofrance-api==1.0.2"], + "requirements": ["meteofrance-api==1.1.0"], "codeowners": ["@hacf-fr", "@oncleben31", "@Quentame"], "iot_class": "cloud_polling", "loggers": ["meteofrance_api"] diff --git a/requirements_all.txt b/requirements_all.txt index d111949e8ae..7e03e0015b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1072,7 +1072,7 @@ messagebird==1.2.0 meteoalertapi==0.3.0 # homeassistant.components.meteo_france -meteofrance-api==1.0.2 +meteofrance-api==1.1.0 # homeassistant.components.mfi mficlient==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e5e6894b928..f834e6eb683 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -774,7 +774,7 @@ meater-python==0.0.8 melnor-bluetooth==0.0.20 # homeassistant.components.meteo_france -meteofrance-api==1.0.2 +meteofrance-api==1.1.0 # homeassistant.components.mfi mficlient==0.3.0 diff --git a/script/pip_check b/script/pip_check index ae780b07d60..9ed327b54f4 100755 --- a/script/pip_check +++ b/script/pip_check @@ -3,7 +3,7 @@ PIP_CACHE=$1 # Number of existing dependency conflicts # Update if a PR resolve one! -DEPENDENCY_CONFLICTS=4 +DEPENDENCY_CONFLICTS=3 PIP_CHECK=$(pip check --cache-dir=$PIP_CACHE) LINE_COUNT=$(echo "$PIP_CHECK" | wc -l) From d87f433be7c114cee29068ce408a1f5498dacb59 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 13 Oct 2022 22:44:47 +0200 Subject: [PATCH 435/985] Bump Freebox to 1.0.0 (#80256) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/freebox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/freebox/manifest.json b/homeassistant/components/freebox/manifest.json index 846bff5f8ce..a6d21bb635f 100644 --- a/homeassistant/components/freebox/manifest.json +++ b/homeassistant/components/freebox/manifest.json @@ -3,7 +3,7 @@ "name": "Freebox", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/freebox", - "requirements": ["freebox-api==0.0.10"], + "requirements": ["freebox-api==1.0.0"], "zeroconf": ["_fbx-api._tcp.local."], "codeowners": ["@hacf-fr", "@Quentame"], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 7e03e0015b7..307f51d59c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -715,7 +715,7 @@ forecast_solar==2.2.0 fortiosapi==1.0.5 # homeassistant.components.freebox -freebox-api==0.0.10 +freebox-api==1.0.0 # homeassistant.components.free_mobile freesms==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f834e6eb683..a58f7df0ec7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -534,7 +534,7 @@ foobot_async==1.0.0 forecast_solar==2.2.0 # homeassistant.components.freebox -freebox-api==0.0.10 +freebox-api==1.0.0 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor From e721d8ed02c6b559136f35f4a05e03a8ab102a4b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 13 Oct 2022 22:49:10 +0200 Subject: [PATCH 436/985] Bump actions/cache from 3.0.10 to 3.0.11 (#80260) --- .github/workflows/ci.yaml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1ee60c6b029..a4952a6376a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -176,7 +176,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: venv key: >- @@ -191,7 +191,7 @@ jobs: pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -220,7 +220,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: venv key: >- @@ -233,7 +233,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -274,7 +274,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: venv key: >- @@ -287,7 +287,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -331,7 +331,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: venv key: >- @@ -344,7 +344,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -377,7 +377,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: venv key: >- @@ -390,7 +390,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -509,7 +509,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: venv key: >- @@ -517,7 +517,7 @@ jobs: needs.info.outputs.python_cache_key }} - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: ${{ env.PIP_CACHE }} key: >- @@ -568,7 +568,7 @@ jobs: check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: venv key: >- @@ -601,7 +601,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: venv key: >- @@ -635,7 +635,7 @@ jobs: check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: venv key: >- @@ -680,7 +680,7 @@ jobs: check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: venv key: >- @@ -729,7 +729,7 @@ jobs: check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: venv key: >- @@ -784,7 +784,7 @@ jobs: check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ From 180b296426e9ea77374bc7a585b3d86fa11edff1 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 13 Oct 2022 22:18:57 +0100 Subject: [PATCH 437/985] IPMA Code quality improvement (#77771) * merge upstream/dev * remove comment * coverage increase * merge upstream/dev * refactor * wait for another PR * remove left overs * wait for next PR * only remove on successful unload Co-authored-by: Shay Levy Co-authored-by: Shay Levy --- homeassistant/components/ipma/__init__.py | 9 +- homeassistant/components/ipma/const.py | 6 +- homeassistant/components/ipma/weather.py | 15 +-- tests/components/ipma/__init__.py | 110 +++++++++++++++++++ tests/components/ipma/test_config_flow.py | 11 +- tests/components/ipma/test_init.py | 57 ++++++++++ tests/components/ipma/test_weather.py | 127 +--------------------- 7 files changed, 184 insertions(+), 151 deletions(-) create mode 100644 tests/components/ipma/test_init.py diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index eec16a0c811..866e79cbe40 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -57,4 +57,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + + return unload_ok diff --git a/homeassistant/components/ipma/const.py b/homeassistant/components/ipma/const.py index 60c8115a5c4..515fb501fbd 100644 --- a/homeassistant/components/ipma/const.py +++ b/homeassistant/components/ipma/const.py @@ -5,7 +5,7 @@ DOMAIN = "ipma" HOME_LOCATION_NAME = "Home" -ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.ipma_{HOME_LOCATION_NAME}" - -DATA_LOCATION = "location" DATA_API = "api" +DATA_LOCATION = "location" + +ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.ipma_{HOME_LOCATION_NAME}" diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index a0fe5b235b3..c448fad592d 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -8,7 +8,6 @@ import async_timeout from pyipma.api import IPMA_API from pyipma.forecast import Forecast from pyipma.location import Location -import voluptuous as vol from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, @@ -33,13 +32,10 @@ from homeassistant.components.weather import ( ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - PLATFORM_SCHEMA, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_LATITUDE, - CONF_LONGITUDE, CONF_MODE, CONF_NAME, PRESSURE_HPA, @@ -47,7 +43,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers import entity_registry from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sun import is_up from homeassistant.util import Throttle @@ -80,15 +76,6 @@ CONDITION_CLASSES = { FORECAST_MODE = ["hourly", "daily"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_MODE, default="daily"): vol.In(FORECAST_MODE), - } -) - async def async_setup_entry( hass: HomeAssistant, diff --git a/tests/components/ipma/__init__.py b/tests/components/ipma/__init__.py index 35099c405bb..4a002140437 100644 --- a/tests/components/ipma/__init__.py +++ b/tests/components/ipma/__init__.py @@ -1 +1,111 @@ """Tests for the IPMA component.""" +from collections import namedtuple +from datetime import datetime, timezone + +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME + +ENTRY_CONFIG = { + CONF_NAME: "Home Town", + CONF_LATITUDE: "1", + CONF_LONGITUDE: "2", + CONF_MODE: "hourly", +} + + +class MockLocation: + """Mock Location from pyipma.""" + + async def observation(self, api): + """Mock Observation.""" + Observation = namedtuple( + "Observation", + [ + "accumulated_precipitation", + "humidity", + "pressure", + "radiation", + "temperature", + "wind_direction", + "wind_intensity_km", + ], + ) + + return Observation(0.0, 71.0, 1000.0, 0.0, 18.0, "NW", 3.94) + + async def forecast(self, api, period): + """Mock Forecast.""" + Forecast = namedtuple( + "Forecast", + [ + "feels_like_temperature", + "forecast_date", + "forecasted_hours", + "humidity", + "max_temperature", + "min_temperature", + "precipitation_probability", + "temperature", + "update_date", + "weather_type", + "wind_direction", + "wind_strength", + ], + ) + + WeatherType = namedtuple("WeatherType", ["id", "en", "pt"]) + + if period == 24: + return [ + Forecast( + None, + datetime(2020, 1, 16, 0, 0, 0), + 24, + None, + 16.2, + 10.6, + "100.0", + 13.4, + "2020-01-15T07:51:00", + WeatherType(9, "Rain/showers", "Chuva/aguaceiros"), + "S", + "10", + ), + ] + if period == 1: + return [ + Forecast( + "7.7", + datetime(2020, 1, 15, 1, 0, 0, tzinfo=timezone.utc), + 1, + "86.9", + 12.0, + None, + 80.0, + 10.6, + "2020-01-15T02:51:00", + WeatherType(10, "Light rain", "Chuva fraca ou chuvisco"), + "S", + "32.7", + ), + Forecast( + "5.7", + datetime(2020, 1, 15, 2, 0, 0, tzinfo=timezone.utc), + 1, + "86.9", + 12.0, + None, + 80.0, + 10.6, + "2020-01-15T02:51:00", + WeatherType(1, "Clear sky", "C\u00e9u limpo"), + "S", + "32.7", + ), + ] + + name = "HomeTown" + station = "HomeTown Station" + station_latitude = 0 + station_longitude = 0 + global_id_local = 1130600 + id_station = 1200545 diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index ea4b0b510e7..e254ba402fb 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -3,21 +3,14 @@ from unittest.mock import Mock, patch from homeassistant.components.ipma import DOMAIN, config_flow -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from .test_weather import MockLocation +from . import MockLocation from tests.common import MockConfigEntry, mock_registry -ENTRY_CONFIG = { - CONF_NAME: "Home Town", - CONF_LATITUDE: "1", - CONF_LONGITUDE: "2", - CONF_MODE: "hourly", -} - async def test_show_config_form(): """Test show configuration form.""" diff --git a/tests/components/ipma/test_init.py b/tests/components/ipma/test_init.py new file mode 100644 index 00000000000..8dd808b1b1b --- /dev/null +++ b/tests/components/ipma/test_init.py @@ -0,0 +1,57 @@ +"""Test the IPMA integration.""" + +from unittest.mock import patch + +from pyipma import IPMAException + +from homeassistant.components.ipma import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE + +from .test_weather import MockLocation + +from tests.common import MockConfigEntry + + +async def test_async_setup_raises_entry_not_ready(hass): + """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" + + with patch( + "pyipma.location.Location.get", side_effect=IPMAException("API unavailable") + ): + + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Home", + data={CONF_LATITUDE: 0, CONF_LONGITUDE: 0, CONF_MODE: "daily"}, + ) + + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_unload_config_entry(hass): + """Test entry unloading.""" + + with patch( + "pyipma.location.Location.get", + return_value=MockLocation(), + ): + config_entry = MockConfigEntry( + domain="ipma", + data={CONF_LATITUDE: 0, CONF_LONGITUDE: 0, CONF_MODE: "daily"}, + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index e129216730d..62450871ee8 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -1,6 +1,5 @@ """The tests for the IPMA weather component.""" -from collections import namedtuple -from datetime import datetime, timezone +from datetime import datetime from unittest.mock import patch from freezegun import freeze_time @@ -22,6 +21,8 @@ from homeassistant.components.weather import ( ) from homeassistant.const import STATE_UNKNOWN +from . import MockLocation + from tests.common import MockConfigEntry TEST_CONFIG = { @@ -39,128 +40,6 @@ TEST_CONFIG_HOURLY = { } -class MockLocation: - """Mock Location from pyipma.""" - - async def observation(self, api): - """Mock Observation.""" - Observation = namedtuple( - "Observation", - [ - "accumulated_precipitation", - "humidity", - "pressure", - "radiation", - "temperature", - "wind_direction", - "wind_intensity_km", - ], - ) - - return Observation(0.0, 71.0, 1000.0, 0.0, 18.0, "NW", 3.94) - - async def forecast(self, api, period): - """Mock Forecast.""" - Forecast = namedtuple( - "Forecast", - [ - "feels_like_temperature", - "forecast_date", - "forecasted_hours", - "humidity", - "max_temperature", - "min_temperature", - "precipitation_probability", - "temperature", - "update_date", - "weather_type", - "wind_direction", - "wind_strength", - ], - ) - - WeatherType = namedtuple("WeatherType", ["id", "en", "pt"]) - - if period == 24: - return [ - Forecast( - None, - datetime(2020, 1, 16, 0, 0, 0), - 24, - None, - 16.2, - 10.6, - "100.0", - 13.4, - "2020-01-15T07:51:00", - WeatherType(9, "Rain/showers", "Chuva/aguaceiros"), - "S", - "10", - ), - ] - if period == 1: - return [ - Forecast( - "7.7", - datetime(2020, 1, 15, 1, 0, 0, tzinfo=timezone.utc), - 1, - "86.9", - 12.0, - None, - 80.0, - 10.6, - "2020-01-15T02:51:00", - WeatherType(10, "Light rain", "Chuva fraca ou chuvisco"), - "S", - "32.7", - ), - Forecast( - "5.7", - datetime(2020, 1, 15, 2, 0, 0, tzinfo=timezone.utc), - 1, - "86.9", - 12.0, - None, - 80.0, - 10.6, - "2020-01-15T02:51:00", - WeatherType(1, "Clear sky", "C\u00e9u limpo"), - "S", - "32.7", - ), - ] - - @property - def name(self): - """Mock location.""" - return "HomeTown" - - @property - def station(self): - """Mock station.""" - return "HomeTown Station" - - @property - def station_latitude(self): - """Mock latitude.""" - return 0 - - @property - def global_id_local(self): - """Mock global identifier of the location.""" - return 1130600 - - @property - def id_station(self): - """Mock identifier of the station.""" - return 1200545 - - @property - def station_longitude(self): - """Mock longitude.""" - return 0 - - class MockBadLocation(MockLocation): """Mock Location with unresponsive api.""" From 5a51738b2f44b669f05cd366811caea00fe3978a Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 14 Oct 2022 00:20:13 +0300 Subject: [PATCH 438/985] Add Switcher runner support (#79430) * Add Switcher runner support * Retrigger docs check * Review suggestions * Move API strings to constants --- .../components/switcher_kis/__init__.py | 2 +- .../components/switcher_kis/cover.py | 130 +++++++++++++ tests/components/switcher_kis/consts.py | 19 ++ tests/components/switcher_kis/test_cover.py | 183 ++++++++++++++++++ 4 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/switcher_kis/cover.py create mode 100644 tests/components/switcher_kis/test_cover.py diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 890ec65dded..be8f140711a 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -29,7 +29,7 @@ from .const import ( ) from .utils import async_start_bridge, async_stop_bridge -PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.CLIMATE, Platform.COVER, Platform.SENSOR, Platform.SWITCH] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switcher_kis/cover.py b/homeassistant/components/switcher_kis/cover.py new file mode 100644 index 00000000000..584f3d7124f --- /dev/null +++ b/homeassistant/components/switcher_kis/cover.py @@ -0,0 +1,130 @@ +"""Switcher integration Cover platform.""" +from __future__ import annotations + +import asyncio +import logging +from typing import Any + +from aioswitcher.api import SwitcherBaseResponse, SwitcherType2Api +from aioswitcher.device import DeviceCategory, ShutterDirection, SwitcherShutter + +from homeassistant.components.cover import ( + ATTR_POSITION, + CoverDeviceClass, + CoverEntity, + CoverEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import SwitcherDataUpdateCoordinator +from .const import SIGNAL_DEVICE_ADD + +_LOGGER = logging.getLogger(__name__) + +API_SET_POSITON = "set_position" +API_STOP = "stop" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Switcher cover from config entry.""" + + @callback + def async_add_cover(coordinator: SwitcherDataUpdateCoordinator) -> None: + """Add cover from Switcher device.""" + if coordinator.data.device_type.category == DeviceCategory.SHUTTER: + async_add_entities([SwitcherCoverEntity(coordinator)]) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, SIGNAL_DEVICE_ADD, async_add_cover) + ) + + +class SwitcherCoverEntity( + CoordinatorEntity[SwitcherDataUpdateCoordinator], CoverEntity +): + """Representation of a Switcher cover entity.""" + + _attr_device_class = CoverDeviceClass.SHUTTER + _attr_supported_features = ( + CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.SET_POSITION + | CoverEntityFeature.STOP + ) + + def __init__(self, coordinator: SwitcherDataUpdateCoordinator) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + + self._attr_name = coordinator.name + self._attr_unique_id = f"{coordinator.device_id}-{coordinator.mac_address}" + self._attr_device_info = DeviceInfo( + connections={ + (device_registry.CONNECTION_NETWORK_MAC, coordinator.mac_address) + } + ) + + self._update_data() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_data() + self.async_write_ha_state() + + def _update_data(self) -> None: + """Update data from device.""" + data: SwitcherShutter = self.coordinator.data + self._attr_current_cover_position = data.position + self._attr_is_closed = data.position == 0 + self._attr_is_closing = data.direction == ShutterDirection.SHUTTER_DOWN + self._attr_is_opening = data.direction == ShutterDirection.SHUTTER_UP + + async def _async_call_api(self, api: str, *args: Any) -> None: + """Call Switcher API.""" + _LOGGER.debug("Calling api for %s, api: '%s', args: %s", self.name, api, args) + response: SwitcherBaseResponse = None + error = None + + try: + async with SwitcherType2Api( + self.coordinator.data.ip_address, self.coordinator.data.device_id + ) as swapi: + response = await getattr(swapi, api)(*args) + except (asyncio.TimeoutError, OSError, RuntimeError) as err: + error = repr(err) + + if error or not response or not response.successful: + self.coordinator.last_update_success = False + self.async_write_ha_state() + raise HomeAssistantError( + f"Call api for {self.name} failed, api: '{api}', " + f"args: {args}, response/error: {response or error}" + ) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close cover.""" + await self._async_call_api(API_SET_POSITON, 0) + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open cover.""" + await self._async_call_api(API_SET_POSITON, 100) + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Move the cover to a specific position.""" + await self._async_call_api(API_SET_POSITON, kwargs[ATTR_POSITION]) + + async def async_stop_cover(self, **_kwargs: Any) -> None: + """Stop the cover.""" + await self._async_call_api(API_STOP) diff --git a/tests/components/switcher_kis/consts.py b/tests/components/switcher_kis/consts.py index 75a99be2709..eaf6a69cb3d 100644 --- a/tests/components/switcher_kis/consts.py +++ b/tests/components/switcher_kis/consts.py @@ -3,7 +3,9 @@ from aioswitcher.device import ( DeviceState, DeviceType, + ShutterDirection, SwitcherPowerPlug, + SwitcherShutter, SwitcherThermostat, SwitcherWaterHeater, ThermostatFanLevel, @@ -23,18 +25,22 @@ DUMMY_AUTO_SHUT_DOWN = "02:00:00" DUMMY_DEVICE_ID1 = "a123bc" DUMMY_DEVICE_ID2 = "cafe12" DUMMY_DEVICE_ID3 = "bada77" +DUMMY_DEVICE_ID4 = "bbd164" DUMMY_DEVICE_NAME1 = "Plug 23BC" DUMMY_DEVICE_NAME2 = "Heater FE12" DUMMY_DEVICE_NAME3 = "Breeze AB39" +DUMMY_DEVICE_NAME4 = "Runner DD77" DUMMY_DEVICE_PASSWORD = "12345678" DUMMY_ELECTRIC_CURRENT1 = 0.5 DUMMY_ELECTRIC_CURRENT2 = 12.8 DUMMY_IP_ADDRESS1 = "192.168.100.157" DUMMY_IP_ADDRESS2 = "192.168.100.158" DUMMY_IP_ADDRESS3 = "192.168.100.159" +DUMMY_IP_ADDRESS4 = "192.168.100.160" DUMMY_MAC_ADDRESS1 = "A1:B2:C3:45:67:D8" DUMMY_MAC_ADDRESS2 = "A1:B2:C3:45:67:D9" DUMMY_MAC_ADDRESS3 = "A1:B2:C3:45:67:DA" +DUMMY_MAC_ADDRESS4 = "A1:B2:C3:45:67:DB" DUMMY_PHONE_ID = "1234" DUMMY_POWER_CONSUMPTION1 = 100 DUMMY_POWER_CONSUMPTION2 = 2780 @@ -46,6 +52,8 @@ DUMMY_TARGET_TEMPERATURE = 23 DUMMY_FAN_LEVEL = ThermostatFanLevel.LOW DUMMY_SWING = ThermostatSwing.OFF DUMMY_REMOTE_ID = "ELEC7001" +DUMMY_POSITION = 54 +DUMMY_DIRECTION = ShutterDirection.SHUTTER_STOP YAML_CONFIG = { DOMAIN: { @@ -79,6 +87,17 @@ DUMMY_WATER_HEATER_DEVICE = SwitcherWaterHeater( DUMMY_AUTO_SHUT_DOWN, ) +DUMMY_SHUTTER_DEVICE = SwitcherShutter( + DeviceType.RUNNER, + DeviceState.ON, + DUMMY_DEVICE_ID4, + DUMMY_IP_ADDRESS4, + DUMMY_MAC_ADDRESS4, + DUMMY_DEVICE_NAME4, + DUMMY_POSITION, + DUMMY_DIRECTION, +) + DUMMY_THERMOSTAT_DEVICE = SwitcherThermostat( DeviceType.BREEZE, DeviceState.ON, diff --git a/tests/components/switcher_kis/test_cover.py b/tests/components/switcher_kis/test_cover.py new file mode 100644 index 00000000000..a4c8b84dadb --- /dev/null +++ b/tests/components/switcher_kis/test_cover.py @@ -0,0 +1,183 @@ +"""Test the Switcher cover platform.""" +from unittest.mock import patch + +from aioswitcher.api import SwitcherBaseResponse +from aioswitcher.device import ShutterDirection +import pytest + +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_POSITION, + DOMAIN as COVER_DOMAIN, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import slugify + +from . import init_integration +from .consts import DUMMY_SHUTTER_DEVICE as DEVICE + +ENTITY_ID = f"{COVER_DOMAIN}.{slugify(DEVICE.name)}" + + +@pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) +async def test_cover(hass, mock_bridge, mock_api, monkeypatch): + """Test cover services.""" + await init_integration(hass) + assert mock_bridge + + # Test initial state - open + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OPEN + + # Test set position + with patch( + "homeassistant.components.switcher_kis.cover.SwitcherType2Api.set_position" + ) as mock_control_device: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_POSITION: 77}, + blocking=True, + ) + + monkeypatch.setattr(DEVICE, "position", 77) + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + + assert mock_api.call_count == 2 + mock_control_device.assert_called_once_with(77) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OPEN + assert state.attributes[ATTR_CURRENT_POSITION] == 77 + + # Test open + with patch( + "homeassistant.components.switcher_kis.cover.SwitcherType2Api.set_position" + ) as mock_control_device: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + monkeypatch.setattr(DEVICE, "direction", ShutterDirection.SHUTTER_UP) + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + + assert mock_api.call_count == 4 + mock_control_device.assert_called_once_with(100) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OPENING + + # Test close + with patch( + "homeassistant.components.switcher_kis.cover.SwitcherType2Api.set_position" + ) as mock_control_device: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + monkeypatch.setattr(DEVICE, "direction", ShutterDirection.SHUTTER_DOWN) + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + + assert mock_api.call_count == 6 + mock_control_device.assert_called_once_with(0) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_CLOSING + + # Test stop + with patch( + "homeassistant.components.switcher_kis.cover.SwitcherType2Api.stop" + ) as mock_control_device: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + monkeypatch.setattr(DEVICE, "direction", ShutterDirection.SHUTTER_STOP) + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + + assert mock_api.call_count == 8 + mock_control_device.assert_called_once() + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OPEN + + # Test closed on position == 0 + monkeypatch.setattr(DEVICE, "position", 0) + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_CLOSED + assert state.attributes[ATTR_CURRENT_POSITION] == 0 + + +@pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) +async def test_cover_control_fail(hass, mock_bridge, mock_api): + """Test cover control fail.""" + await init_integration(hass) + assert mock_bridge + + # Test initial state - open + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OPEN + + # Test exception during set position + with patch( + "homeassistant.components.switcher_kis.cover.SwitcherType2Api.set_position", + side_effect=RuntimeError("fake error"), + ) as mock_control_device: + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_POSITION: 44}, + blocking=True, + ) + + assert mock_api.call_count == 2 + mock_control_device.assert_called_once_with(44) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_UNAVAILABLE + + # Make device available again + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OPEN + + # Test error response during set position + with patch( + "homeassistant.components.switcher_kis.cover.SwitcherType2Api.set_position", + return_value=SwitcherBaseResponse(None), + ) as mock_control_device: + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_POSITION: 27}, + blocking=True, + ) + + assert mock_api.call_count == 4 + mock_control_device.assert_called_once_with(27) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_UNAVAILABLE From 46c60438565bbb2e778bb1fc22b13fc6dfeb53f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Oct 2022 11:41:06 -1000 Subject: [PATCH 439/985] Bump dbus-fast to 1.45.0 (#80289) significant performance improvements https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.44.0...v1.45.0 --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index aaaff591e6b..4d237c7e915 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.1.3", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.4", - "dbus-fast==1.44.0" + "dbus-fast==1.45.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f6eece8de0b..fc6840d0b8b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.4 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.44.0 +dbus-fast==1.45.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 307f51d59c0..4f0f9722b96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -546,7 +546,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.44.0 +dbus-fast==1.45.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a58f7df0ec7..f0bbbb4a177 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -426,7 +426,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.44.0 +dbus-fast==1.45.0 # homeassistant.components.debugpy debugpy==1.6.3 From be46702a535cc1d1138a15d33ffad963cc62ea20 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 13 Oct 2022 23:47:59 +0200 Subject: [PATCH 440/985] Replace deprecated set-output commands [ci] (#80259) --- .github/workflows/ci.yaml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a4952a6376a..8e6cc2398d3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -60,15 +60,15 @@ jobs: - name: Generate partial Python venv restore key id: generate_python_cache_key run: >- - echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ + echo "key=venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }}" + hashFiles('homeassistant/package_constraints.txt') }}" >> $GITHUB_OUTPUT - name: Generate partial pre-commit restore key id: generate_pre-commit_cache_key run: >- - echo "::set-output name=key::pre-commit-${{ env.CACHE_VERSION }}-${{ - hashFiles('.pre-commit-config.yaml') }}" + echo "key=pre-commit-${{ env.CACHE_VERSION }}-${{ + hashFiles('.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT - name: Filter for core changes uses: dorny/paths-filter@v2.11.1 id: core @@ -146,19 +146,19 @@ jobs: # Output & sent to GitHub Actions echo "python_versions: ${ALL_PYTHON_VERSIONS}" - echo "::set-output name=python_versions::${ALL_PYTHON_VERSIONS}" + echo "python_versions=${ALL_PYTHON_VERSIONS}" >> $GITHUB_OUTPUT echo "test_full_suite: ${test_full_suite}" - echo "::set-output name=test_full_suite::${test_full_suite}" + echo "test_full_suite=${test_full_suite}" >> $GITHUB_OUTPUT echo "integrations_glob: ${integrations_glob}" - echo "::set-output name=integrations_glob::${integrations_glob}" + echo "integrations_glob=${integrations_glob}" >> $GITHUB_OUTPUT echo "test_group_count: ${test_group_count}" - echo "::set-output name=test_group_count::${test_group_count}" + echo "test_group_count=${test_group_count}" >> $GITHUB_OUTPUT echo "test_groups: ${test_groups}" - echo "::set-output name=test_groups::${test_groups}" + echo "test_groups=${test_groups}" >> $GITHUB_OUTPUT echo "tests: ${tests}" - echo "::set-output name=tests::${tests}" + echo "tests=${tests}" >> $GITHUB_OUTPUT echo "tests_glob: ${tests_glob}" - echo "::set-output name=tests_glob::${tests_glob}" + echo "tests_glob=${tests_glob}" >> $GITHUB_OUTPUT pre-commit: name: Prepare pre-commit base @@ -505,8 +505,8 @@ jobs: - name: Generate partial pip restore key id: generate-pip-key run: >- - echo "::set-output name=key::pip-${{ env.PIP_CACHE_VERSION }}-${{ - env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" + echo "key=pip-${{ env.PIP_CACHE_VERSION }}-${{ + env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT - name: Restore base Python virtual environment id: cache-venv uses: actions/cache@v3.0.11 From 000e0920968d7713e0fef9251a476e66bdbe1894 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 14 Oct 2022 00:37:00 +0000 Subject: [PATCH 441/985] [ci skip] Translation update --- .../components/bayesian/translations/ca.json | 2 ++ .../devolo_home_network/translations/fr.json | 8 +++++- .../translations/pt-BR.json | 8 +++++- .../lametric/translations/select.fr.json | 8 ++++++ .../lametric/translations/select.pt-BR.json | 8 ++++++ .../lutron_caseta/translations/ca.json | 3 +++ .../lutron_caseta/translations/de.json | 3 +++ .../lutron_caseta/translations/et.json | 3 +++ .../lutron_caseta/translations/fr.json | 3 +++ .../lutron_caseta/translations/id.json | 3 +++ .../lutron_caseta/translations/no.json | 3 +++ .../lutron_caseta/translations/pl.json | 3 +++ .../lutron_caseta/translations/pt-BR.json | 3 +++ .../lutron_caseta/translations/ru.json | 3 +++ .../lutron_caseta/translations/zh-Hant.json | 3 +++ .../components/snooz/translations/ca.json | 6 +++++ .../components/snooz/translations/fr.json | 21 +++++++++++++++ .../components/snooz/translations/pt-BR.json | 27 +++++++++++++++++++ .../components/zwave_js/translations/ca.json | 3 ++- .../components/zwave_js/translations/fr.json | 3 ++- .../zwave_js/translations/pt-BR.json | 3 ++- 21 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/lametric/translations/select.fr.json create mode 100644 homeassistant/components/lametric/translations/select.pt-BR.json create mode 100644 homeassistant/components/snooz/translations/fr.json create mode 100644 homeassistant/components/snooz/translations/pt-BR.json diff --git a/homeassistant/components/bayesian/translations/ca.json b/homeassistant/components/bayesian/translations/ca.json index 45c96135eb7..97d9d377885 100644 --- a/homeassistant/components/bayesian/translations/ca.json +++ b/homeassistant/components/bayesian/translations/ca.json @@ -1,9 +1,11 @@ { "issues": { "manual_migration": { + "description": "La integraci\u00f3 bayesiana ara tamb\u00e9 actualitza la probabilitat si l'observat `to_state`, `above', `below` o `value_template` retorna `Fals` en lloc de nom\u00e9s `Cert`. Per tant, ja no cal tenir entrades duplicades i complement\u00e0ries per a cada estat binari. Pots eliminar l'entrada duplicada de `{entity}`.", "title": "Es necessita una correcci\u00f3 manual YAML per a Bayesian" }, "no_prob_given_false": { + "description": "A la integraci\u00f3 bayesiana, `prob_given_false` \u00e9s ara una variable de configuraci\u00f3 necess\u00e0ria (no hi havia cap motiu per al valor predeterminat anterior). Afegeix-la a `configuration.yaml` a `bayesian/ {entity}`. Les observacions s'ignoraran mentre no ho afegeixis.", "title": "Es necessita afegir configuraci\u00f3 manual YAML per a Bayesian" } } diff --git a/homeassistant/components/devolo_home_network/translations/fr.json b/homeassistant/components/devolo_home_network/translations/fr.json index 50e601bb14a..cbf4a881c2b 100644 --- a/homeassistant/components/devolo_home_network/translations/fr.json +++ b/homeassistant/components/devolo_home_network/translations/fr.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "home_control": "L'unit\u00e9 centrale devolo Home Control ne fonctionne pas avec cette int\u00e9gration." + "home_control": "L'unit\u00e9 centrale devolo Home Control ne fonctionne pas avec cette int\u00e9gration.", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -10,6 +11,11 @@ }, "flow_title": "{product} ( {name} )", "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + } + }, "user": { "data": { "ip_address": "Adresse IP" diff --git a/homeassistant/components/devolo_home_network/translations/pt-BR.json b/homeassistant/components/devolo_home_network/translations/pt-BR.json index 94a1f632d78..9eae8cedf0f 100644 --- a/homeassistant/components/devolo_home_network/translations/pt-BR.json +++ b/homeassistant/components/devolo_home_network/translations/pt-BR.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", - "home_control": "A Unidade Central de Home Control Devolo n\u00e3o funciona com esta integra\u00e7\u00e3o." + "home_control": "A Unidade Central de Home Control Devolo n\u00e3o funciona com esta integra\u00e7\u00e3o.", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "cannot_connect": "Falha ao conectar", @@ -10,6 +11,11 @@ }, "flow_title": "{product} ({name})", "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + } + }, "user": { "data": { "ip_address": "Endere\u00e7o IP" diff --git a/homeassistant/components/lametric/translations/select.fr.json b/homeassistant/components/lametric/translations/select.fr.json new file mode 100644 index 00000000000..6502e00f7fe --- /dev/null +++ b/homeassistant/components/lametric/translations/select.fr.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "Automatique", + "manual": "Manuel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/select.pt-BR.json b/homeassistant/components/lametric/translations/select.pt-BR.json new file mode 100644 index 00000000000..dcf5c796e00 --- /dev/null +++ b/homeassistant/components/lametric/translations/select.pt-BR.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "Autom\u00e1tico", + "manual": "Manual" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/ca.json b/homeassistant/components/lutron_caseta/translations/ca.json index de714fea726..b519cd2a864 100644 --- a/homeassistant/components/lutron_caseta/translations/ca.json +++ b/homeassistant/components/lutron_caseta/translations/ca.json @@ -33,6 +33,9 @@ "button_2": "Segon bot\u00f3", "button_3": "Tercer bot\u00f3", "button_4": "Quart bot\u00f3", + "button_5": "Cinqu\u00e8 bot\u00f3", + "button_6": "Sis\u00e8 bot\u00f3", + "button_7": "Set\u00e8 bot\u00f3", "close_1": "Tanca 1", "close_2": "Tanca 2", "close_3": "Tanca 3", diff --git a/homeassistant/components/lutron_caseta/translations/de.json b/homeassistant/components/lutron_caseta/translations/de.json index 4bd8e2a5931..d2406ef16d4 100644 --- a/homeassistant/components/lutron_caseta/translations/de.json +++ b/homeassistant/components/lutron_caseta/translations/de.json @@ -33,6 +33,9 @@ "button_2": "Zweite Taste", "button_3": "Dritte Taste", "button_4": "Vierte Taste", + "button_5": "Taste 5", + "button_6": "Taste 6", + "button_7": "Taste 7", "close_1": "Einen schlie\u00dfen", "close_2": "Zwei schlie\u00dfen", "close_3": "Drei schlie\u00dfen", diff --git a/homeassistant/components/lutron_caseta/translations/et.json b/homeassistant/components/lutron_caseta/translations/et.json index b6d73a920d4..24f6fef4ac3 100644 --- a/homeassistant/components/lutron_caseta/translations/et.json +++ b/homeassistant/components/lutron_caseta/translations/et.json @@ -33,6 +33,9 @@ "button_2": "Teine nupp", "button_3": "Kolmas nupp", "button_4": "Neljas nupp", + "button_5": "Viies nupp", + "button_6": "Kuues nupp", + "button_7": "Seitsmes nupp", "close_1": "Sule #1", "close_2": "Sule #2", "close_3": "Sule #3", diff --git a/homeassistant/components/lutron_caseta/translations/fr.json b/homeassistant/components/lutron_caseta/translations/fr.json index c5f259167d2..fd07ef3e87e 100644 --- a/homeassistant/components/lutron_caseta/translations/fr.json +++ b/homeassistant/components/lutron_caseta/translations/fr.json @@ -33,6 +33,9 @@ "button_2": "Deuxi\u00e8me bouton", "button_3": "Troisi\u00e8me bouton", "button_4": "Quatri\u00e8me bouton", + "button_5": "Cinqui\u00e8me bouton", + "button_6": "Sixi\u00e8me bouton", + "button_7": "Septi\u00e8me bouton", "close_1": "Fermer 1", "close_2": "Fermer 2", "close_3": "Fermer 3", diff --git a/homeassistant/components/lutron_caseta/translations/id.json b/homeassistant/components/lutron_caseta/translations/id.json index 7789d784d23..66d768c20ac 100644 --- a/homeassistant/components/lutron_caseta/translations/id.json +++ b/homeassistant/components/lutron_caseta/translations/id.json @@ -33,6 +33,9 @@ "button_2": "Tombol kedua", "button_3": "Tombol ketiga", "button_4": "Tombol keempat", + "button_5": "Tombol kelima", + "button_6": "Tombol keenam", + "button_7": "Tombol ketujuh", "close_1": "Tutup 1", "close_2": "Tutup 2", "close_3": "Tutup 3", diff --git a/homeassistant/components/lutron_caseta/translations/no.json b/homeassistant/components/lutron_caseta/translations/no.json index 91e7bc28007..e5f8e72330d 100644 --- a/homeassistant/components/lutron_caseta/translations/no.json +++ b/homeassistant/components/lutron_caseta/translations/no.json @@ -33,6 +33,9 @@ "button_2": "Andre knapp", "button_3": "Tredje knapp", "button_4": "Fjerde knapp", + "button_5": "Femte knapp", + "button_6": "Sjette knapp", + "button_7": "Syvende knapp", "close_1": "Lukk 1", "close_2": "Lukk 2", "close_3": "Lukk 3", diff --git a/homeassistant/components/lutron_caseta/translations/pl.json b/homeassistant/components/lutron_caseta/translations/pl.json index 47e6a07e146..a37360eb937 100644 --- a/homeassistant/components/lutron_caseta/translations/pl.json +++ b/homeassistant/components/lutron_caseta/translations/pl.json @@ -33,6 +33,9 @@ "button_2": "drugi", "button_3": "trzeci", "button_4": "czwarty", + "button_5": "pi\u0105ty", + "button_6": "sz\u00f3sty", + "button_7": "si\u00f3dmy", "close_1": "zamknij 1", "close_2": "zamknij 2", "close_3": "zamknij 3", diff --git a/homeassistant/components/lutron_caseta/translations/pt-BR.json b/homeassistant/components/lutron_caseta/translations/pt-BR.json index 28a85a8820d..274d7d25b95 100644 --- a/homeassistant/components/lutron_caseta/translations/pt-BR.json +++ b/homeassistant/components/lutron_caseta/translations/pt-BR.json @@ -33,6 +33,9 @@ "button_2": "Segundo bot\u00e3o", "button_3": "Terceiro bot\u00e3o", "button_4": "Quarto bot\u00e3o", + "button_5": "Quinto bot\u00e3o", + "button_6": "Sexto bot\u00e3o", + "button_7": "S\u00e9timo bot\u00e3o", "close_1": "Fechar 1", "close_2": "Fechar 2", "close_3": "Fechar 3", diff --git a/homeassistant/components/lutron_caseta/translations/ru.json b/homeassistant/components/lutron_caseta/translations/ru.json index 090af1923f3..07705b270ba 100644 --- a/homeassistant/components/lutron_caseta/translations/ru.json +++ b/homeassistant/components/lutron_caseta/translations/ru.json @@ -33,6 +33,9 @@ "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_5": "\u041f\u044f\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_6": "\u0428\u0435\u0441\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_7": "\u0421\u0435\u0434\u044c\u043c\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "close_1": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c 1", "close_2": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c 2", "close_3": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c 3", diff --git a/homeassistant/components/lutron_caseta/translations/zh-Hant.json b/homeassistant/components/lutron_caseta/translations/zh-Hant.json index 320c26fd2ea..cb2ee04c948 100644 --- a/homeassistant/components/lutron_caseta/translations/zh-Hant.json +++ b/homeassistant/components/lutron_caseta/translations/zh-Hant.json @@ -33,6 +33,9 @@ "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215", "button_3": "\u7b2c\u4e09\u500b\u6309\u9215", "button_4": "\u7b2c\u56db\u500b\u6309\u9215", + "button_5": "\u7b2c\u4e94\u500b\u6309\u9215", + "button_6": "\u7b2c\u516d\u500b\u6309\u9215", + "button_7": "\u7b2c\u4e03\u500b\u6309\u9215", "close_1": "\u95dc\u9589 1", "close_2": "\u95dc\u9589 2", "close_3": "\u95dc\u9589 3", diff --git a/homeassistant/components/snooz/translations/ca.json b/homeassistant/components/snooz/translations/ca.json index 0cd4571dc9d..dca36286b90 100644 --- a/homeassistant/components/snooz/translations/ca.json +++ b/homeassistant/components/snooz/translations/ca.json @@ -6,10 +6,16 @@ "no_devices_found": "No s'han trobat dispositius a la xarxa" }, "flow_title": "{name}", + "progress": { + "wait_for_pairing_mode": "Per completar la configuraci\u00f3, posa el dispositiu en mode de vinculaci\u00f3. \n\n### Com entrar en el mode de vinculaci\u00f3\n1. For\u00e7a el tancament de les aplicacions m\u00f2bils SNOOZ.\n2. Mant\u00e9 premut el bot\u00f3 d'engegada del dispositiu i allibera'l quan els llums comencin a parpellejar (despr\u00e9s d'uns 5 segons)." + }, "step": { "bluetooth_confirm": { "description": "Vols configurar {name}?" }, + "pairing_timeout": { + "description": "El dispositiu no ha entrat en mode de vinculaci\u00f3. Fes clic a Envia per tornar-ho a intentar.\n\n### Resoluci\u00f3 de problemes\n1. Comprova que el dispositiu no estigui connectat a l'aplicaci\u00f3 m\u00f2bil.\n2. Desconnecta el dispositiu durant 5 segons i, a continuaci\u00f3, torna'l a connectar." + }, "user": { "data": { "address": "Dispositiu" diff --git a/homeassistant/components/snooz/translations/fr.json b/homeassistant/components/snooz/translations/fr.json new file mode 100644 index 00000000000..c8a1af034cf --- /dev/null +++ b/homeassistant/components/snooz/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/snooz/translations/pt-BR.json b/homeassistant/components/snooz/translations/pt-BR.json new file mode 100644 index 00000000000..953e074c5aa --- /dev/null +++ b/homeassistant/components/snooz/translations/pt-BR.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "flow_title": "{name}", + "progress": { + "wait_for_pairing_mode": "Para concluir a configura\u00e7\u00e3o, coloque este dispositivo no modo de emparelhamento. \n\n ### Como entrar no modo de emparelhamento\n 1. For\u00e7ar o encerramento dos aplicativos m\u00f3veis SNOOZ.\n 2. Pressione e segure o bot\u00e3o liga/desliga no dispositivo. Solte quando as luzes come\u00e7arem a piscar (aproximadamente 5 segundos)." + }, + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "pairing_timeout": { + "description": "O dispositivo n\u00e3o entrou no modo de emparelhamento. Clique em Enviar para tentar novamente. \n\n ### Solu\u00e7\u00e3o de problemas\n 1. Verifique se o dispositivo n\u00e3o est\u00e1 conectado ao aplicativo m\u00f3vel.\n 2. Desconecte o dispositivo por 5 segundos e conecte-o novamente." + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/ca.json b/homeassistant/components/zwave_js/translations/ca.json index 455fd8f9127..fb478f3ad5e 100644 --- a/homeassistant/components/zwave_js/translations/ca.json +++ b/homeassistant/components/zwave_js/translations/ca.json @@ -10,7 +10,8 @@ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "cannot_connect": "Ha fallat la connexi\u00f3", "discovery_requires_supervisor": "El descobriment requereix el supervisor.", - "not_zwave_device": "El dispositiu descobert no \u00e9s un dispositiu Z-Wave." + "not_zwave_device": "El dispositiu descobert no \u00e9s un dispositiu Z-Wave.", + "not_zwave_js_addon": "El complement descobert no \u00e9s el complement oficial Z-Wave JS." }, "error": { "addon_start_failed": "No s'ha pogut iniciar el complement Z-Wave JS. Comprova la configuraci\u00f3.", diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index cf7552491c7..55c613e740a 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -10,7 +10,8 @@ "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "cannot_connect": "\u00c9chec de connexion", "discovery_requires_supervisor": "La d\u00e9couverte n\u00e9cessite le superviseur.", - "not_zwave_device": "L'appareil d\u00e9couvert n'est pas un appareil Z-Wave." + "not_zwave_device": "L'appareil d\u00e9couvert n'est pas un appareil Z-Wave.", + "not_zwave_js_addon": "Le module compl\u00e9mentaire d\u00e9couvert n'est pas le module compl\u00e9mentaire officiel de Z-Wave JS." }, "error": { "addon_start_failed": "\u00c9chec du d\u00e9marrage du module compl\u00e9mentaire Z-Wave JS. V\u00e9rifiez la configuration.", diff --git a/homeassistant/components/zwave_js/translations/pt-BR.json b/homeassistant/components/zwave_js/translations/pt-BR.json index 83b6bed6365..dd26153495c 100644 --- a/homeassistant/components/zwave_js/translations/pt-BR.json +++ b/homeassistant/components/zwave_js/translations/pt-BR.json @@ -10,7 +10,8 @@ "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "cannot_connect": "Falha ao conectar", "discovery_requires_supervisor": "A descoberta requer o supervisor.", - "not_zwave_device": "O dispositivo descoberto n\u00e3o \u00e9 um dispositivo Z-Wave." + "not_zwave_device": "O dispositivo descoberto n\u00e3o \u00e9 um dispositivo Z-Wave.", + "not_zwave_js_addon": "O complemento descoberto n\u00e3o \u00e9 o complemento oficial do Z-Wave JS." }, "error": { "addon_start_failed": "Falha ao iniciar o add-on Z-Wave JS. Verifique a configura\u00e7\u00e3o.", From 5b6e46e7b78d9a4a030dfe4c0ff8bd18054cb328 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Oct 2022 14:53:09 -1000 Subject: [PATCH 442/985] Fix nexia permanent hold when cool and heat temps are within 2 degrees (#80297) --- homeassistant/components/nexia/climate.py | 4 ++-- homeassistant/components/nexia/manifest.json | 2 +- homeassistant/components/nexia/switch.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 81e6158a872..66c325d2fc3 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -206,7 +206,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): """Set the hvac run mode.""" if run_mode is not None: if run_mode == HOLD_PERMANENT: - await self._zone.call_permanent_hold() + await self._zone.set_permanent_hold() else: await self._zone.call_return_to_schedule() if hvac_mode is not None: @@ -399,7 +399,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): await self._zone.call_return_to_schedule() await self._zone.set_mode(mode=OPERATION_MODE_AUTO) else: - await self._zone.call_permanent_hold() + await self._zone.set_permanent_hold() await self._zone.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode]) self._signal_zone_update() diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index e381dc95897..77280b1f503 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==2.0.2"], + "requirements": ["nexia==2.0.4"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/homeassistant/components/nexia/switch.py b/homeassistant/components/nexia/switch.py index e242032c947..643a4d585c4 100644 --- a/homeassistant/components/nexia/switch.py +++ b/homeassistant/components/nexia/switch.py @@ -62,7 +62,7 @@ class NexiaHoldSwitch(NexiaThermostatZoneEntity, SwitchEntity): if self._zone.get_current_mode() == OPERATION_MODE_OFF: await self._zone.call_permanent_off() else: - await self._zone.call_permanent_hold() + await self._zone.set_permanent_hold() self._signal_zone_update() async def async_turn_off(self, **kwargs: Any) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index 4f0f9722b96..7a2e3c7c32b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1138,7 +1138,7 @@ nettigo-air-monitor==1.4.2 neurio==0.3.1 # homeassistant.components.nexia -nexia==2.0.2 +nexia==2.0.4 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0bbbb4a177..a00a568d227 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -828,7 +828,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.4.2 # homeassistant.components.nexia -nexia==2.0.2 +nexia==2.0.4 # homeassistant.components.discord nextcord==2.0.0a8 From d9af274da3deb82a04ac3916d583e9c1135878d7 Mon Sep 17 00:00:00 2001 From: Geliras <33727851+Geliras@users.noreply.github.com> Date: Fri, 14 Oct 2022 08:23:50 +0200 Subject: [PATCH 443/985] Add edl21 sensors (#80214) * Adding unhandled sensors Adding unhandled sensors as mentioned here: https://github.com/home-assistant/core/issues/78599 https://github.com/home-assistant/core/issues/64696 OBIS codes of EFR SGM-C4 from manual found at page 32: https://www.mit-n.de/fileadmin/user_upload/Dateien/Messwesen/Messwesen_Strom/EFR-SGM-C4-Produkthandbuch.pdf * Update sensor.py --- homeassistant/components/edl21/sensor.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index cfdbbd01a2e..c598827d244 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -223,9 +223,17 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), # C=81: Angles # D=7: Instantaneous value + # E=1: U(L2) x U(L1) + # E=2: U(L3) x U(L1) # E=4: U(L1) x I(L1) # E=15: U(L2) x I(L2) # E=26: U(L3) x I(L3) + SensorEntityDescription( + key="1-0:81.7.1*255", name="U(L2)/U(L1) phase angle", icon="mdi:sine-wave" + ), + SensorEntityDescription( + key="1-0:81.7.2*255", name="U(L3)/U(L1) phase angle", icon="mdi:sine-wave" + ), SensorEntityDescription( key="1-0:81.7.4*255", name="U(L1)/I(L1) phase angle", icon="mdi:sine-wave" ), @@ -273,9 +281,13 @@ class EDL21: _OBIS_BLACKLIST = { # C=96: Electricity-related service entries - "1-0:96.50.1*1", # Manufacturer specific - "1-0:96.90.2*1", # Manufacturer specific - "1-0:96.90.2*2", # Manufacturer specific + "1-0:96.50.1*1", # Manufacturer specific EFR SGM-C4 Hardware version + "1-0:96.50.1*4", # Manufacturer specific EFR SGM-C4 Hardware version + "1-0:96.50.4*4", # Manufacturer specific EFR SGM-C4 Parameters version + "1-0:96.90.2*1", # Manufacturer specific EFR SGM-C4 Firmware Checksum + "1-0:96.90.2*2", # Manufacturer specific EFR SGM-C4 Firmware Checksum + # C=97: Electricity-related service entries + "1-0:97.97.0*0", # Manufacturer specific EFR SGM-C4 Error register # A=129: Manufacturer specific "129-129:199.130.3*255", # Iskraemeco: Manufacturer "129-129:199.130.5*255", # Iskraemeco: Public Key From 2e261d5dc28479b1d1d0ea8b8ef6cfa2cf6c4917 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 14 Oct 2022 08:32:19 +0200 Subject: [PATCH 444/985] Allow specifying the target table when importing statistics (#80230) Allow specifying the table when importing statistics --- homeassistant/components/recorder/core.py | 9 +++++++-- homeassistant/components/recorder/statistics.py | 9 +++++---- homeassistant/components/recorder/tasks.py | 10 ++++++++-- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index d5e095d8104..833064cee63 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -61,7 +61,9 @@ from .db_schema import ( Events, StateAttributes, States, + Statistics, StatisticsRuns, + StatisticsShortTerm, ) from .executor import DBInterruptibleThreadPoolExecutor from .models import ( @@ -534,10 +536,13 @@ class Recorder(threading.Thread): @callback def async_import_statistics( - self, metadata: StatisticMetaData, stats: Iterable[StatisticData] + self, + metadata: StatisticMetaData, + stats: Iterable[StatisticData], + table: type[Statistics | StatisticsShortTerm], ) -> None: """Schedule import of statistics.""" - self.queue_task(ImportStatisticsTask(metadata, stats)) + self.queue_task(ImportStatisticsTask(metadata, stats, table)) @callback def _async_setup_periodic_tasks(self) -> None: diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 3ff438f50da..773676b07b0 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -1461,7 +1461,7 @@ def _async_import_statistics( statistic["last_reset"] = dt_util.as_utc(last_reset) # Insert job in recorder's queue - get_instance(hass).async_import_statistics(metadata, statistics) + get_instance(hass).async_import_statistics(metadata, statistics, Statistics) @callback @@ -1551,6 +1551,7 @@ def import_statistics( instance: Recorder, metadata: StatisticMetaData, statistics: Iterable[StatisticData], + table: type[Statistics | StatisticsShortTerm], ) -> bool: """Process an import_statistics job.""" @@ -1564,11 +1565,11 @@ def import_statistics( metadata_id = _update_or_add_metadata(session, metadata, old_metadata_dict) for stat in statistics: if stat_id := _statistics_exists( - session, Statistics, metadata_id, stat["start"] + session, table, metadata_id, stat["start"] ): - _update_statistics(session, Statistics, stat_id, stat) + _update_statistics(session, table, stat_id, stat) else: - _insert_statistics(session, Statistics, metadata_id, stat) + _insert_statistics(session, table, metadata_id, stat) return True diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index 4fa3a3cc40c..1b8e03ebf17 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -14,6 +14,7 @@ from homeassistant.helpers.typing import UndefinedType from . import purge, statistics from .const import DOMAIN, EXCLUDE_ATTRIBUTES +from .db_schema import Statistics, StatisticsShortTerm from .models import StatisticData, StatisticMetaData from .util import periodic_db_cleanups @@ -147,13 +148,18 @@ class ImportStatisticsTask(RecorderTask): metadata: StatisticMetaData statistics: Iterable[StatisticData] + table: type[Statistics | StatisticsShortTerm] def run(self, instance: Recorder) -> None: """Run statistics task.""" - if statistics.import_statistics(instance, self.metadata, self.statistics): + if statistics.import_statistics( + instance, self.metadata, self.statistics, self.table + ): return # Schedule a new statistics task if this one didn't finish - instance.queue_task(ImportStatisticsTask(self.metadata, self.statistics)) + instance.queue_task( + ImportStatisticsTask(self.metadata, self.statistics, self.table) + ) @dataclass From d327355afcd0643797f56686added805dbc78aac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Oct 2022 20:51:27 -1000 Subject: [PATCH 445/985] Bump HAP-python to fix pairing with iOS 16 (#80301) Using the ha- fork until upstream can pickup and merge pending PRs. The plan is to revert back to upstream HAP-python when its back in sync Fixes #79305 Fixes #79304 --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 2ca78b6d915..187eaf4c869 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": [ - "HAP-python==4.5.0", + "ha-HAP-python==4.5.2", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1" diff --git a/requirements_all.txt b/requirements_all.txt index 7a2e3c7c32b..1a0210dd82f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -10,9 +10,6 @@ AIOAladdinConnect==0.1.46 # homeassistant.components.adax Adax-local==0.1.4 -# homeassistant.components.homekit -HAP-python==4.5.0 - # homeassistant.components.mastodon Mastodon.py==1.5.1 @@ -821,6 +818,9 @@ gstreamer-player==1.1.2 # homeassistant.components.profiler guppy3==3.1.2 +# homeassistant.components.homekit +ha-HAP-python==4.5.2 + # homeassistant.components.generic # homeassistant.components.stream ha-av==10.0.0b5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a00a568d227..884c57df5dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -12,9 +12,6 @@ AIOAladdinConnect==0.1.46 # homeassistant.components.adax Adax-local==0.1.4 -# homeassistant.components.homekit -HAP-python==4.5.0 - # homeassistant.components.flick_electric PyFlick==0.0.2 @@ -613,6 +610,9 @@ gspread==5.5.0 # homeassistant.components.profiler guppy3==3.1.2 +# homeassistant.components.homekit +ha-HAP-python==4.5.2 + # homeassistant.components.generic # homeassistant.components.stream ha-av==10.0.0b5 From 2c206ad05081d6b6379277f7b6480c4a3e4c8df3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 14 Oct 2022 09:10:38 +0200 Subject: [PATCH 446/985] Fix flaky recorder test (#80246) * Fix flaky recorder test * Update tests/components/recorder/test_init.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- tests/components/recorder/test_init.py | 70 ++++++++++++-------------- 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 9939fc7fb46..bcbf27faf18 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1002,54 +1002,48 @@ def test_statistics_runs_initiated(hass_recorder): ) - timedelta(minutes=5) -def test_compile_missing_statistics(tmpdir): +@pytest.mark.freeze_time("2022-09-13 09:00:00+02:00") +def test_compile_missing_statistics(tmpdir, freezer): """Test missing statistics are compiled on startup.""" now = dt_util.utcnow().replace(minute=0, second=0, microsecond=0) test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" - with patch( - "homeassistant.components.recorder.core.dt_util.utcnow", return_value=now - ): + hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) + setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) + hass.start() + wait_recording_done(hass) + wait_recording_done(hass) - hass = get_test_home_assistant() - recorder_helper.async_initialize_recorder(hass) - setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) - hass.start() - wait_recording_done(hass) - wait_recording_done(hass) + with session_scope(hass=hass) as session: + statistics_runs = list(session.query(StatisticsRuns)) + assert len(statistics_runs) == 1 + last_run = process_timestamp(statistics_runs[0].start) + assert last_run == now - timedelta(minutes=5) - with session_scope(hass=hass) as session: - statistics_runs = list(session.query(StatisticsRuns)) - assert len(statistics_runs) == 1 - last_run = process_timestamp(statistics_runs[0].start) - assert last_run == now - timedelta(minutes=5) + wait_recording_done(hass) + wait_recording_done(hass) + hass.stop() - wait_recording_done(hass) - wait_recording_done(hass) - hass.stop() + # Start Home Assistant one hour later + freezer.tick(timedelta(hours=1)) + hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) + setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) + hass.start() + wait_recording_done(hass) + wait_recording_done(hass) - with patch( - "homeassistant.components.recorder.core.dt_util.utcnow", - return_value=now + timedelta(hours=1), - ): + with session_scope(hass=hass) as session: + statistics_runs = list(session.query(StatisticsRuns)) + assert len(statistics_runs) == 13 # 12 5-minute runs + last_run = process_timestamp(statistics_runs[1].start) + assert last_run == now - hass = get_test_home_assistant() - recorder_helper.async_initialize_recorder(hass) - setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) - hass.start() - wait_recording_done(hass) - wait_recording_done(hass) - - with session_scope(hass=hass) as session: - statistics_runs = list(session.query(StatisticsRuns)) - assert len(statistics_runs) == 13 # 12 5-minute runs - last_run = process_timestamp(statistics_runs[1].start) - assert last_run == now - - wait_recording_done(hass) - wait_recording_done(hass) - hass.stop() + wait_recording_done(hass) + wait_recording_done(hass) + hass.stop() def test_saving_sets_old_state(hass_recorder): From 6470a6b2033636faa59129f29c6ba51b8ceddd62 Mon Sep 17 00:00:00 2001 From: Kevin Cathcart Date: Fri, 14 Oct 2022 03:48:45 -0400 Subject: [PATCH 447/985] Update issue report link for installation type (#80300) Update link for installation type --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6b13f0980b1..0390910cc58 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -46,9 +46,9 @@ body: attributes: label: What type of installation are you running? description: > - Can be found in: [Settings -> About](https://my.home-assistant.io/redirect/info/). + Can be found in: [Settings -> System-> Repairs -> Three Dots in Upper Right -> System information](https://my.home-assistant.io/redirect/system_health/). - [![Open your Home Assistant instance and show your Home Assistant version information.](https://my.home-assistant.io/badges/info.svg)](https://my.home-assistant.io/redirect/info/) + [![Open your Home Assistant instance and show health information about your system.](https://my.home-assistant.io/badges/system_health.svg)](https://my.home-assistant.io/redirect/system_health/) options: - Home Assistant OS - Home Assistant Container From 1cc06cf83b54d9cd0971e01c4bc59ea34cbba557 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Fri, 14 Oct 2022 09:12:20 +0100 Subject: [PATCH 448/985] Bump temperusb to 1.6.0 (#80296) Co-authored-by: Dave T --- homeassistant/components/temper/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/temper/manifest.json b/homeassistant/components/temper/manifest.json index b71bbe91563..72d998a9d4a 100644 --- a/homeassistant/components/temper/manifest.json +++ b/homeassistant/components/temper/manifest.json @@ -2,7 +2,7 @@ "domain": "temper", "name": "TEMPer", "documentation": "https://www.home-assistant.io/integrations/temper", - "requirements": ["temperusb==1.5.3"], + "requirements": ["temperusb==1.6.0"], "codeowners": [], "iot_class": "local_polling", "loggers": ["pyusb", "temperusb"] diff --git a/requirements_all.txt b/requirements_all.txt index 1a0210dd82f..c8e5427e2c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2377,7 +2377,7 @@ tellduslive==0.10.11 temescal==0.5 # homeassistant.components.temper -temperusb==1.5.3 +temperusb==1.6.0 # homeassistant.components.nibe_heatpump tenacity==8.0.1 From 0f9703cd99459cc78f1036b1899d67a69ea4ccd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9n?= Date: Fri, 14 Oct 2022 10:19:32 +0200 Subject: [PATCH 449/985] Add Heiwa as supported brand (#80242) --- homeassistant/components/gree/manifest.json | 5 ++++- homeassistant/generated/supported_brands.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index 97c0ec1780c..06b8b109175 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -7,5 +7,8 @@ "dependencies": ["network"], "codeowners": ["@cmroche"], "iot_class": "local_polling", - "loggers": ["greeclimate"] + "loggers": ["greeclimate"], + "supported_brands": { + "heiwa": "Heiwa" + } } diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py index bb996813bba..30a0601861d 100644 --- a/homeassistant/generated/supported_brands.py +++ b/homeassistant/generated/supported_brands.py @@ -5,6 +5,7 @@ To update, run python3 -m script.hassfest HAS_SUPPORTED_BRANDS = [ "denonavr", + "gree", "hunterdouglas_powerview", "inkbird", "motion_blinds", From d3840a04b58765b2eb59c0bd12338c0b0b27bf82 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 14 Oct 2022 12:00:30 +0300 Subject: [PATCH 450/985] Remove quality scale checkboxes from pull request template (#80298) --- .github/PULL_REQUEST_TEMPLATE.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 52d25226930..23b355a223f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -75,18 +75,6 @@ If the code communicates with devices, web services, or third-party tools: - [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description. - [ ] Untested files have been added to `.coveragerc`. -The integration reached or maintains the following [Integration Quality Scale][quality-scale]: - - -- [ ] No score or internal -- [ ] 🥈 Silver -- [ ] 🥇 Gold -- [ ] 🏆 Platinum - ssdp_confirm(None) --> ssdp_confirm({}) --> create_entry() # - user(None): scan --> user({...}) --> create_entry() - # - import(None) --> create_entry() def __init__(self) -> None: """Initialize the UPnP/IGD config flow.""" diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index 023ec82a487..8d98790983a 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -11,13 +11,16 @@ BYTES_RECEIVED = "bytes_received" BYTES_SENT = "bytes_sent" PACKETS_RECEIVED = "packets_received" PACKETS_SENT = "packets_sent" +KIBIBYTES_PER_SEC_RECEIVED = "kibibytes_per_sec_received" +KIBIBYTES_PER_SEC_SENT = "kibibytes_per_sec_sent" +PACKETS_PER_SEC_RECEIVED = "packets_per_sec_received" +PACKETS_PER_SEC_SENT = "packets_per_sec_sent" TIMESTAMP = "timestamp" DATA_PACKETS = "packets" DATA_RATE_PACKETS_PER_SECOND = f"{DATA_PACKETS}/{TIME_SECONDS}" WAN_STATUS = "wan_status" ROUTER_IP = "ip" ROUTER_UPTIME = "uptime" -KIBIBYTE = 1024 CONFIG_ENTRY_ST = "st" CONFIG_ENTRY_UDN = "udn" CONFIG_ENTRY_ORIGINAL_UDN = "original_udn" diff --git a/homeassistant/components/upnp/coordinator.py b/homeassistant/components/upnp/coordinator.py new file mode 100644 index 00000000000..18d37b4a388 --- /dev/null +++ b/homeassistant/components/upnp/coordinator.py @@ -0,0 +1,50 @@ +"""UPnP/IGD coordinator.""" + +from collections.abc import Mapping +from datetime import timedelta +from typing import Any + +from async_upnp_client.exceptions import UpnpCommunicationError + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import LOGGER +from .device import Device + + +class UpnpDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to update data from UPNP device.""" + + def __init__( + self, + hass: HomeAssistant, + device: Device, + device_entry: DeviceEntry, + update_interval: timedelta, + ) -> None: + """Initialize.""" + self.device = device + self.device_entry = device_entry + + super().__init__( + hass, + LOGGER, + name=device.name, + update_interval=update_interval, + ) + + async def _async_update_data(self) -> Mapping[str, Any]: + """Update data.""" + try: + return await self.device.async_get_data() + except UpnpCommunicationError as exception: + LOGGER.debug( + "Caught exception when updating device: %s, exception: %s", + self.device, + exception, + ) + raise UpdateFailed( + f"Unable to communicate with IGD at: {self.device.device_url}" + ) from exception diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index e06ada02b77..61784749c6f 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -1,7 +1,6 @@ """Home Assistant representation of an UPnP/IGD.""" from __future__ import annotations -import asyncio from collections.abc import Mapping from functools import partial from ipaddress import ip_address @@ -10,19 +9,21 @@ from urllib.parse import urlparse from async_upnp_client.aiohttp import AiohttpSessionRequester from async_upnp_client.client_factory import UpnpFactory -from async_upnp_client.exceptions import UpnpError -from async_upnp_client.profiles.igd import IgdDevice, StatusInfo +from async_upnp_client.profiles.igd import IgdDevice from getmac import get_mac_address from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from homeassistant.util.dt import utcnow from .const import ( BYTES_RECEIVED, BYTES_SENT, + KIBIBYTES_PER_SEC_RECEIVED, + KIBIBYTES_PER_SEC_SENT, LOGGER as _LOGGER, + PACKETS_PER_SEC_RECEIVED, + PACKETS_PER_SEC_SENT, PACKETS_RECEIVED, PACKETS_SENT, ROUTER_IP, @@ -51,7 +52,7 @@ async def async_create_device(hass: HomeAssistant, ssdp_location: str) -> Device session = async_get_clientsession(hass, verify_ssl=False) requester = AiohttpSessionRequester(session, with_sleep=True, timeout=20) - factory = UpnpFactory(requester, disable_state_variable_validation=True) + factory = UpnpFactory(requester, non_strict=True) upnp_device = await factory.async_create_device(ssdp_location) # Create profile wrapper. @@ -134,69 +135,35 @@ class Device: """Get string representation.""" return f"IGD Device: {self.name}/{self.udn}::{self.device_type}" - async def async_get_traffic_data(self) -> Mapping[str, Any]: - """ - Get all traffic data in one go. + async def async_get_data(self) -> Mapping[str, Any]: + """Get all data from device.""" + _LOGGER.debug("Getting data for device: %s", self) + igd_state = await self._igd_device.async_get_traffic_and_status_data() + status_info = igd_state.status_info + if status_info is not None and not isinstance(status_info, Exception): + wan_status = status_info.connection_status + router_uptime = status_info.uptime + else: + wan_status = None + router_uptime = None - Traffic data consists of: - - total bytes sent - - total bytes received - - total packets sent - - total packats received + def get_value(value: Any) -> Any: + if value is None or isinstance(value, Exception): + return None - Data is timestamped. - """ - _LOGGER.debug("Getting traffic statistics from device: %s", self) - - values = await asyncio.gather( - self._igd_device.async_get_total_bytes_received(), - self._igd_device.async_get_total_bytes_sent(), - self._igd_device.async_get_total_packets_received(), - self._igd_device.async_get_total_packets_sent(), - ) + return value return { - TIMESTAMP: utcnow(), - BYTES_RECEIVED: values[0], - BYTES_SENT: values[1], - PACKETS_RECEIVED: values[2], - PACKETS_SENT: values[3], - } - - async def async_get_status(self) -> Mapping[str, Any]: - """Get connection status, uptime, and external IP.""" - _LOGGER.debug("Getting status for device: %s", self) - - values = await asyncio.gather( - self._igd_device.async_get_status_info(), - self._igd_device.async_get_external_ip_address(), - return_exceptions=True, - ) - status_info: StatusInfo | None = None - router_ip: str | None = None - - for idx, value in enumerate(values): - if isinstance(value, UpnpError): - # Not all routers support some of these items although based - # on defined standard they should. - _LOGGER.debug( - "Exception occurred while trying to get status %s for device %s: %s", - "status" if idx == 1 else "external IP address", - self, - str(value), - ) - continue - - if isinstance(value, Exception): - raise value - - if isinstance(value, StatusInfo): - status_info = value - elif isinstance(value, str): - router_ip = value - - return { - WAN_STATUS: status_info[0] if status_info is not None else None, - ROUTER_UPTIME: status_info[2] if status_info is not None else None, - ROUTER_IP: router_ip, + TIMESTAMP: igd_state.timestamp, + BYTES_RECEIVED: get_value(igd_state.bytes_received), + BYTES_SENT: get_value(igd_state.bytes_sent), + PACKETS_RECEIVED: get_value(igd_state.packets_received), + PACKETS_SENT: get_value(igd_state.packets_sent), + WAN_STATUS: wan_status, + ROUTER_UPTIME: router_uptime, + ROUTER_IP: get_value(igd_state.external_ip_address), + KIBIBYTES_PER_SEC_RECEIVED: igd_state.kibibytes_per_sec_received, + KIBIBYTES_PER_SEC_SENT: igd_state.kibibytes_per_sec_sent, + PACKETS_PER_SEC_RECEIVED: igd_state.packets_per_sec_received, + PACKETS_PER_SEC_SENT: igd_state.packets_per_sec_sent, } diff --git a/homeassistant/components/upnp/entity.py b/homeassistant/components/upnp/entity.py new file mode 100644 index 00000000000..b787018adcc --- /dev/null +++ b/homeassistant/components/upnp/entity.py @@ -0,0 +1,54 @@ +"""Entity for UPnP/IGD.""" +from __future__ import annotations + +from dataclasses import dataclass + +from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .coordinator import UpnpDataUpdateCoordinator + + +@dataclass +class UpnpEntityDescription(EntityDescription): + """UPnP entity description.""" + + format: str = "s" + unique_id: str | None = None + value_key: str | None = None + + def __post_init__(self): + """Post initialize.""" + self.value_key = self.value_key or self.key + + +class UpnpEntity(CoordinatorEntity[UpnpDataUpdateCoordinator]): + """Base class for UPnP/IGD entities.""" + + entity_description: UpnpEntityDescription + + def __init__( + self, + coordinator: UpnpDataUpdateCoordinator, + entity_description: UpnpEntityDescription, + ) -> None: + """Initialize the base entities.""" + super().__init__(coordinator) + self._device = coordinator.device + self.entity_description = entity_description + self._attr_name = f"{coordinator.device.name} {entity_description.name}" + self._attr_unique_id = f"{coordinator.device.original_udn}_{entity_description.unique_id or entity_description.key}" + self._attr_device_info = DeviceInfo( + connections=coordinator.device_entry.connections, + name=coordinator.device_entry.name, + manufacturer=coordinator.device_entry.manufacturer, + model=coordinator.device_entry.model, + configuration_url=coordinator.device_entry.configuration_url, + ) + + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and ( + self.coordinator.data.get(self.entity_description.key) is not None + ) diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index c574a5d7269..9b4151c35c5 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -15,5 +15,6 @@ } ], "iot_class": "local_polling", - "loggers": ["async_upnp_client"] + "loggers": ["async_upnp_client"], + "integration_type": "device" } diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 53a918ba053..3d0c71fafdb 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -1,31 +1,46 @@ """Support for UPnP/IGD Sensors.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity +from dataclasses import dataclass + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_BYTES, DATA_RATE_KIBIBYTES_PER_SECOND, TIME_SECONDS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import UpnpDataUpdateCoordinator, UpnpEntity, UpnpSensorEntityDescription from .const import ( BYTES_RECEIVED, BYTES_SENT, DATA_PACKETS, DATA_RATE_PACKETS_PER_SECOND, DOMAIN, - KIBIBYTE, + KIBIBYTES_PER_SEC_RECEIVED, + KIBIBYTES_PER_SEC_SENT, LOGGER, + PACKETS_PER_SEC_RECEIVED, + PACKETS_PER_SEC_SENT, PACKETS_RECEIVED, PACKETS_SENT, ROUTER_IP, ROUTER_UPTIME, - TIMESTAMP, WAN_STATUS, ) +from .coordinator import UpnpDataUpdateCoordinator +from .entity import UpnpEntity, UpnpEntityDescription -RAW_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( + +@dataclass +class UpnpSensorEntityDescription(UpnpEntityDescription, SensorEntityDescription): + """A class that describes a sensor UPnP entities.""" + + +SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( UpnpSensorEntityDescription( key=BYTES_RECEIVED, name=f"{DATA_BYTES} received", @@ -33,6 +48,7 @@ RAW_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( native_unit_of_measurement=DATA_BYTES, format="d", entity_registry_enabled_default=False, + state_class=SensorStateClass.TOTAL_INCREASING, ), UpnpSensorEntityDescription( key=BYTES_SENT, @@ -41,6 +57,7 @@ RAW_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( native_unit_of_measurement=DATA_BYTES, format="d", entity_registry_enabled_default=False, + state_class=SensorStateClass.TOTAL_INCREASING, ), UpnpSensorEntityDescription( key=PACKETS_RECEIVED, @@ -49,6 +66,7 @@ RAW_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( native_unit_of_measurement=DATA_PACKETS, format="d", entity_registry_enabled_default=False, + state_class=SensorStateClass.TOTAL_INCREASING, ), UpnpSensorEntityDescription( key=PACKETS_SENT, @@ -57,11 +75,13 @@ RAW_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( native_unit_of_measurement=DATA_PACKETS, format="d", entity_registry_enabled_default=False, + state_class=SensorStateClass.TOTAL_INCREASING, ), UpnpSensorEntityDescription( key=ROUTER_IP, name="External IP", icon="mdi:server-network", + entity_category=EntityCategory.DIAGNOSTIC, ), UpnpSensorEntityDescription( key=ROUTER_UPTIME, @@ -79,42 +99,47 @@ RAW_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), -) - -DERIVED_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( UpnpSensorEntityDescription( key=BYTES_RECEIVED, + value_key=KIBIBYTES_PER_SEC_RECEIVED, unique_id="KiB/sec_received", name=f"{DATA_RATE_KIBIBYTES_PER_SECOND} received", icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_KIBIBYTES_PER_SECOND, format=".1f", + state_class=SensorStateClass.MEASUREMENT, ), UpnpSensorEntityDescription( key=BYTES_SENT, + value_key=KIBIBYTES_PER_SEC_SENT, unique_id="KiB/sec_sent", name=f"{DATA_RATE_KIBIBYTES_PER_SECOND} sent", icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_KIBIBYTES_PER_SECOND, format=".1f", + state_class=SensorStateClass.MEASUREMENT, ), UpnpSensorEntityDescription( key=PACKETS_RECEIVED, + value_key=PACKETS_PER_SEC_RECEIVED, unique_id="packets/sec_received", name=f"{DATA_RATE_PACKETS_PER_SECOND} received", icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_PACKETS_PER_SECOND, format=".1f", entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, ), UpnpSensorEntityDescription( key=PACKETS_SENT, + value_key=PACKETS_PER_SEC_SENT, unique_id="packets/sec_sent", name=f"{DATA_RATE_PACKETS_PER_SECOND} sent", icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_PACKETS_PER_SECOND, format=".1f", entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, ), ) @@ -125,26 +150,16 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the UPnP/IGD sensors.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator: UpnpDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] entities: list[UpnpSensor] = [ - RawUpnpSensor( + UpnpSensor( coordinator=coordinator, entity_description=entity_description, ) - for entity_description in RAW_SENSORS + for entity_description in SENSOR_DESCRIPTIONS if coordinator.data.get(entity_description.key) is not None ] - entities.extend( - [ - DerivedUpnpSensor( - coordinator=coordinator, - entity_description=entity_description, - ) - for entity_description in DERIVED_SENSORS - if coordinator.data.get(entity_description.key) is not None - ] - ) LOGGER.debug("Adding sensor entities: %s", entities) async_add_entities(entities) @@ -155,64 +170,10 @@ class UpnpSensor(UpnpEntity, SensorEntity): entity_description: UpnpSensorEntityDescription - -class RawUpnpSensor(UpnpSensor): - """Representation of a UPnP/IGD sensor.""" - @property def native_value(self) -> str | None: """Return the state of the device.""" - value = self.coordinator.data[self.entity_description.key] + value = self.coordinator.data[self.entity_description.value_key] if value is None: return None return format(value, self.entity_description.format) - - -class DerivedUpnpSensor(UpnpSensor): - """Representation of a UNIT Sent/Received per second sensor.""" - - def __init__( - self, - coordinator: UpnpDataUpdateCoordinator, - entity_description: UpnpSensorEntityDescription, - ) -> None: - """Initialize sensor.""" - super().__init__(coordinator=coordinator, entity_description=entity_description) - self._last_value = None - self._last_timestamp = None - - def _has_overflowed(self, current_value) -> bool: - """Check if value has overflowed.""" - return current_value < self._last_value - - @property - def native_value(self) -> str | None: - """Return the state of the device.""" - # Can't calculate any derivative if we have only one value. - current_value = self.coordinator.data[self.entity_description.key] - if current_value is None: - return None - current_timestamp = self.coordinator.data[TIMESTAMP] - if self._last_value is None or self._has_overflowed(current_value): - self._last_value = current_value - self._last_timestamp = current_timestamp - return None - - # Calculate derivative. - delta_value = current_value - self._last_value - if ( - self.entity_description.native_unit_of_measurement - == DATA_RATE_KIBIBYTES_PER_SECOND - ): - delta_value /= KIBIBYTE - delta_time = current_timestamp - self._last_timestamp - if delta_time.total_seconds() == 0: - # Prevent division by 0. - return None - derived = delta_value / delta_time.total_seconds() - - # Store current values for future use. - self._last_value = current_value - self._last_timestamp = current_timestamp - - return format(derived, self.entity_description.format) diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 09a7a1f4a16..08317d06a5c 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5627,7 +5627,7 @@ }, "upnp": { "name": "UPnP/IGD", - "integration_type": "hub", + "integration_type": "device", "config_flow": true, "iot_class": "local_polling" }, diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index aee25a5d112..f26fb39e42a 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -1,11 +1,12 @@ """Configuration for SSDP tests.""" from __future__ import annotations +from datetime import datetime from unittest.mock import AsyncMock, MagicMock, PropertyMock, create_autospec, patch from urllib.parse import urlparse from async_upnp_client.client import UpnpDevice -from async_upnp_client.profiles.igd import IgdDevice, StatusInfo +from async_upnp_client.profiles.igd import IgdDevice, IgdState, StatusInfo import pytest from homeassistant.components import ssdp @@ -65,16 +66,23 @@ def mock_igd_device() -> IgdDevice: mock_igd_device.udn = TEST_DISCOVERY.ssdp_udn mock_igd_device.device = mock_upnp_device - mock_igd_device.async_get_total_bytes_received.return_value = 0 - mock_igd_device.async_get_total_bytes_sent.return_value = 0 - mock_igd_device.async_get_total_packets_received.return_value = 0 - mock_igd_device.async_get_total_packets_sent.return_value = 0 - mock_igd_device.async_get_status_info.return_value = StatusInfo( - "Connected", - "", - 10, + mock_igd_device.async_get_traffic_and_status_data.return_value = IgdState( + timestamp=datetime.now(), + bytes_received=0, + bytes_sent=0, + packets_received=0, + packets_sent=0, + status_info=StatusInfo( + "Connected", + "", + 10, + ), + external_ip_address="8.9.10.11", + kibibytes_per_sec_received=None, + kibibytes_per_sec_sent=None, + packets_per_sec_received=None, + packets_per_sec_sent=None, ) - mock_igd_device.async_get_external_ip_address.return_value = "8.9.10.11" with patch( "homeassistant.components.upnp.device.UpnpFactory.async_create_device" diff --git a/tests/components/upnp/test_binary_sensor.py b/tests/components/upnp/test_binary_sensor.py index 24e5cdce47c..769a5d790c8 100644 --- a/tests/components/upnp/test_binary_sensor.py +++ b/tests/components/upnp/test_binary_sensor.py @@ -1,8 +1,8 @@ """Tests for UPnP/IGD binary_sensor.""" -from datetime import timedelta +from datetime import datetime, timedelta -from async_upnp_client.profiles.igd import StatusInfo +from async_upnp_client.profiles.igd import IgdDevice, IgdState, StatusInfo from homeassistant.components.upnp.const import DEFAULT_SCAN_INTERVAL from homeassistant.core import HomeAssistant @@ -20,11 +20,23 @@ async def test_upnp_binary_sensors( assert wan_status_state.state == "on" # Second poll. - mock_igd_device = mock_config_entry.igd_device - mock_igd_device.async_get_status_info.return_value = StatusInfo( - "Disconnected", - "", - 40, + mock_igd_device: IgdDevice = mock_config_entry.igd_device + mock_igd_device.async_get_traffic_and_status_data.return_value = IgdState( + timestamp=datetime.now(), + bytes_received=0, + bytes_sent=0, + packets_received=0, + packets_sent=0, + status_info=StatusInfo( + "Disconnected", + "", + 40, + ), + external_ip_address="8.9.10.11", + kibibytes_per_sec_received=None, + kibibytes_per_sec_sent=None, + packets_per_sec_received=None, + packets_per_sec_sent=None, ) async_fire_time_changed( diff --git a/tests/components/upnp/test_sensor.py b/tests/components/upnp/test_sensor.py index 2abd357ac31..f5eb69bfae9 100644 --- a/tests/components/upnp/test_sensor.py +++ b/tests/components/upnp/test_sensor.py @@ -1,10 +1,8 @@ """Tests for UPnP/IGD sensor.""" -from datetime import timedelta -from unittest.mock import patch +from datetime import datetime, timedelta -from async_upnp_client.profiles.igd import StatusInfo -import pytest +from async_upnp_client.profiles.igd import IgdDevice, IgdState, StatusInfo from homeassistant.components.upnp.const import DEFAULT_SCAN_INTERVAL from homeassistant.core import HomeAssistant @@ -14,7 +12,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed async def test_upnp_sensors(hass: HomeAssistant, mock_config_entry: MockConfigEntry): - """Test normal sensors.""" + """Test sensors.""" # First poll. assert hass.states.get("sensor.mock_name_b_received").state == "0" assert hass.states.get("sensor.mock_name_b_sent").state == "0" @@ -22,19 +20,30 @@ async def test_upnp_sensors(hass: HomeAssistant, mock_config_entry: MockConfigEn assert hass.states.get("sensor.mock_name_packets_sent").state == "0" assert hass.states.get("sensor.mock_name_external_ip").state == "8.9.10.11" assert hass.states.get("sensor.mock_name_wan_status").state == "Connected" + assert hass.states.get("sensor.mock_name_kib_s_received").state == "unknown" + assert hass.states.get("sensor.mock_name_kib_s_sent").state == "unknown" + assert hass.states.get("sensor.mock_name_packets_s_received").state == "unknown" + assert hass.states.get("sensor.mock_name_packets_s_sent").state == "unknown" # Second poll. - mock_igd_device = mock_config_entry.igd_device - mock_igd_device.async_get_total_bytes_received.return_value = 10240 - mock_igd_device.async_get_total_bytes_sent.return_value = 20480 - mock_igd_device.async_get_total_packets_received.return_value = 30 - mock_igd_device.async_get_total_packets_sent.return_value = 40 - mock_igd_device.async_get_status_info.return_value = StatusInfo( - "Disconnected", - "", - 40, + mock_igd_device: IgdDevice = mock_config_entry.igd_device + mock_igd_device.async_get_traffic_and_status_data.return_value = IgdState( + timestamp=datetime.now(), + bytes_received=10240, + bytes_sent=20480, + packets_received=30, + packets_sent=40, + status_info=StatusInfo( + "Disconnected", + "", + 40, + ), + external_ip_address="", + kibibytes_per_sec_received=10.0, + kibibytes_per_sec_sent=20.0, + packets_per_sec_received=30.0, + packets_per_sec_sent=40.0, ) - mock_igd_device.async_get_external_ip_address.return_value = "" now = dt_util.utcnow() async_fire_time_changed(hass, now + timedelta(seconds=DEFAULT_SCAN_INTERVAL)) @@ -46,50 +55,7 @@ async def test_upnp_sensors(hass: HomeAssistant, mock_config_entry: MockConfigEn assert hass.states.get("sensor.mock_name_packets_sent").state == "40" assert hass.states.get("sensor.mock_name_external_ip").state == "" assert hass.states.get("sensor.mock_name_wan_status").state == "Disconnected" - - -async def test_derived_upnp_sensors( - hass: HomeAssistant, mock_config_entry: MockConfigEntry -): - """Test derived sensors.""" - # First poll. - assert hass.states.get("sensor.mock_name_kib_s_received").state == "unknown" - assert hass.states.get("sensor.mock_name_kib_s_sent").state == "unknown" - assert hass.states.get("sensor.mock_name_packets_s_received").state == "unknown" - assert hass.states.get("sensor.mock_name_packets_s_sent").state == "unknown" - - # Second poll. - mock_igd_device = mock_config_entry.igd_device - mock_igd_device.async_get_total_bytes_received.return_value = int( - 10240 * DEFAULT_SCAN_INTERVAL - ) - mock_igd_device.async_get_total_bytes_sent.return_value = int( - 20480 * DEFAULT_SCAN_INTERVAL - ) - mock_igd_device.async_get_total_packets_received.return_value = int( - 30 * DEFAULT_SCAN_INTERVAL - ) - mock_igd_device.async_get_total_packets_sent.return_value = int( - 40 * DEFAULT_SCAN_INTERVAL - ) - - now = dt_util.utcnow() - with patch( - "homeassistant.components.upnp.device.utcnow", - return_value=now + timedelta(seconds=DEFAULT_SCAN_INTERVAL), - ): - async_fire_time_changed(hass, now + timedelta(seconds=DEFAULT_SCAN_INTERVAL)) - await hass.async_block_till_done() - - assert float( - hass.states.get("sensor.mock_name_kib_s_received").state - ) == pytest.approx(10.0, rel=0.1) - assert float( - hass.states.get("sensor.mock_name_kib_s_sent").state - ) == pytest.approx(20.0, rel=0.1) - assert float( - hass.states.get("sensor.mock_name_packets_s_received").state - ) == pytest.approx(30.0, rel=0.1) - assert float( - hass.states.get("sensor.mock_name_packets_s_sent").state - ) == pytest.approx(40.0, rel=0.1) + assert hass.states.get("sensor.mock_name_kib_s_received").state == "10.0" + assert hass.states.get("sensor.mock_name_kib_s_sent").state == "20.0" + assert hass.states.get("sensor.mock_name_packets_s_received").state == "30.0" + assert hass.states.get("sensor.mock_name_packets_s_sent").state == "40.0" From d107d8df7896a80e2220531b0cdd02fa0fd687ec Mon Sep 17 00:00:00 2001 From: Klaas Neirinck Date: Wed, 26 Oct 2022 21:37:39 +0200 Subject: [PATCH 876/985] Improve readability by reducing indentation (#81040) --- homeassistant/components/comfoconnect/fan.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index 7577dd77aeb..5341a5f6925 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -160,12 +160,12 @@ class ComfoConnectFan(FanEntity): def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if self.preset_modes and preset_mode in self.preset_modes: - _LOGGER.debug("Changing preset mode to %s", preset_mode) - if preset_mode == PRESET_MODE_AUTO: - # force set it to manual first - self._ccb.comfoconnect.cmd_rmi_request(CMD_MODE_MANUAL) - # now set it to auto so any previous percentage set gets undone - self._ccb.comfoconnect.cmd_rmi_request(CMD_MODE_AUTO) - else: + if not self.preset_modes or preset_mode not in self.preset_modes: raise ValueError(f"Invalid preset mode: {preset_mode}") + + _LOGGER.debug("Changing preset mode to %s", preset_mode) + if preset_mode == PRESET_MODE_AUTO: + # force set it to manual first + self._ccb.comfoconnect.cmd_rmi_request(CMD_MODE_MANUAL) + # now set it to auto so any previous percentage set gets undone + self._ccb.comfoconnect.cmd_rmi_request(CMD_MODE_AUTO) From 2a6f2f431d628e18a5b9d21a58111bcee321547b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Oct 2022 14:39:27 -0500 Subject: [PATCH 877/985] Bump dbus-fast to 1.49.0 (#81043) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index a725ecbc70e..a3348a1611b 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.4.2", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.47.0" + "dbus-fast==1.49.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f26e78bdd13..c7dd51d76df 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.47.0 +dbus-fast==1.49.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1948c3ecedd..4a6b3708aae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.47.0 +dbus-fast==1.49.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13c359f787e..1b39523ca42 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.47.0 +dbus-fast==1.49.0 # homeassistant.components.debugpy debugpy==1.6.3 From cf0f79294bef3c138ecdf1d12a2c86e028399c6f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Oct 2022 21:50:53 +0200 Subject: [PATCH 878/985] Bumped version to 2022.11.0b0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 112b1637c46..c194782ed29 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 3ca463d6fc1..a869da99baa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.0.dev0" +version = "2022.11.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 11cc7e156626991353ff78efd21378626c570ecb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Oct 2022 21:51:09 +0200 Subject: [PATCH 879/985] Add WS API recorder/statistic_during_period (#80663) --- .../components/recorder/statistics.py | 373 ++++++++++++- .../components/recorder/websocket_api.py | 148 +++++- .../components/recorder/test_websocket_api.py | 491 +++++++++++++++++- 3 files changed, 987 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 2b249aeeb14..8a744fd4daa 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -1113,6 +1113,377 @@ def _statistics_during_period_stmt_short_term( return stmt +def _get_max_mean_min_statistic_in_sub_period( + session: Session, + result: dict[str, float], + start_time: datetime | None, + end_time: datetime | None, + table: type[Statistics | StatisticsShortTerm], + types: set[str], + metadata_id: int, +) -> None: + """Return max, mean and min during the period.""" + # Calculate max, mean, min + columns = [] + if "max" in types: + columns.append(func.max(table.max)) + if "mean" in types: + columns.append(func.avg(table.mean)) + columns.append(func.count(table.mean)) + if "min" in types: + columns.append(func.min(table.min)) + stmt = lambda_stmt(lambda: select(columns).filter(table.metadata_id == metadata_id)) + if start_time is not None: + stmt += lambda q: q.filter(table.start >= start_time) + if end_time is not None: + stmt += lambda q: q.filter(table.start < end_time) + stats = execute_stmt_lambda_element(session, stmt) + if "max" in types and stats and (new_max := stats[0].max) is not None: + old_max = result.get("max") + result["max"] = max(new_max, old_max) if old_max is not None else new_max + if "mean" in types and stats and stats[0].avg is not None: + duration = stats[0].count * table.duration.total_seconds() + result["duration"] = result.get("duration", 0.0) + duration + result["mean_acc"] = result.get("mean_acc", 0.0) + stats[0].avg * duration + if "min" in types and stats and (new_min := stats[0].min) is not None: + old_min = result.get("min") + result["min"] = min(new_min, old_min) if old_min is not None else new_min + + +def _get_max_mean_min_statistic( + session: Session, + head_start_time: datetime | None, + head_end_time: datetime | None, + main_start_time: datetime | None, + main_end_time: datetime | None, + tail_start_time: datetime | None, + tail_end_time: datetime | None, + tail_only: bool, + metadata_id: int, + types: set[str], +) -> dict[str, float | None]: + """Return max, mean and min during the period. + + The mean is a time weighted average, combining hourly and 5-minute statistics if + necessary. + """ + max_mean_min: dict[str, float] = {} + result: dict[str, float | None] = {} + + if tail_start_time is not None: + # Calculate max, mean, min + _get_max_mean_min_statistic_in_sub_period( + session, + max_mean_min, + tail_start_time, + tail_end_time, + StatisticsShortTerm, + types, + metadata_id, + ) + + if not tail_only: + _get_max_mean_min_statistic_in_sub_period( + session, + max_mean_min, + main_start_time, + main_end_time, + Statistics, + types, + metadata_id, + ) + + if head_start_time is not None: + _get_max_mean_min_statistic_in_sub_period( + session, + max_mean_min, + head_start_time, + head_end_time, + StatisticsShortTerm, + types, + metadata_id, + ) + + if "max" in types: + result["max"] = max_mean_min.get("max") + if "mean" in types: + if "mean_acc" not in max_mean_min: + result["mean"] = None + else: + result["mean"] = max_mean_min["mean_acc"] / max_mean_min["duration"] + if "min" in types: + result["min"] = max_mean_min.get("min") + return result + + +def _get_oldest_sum_statistic( + session: Session, + head_start_time: datetime | None, + main_start_time: datetime | None, + tail_start_time: datetime | None, + tail_only: bool, + metadata_id: int, +) -> float | None: + """Return the oldest non-NULL sum during the period.""" + + def _get_oldest_sum_statistic_in_sub_period( + session: Session, + start_time: datetime | None, + table: type[Statistics | StatisticsShortTerm], + metadata_id: int, + ) -> tuple[float | None, datetime | None]: + """Return the oldest non-NULL sum during the period.""" + stmt = lambda_stmt( + lambda: select(table.sum, table.start) + .filter(table.metadata_id == metadata_id) + .filter(table.sum.is_not(None)) + .order_by(table.start.asc()) + .limit(1) + ) + if start_time is not None: + start_time = start_time + table.duration - timedelta.resolution + if table == StatisticsShortTerm: + minutes = start_time.minute - start_time.minute % 5 + period = start_time.replace(minute=minutes, second=0, microsecond=0) + else: + period = start_time.replace(minute=0, second=0, microsecond=0) + prev_period = period - table.duration + stmt += lambda q: q.filter(table.start == prev_period) + stats = execute_stmt_lambda_element(session, stmt) + return ( + (stats[0].sum, process_timestamp(stats[0].start)) if stats else (None, None) + ) + + oldest_start: datetime | None + oldest_sum: float | None = None + + if head_start_time is not None: + oldest_sum, oldest_start = _get_oldest_sum_statistic_in_sub_period( + session, head_start_time, StatisticsShortTerm, metadata_id + ) + if ( + oldest_start is not None + and oldest_start < head_start_time + and oldest_sum is not None + ): + return oldest_sum + + if not tail_only: + assert main_start_time is not None + oldest_sum, oldest_start = _get_oldest_sum_statistic_in_sub_period( + session, main_start_time, Statistics, metadata_id + ) + if ( + oldest_start is not None + and oldest_start < main_start_time + and oldest_sum is not None + ): + return oldest_sum + return 0 + + if tail_start_time is not None: + oldest_sum, oldest_start = _get_oldest_sum_statistic_in_sub_period( + session, tail_start_time, StatisticsShortTerm, metadata_id + ) + if ( + oldest_start is not None + and oldest_start < tail_start_time + and oldest_sum is not None + ): + return oldest_sum + + return 0 + + +def _get_newest_sum_statistic( + session: Session, + head_start_time: datetime | None, + head_end_time: datetime | None, + main_start_time: datetime | None, + main_end_time: datetime | None, + tail_start_time: datetime | None, + tail_end_time: datetime | None, + tail_only: bool, + metadata_id: int, +) -> float | None: + """Return the newest non-NULL sum during the period.""" + + def _get_newest_sum_statistic_in_sub_period( + session: Session, + start_time: datetime | None, + end_time: datetime | None, + table: type[Statistics | StatisticsShortTerm], + metadata_id: int, + ) -> float | None: + """Return the newest non-NULL sum during the period.""" + stmt = lambda_stmt( + lambda: select( + table.sum, + ) + .filter(table.metadata_id == metadata_id) + .filter(table.sum.is_not(None)) + .order_by(table.start.desc()) + .limit(1) + ) + if start_time is not None: + stmt += lambda q: q.filter(table.start >= start_time) + if end_time is not None: + stmt += lambda q: q.filter(table.start < end_time) + stats = execute_stmt_lambda_element(session, stmt) + + return stats[0].sum if stats else None + + newest_sum: float | None = None + + if tail_start_time is not None: + newest_sum = _get_newest_sum_statistic_in_sub_period( + session, tail_start_time, tail_end_time, StatisticsShortTerm, metadata_id + ) + if newest_sum is not None: + return newest_sum + + if not tail_only: + newest_sum = _get_newest_sum_statistic_in_sub_period( + session, main_start_time, main_end_time, Statistics, metadata_id + ) + if newest_sum is not None: + return newest_sum + + if head_start_time is not None: + newest_sum = _get_newest_sum_statistic_in_sub_period( + session, head_start_time, head_end_time, StatisticsShortTerm, metadata_id + ) + + return newest_sum + + +def statistic_during_period( + hass: HomeAssistant, + start_time: datetime | None, + end_time: datetime | None, + statistic_id: str, + types: set[str] | None, + units: dict[str, str] | None, +) -> dict[str, Any]: + """Return a statistic data point for the UTC period start_time - end_time.""" + metadata = None + + if not types: + types = {"max", "mean", "min", "change"} + + result: dict[str, Any] = {} + + # To calculate the summary, data from the statistics (hourly) and short_term_statistics + # (5 minute) tables is combined + # - The short term statistics table is used for the head and tail of the period, + # if the period it doesn't start or end on a full hour + # - The statistics table is used for the remainder of the time + now = dt_util.utcnow() + if end_time is not None and end_time > now: + end_time = now + + tail_only = ( + start_time is not None + and end_time is not None + and end_time - start_time < timedelta(hours=1) + ) + + # Calculate the head period + head_start_time: datetime | None = None + head_end_time: datetime | None = None + if not tail_only and start_time is not None and start_time.minute: + head_start_time = start_time + head_end_time = start_time.replace( + minute=0, second=0, microsecond=0 + ) + timedelta(hours=1) + + # Calculate the tail period + tail_start_time: datetime | None = None + tail_end_time: datetime | None = None + if end_time is None: + tail_start_time = now.replace(minute=0, second=0, microsecond=0) + elif end_time.minute: + tail_start_time = ( + start_time + if tail_only + else end_time.replace(minute=0, second=0, microsecond=0) + ) + tail_end_time = end_time + + # Calculate the main period + main_start_time: datetime | None = None + main_end_time: datetime | None = None + if not tail_only: + main_start_time = start_time if head_end_time is None else head_end_time + main_end_time = end_time if tail_start_time is None else tail_start_time + + with session_scope(hass=hass) as session: + # Fetch metadata for the given statistic_id + metadata = get_metadata_with_session(session, statistic_ids=[statistic_id]) + if not metadata: + return result + + metadata_id = metadata[statistic_id][0] + + if not types.isdisjoint({"max", "mean", "min"}): + result = _get_max_mean_min_statistic( + session, + head_start_time, + head_end_time, + main_start_time, + main_end_time, + tail_start_time, + tail_end_time, + tail_only, + metadata_id, + types, + ) + + if "change" in types: + oldest_sum: float | None + if start_time is None: + oldest_sum = 0.0 + else: + oldest_sum = _get_oldest_sum_statistic( + session, + head_start_time, + main_start_time, + tail_start_time, + tail_only, + metadata_id, + ) + newest_sum = _get_newest_sum_statistic( + session, + head_start_time, + head_end_time, + main_start_time, + main_end_time, + tail_start_time, + tail_end_time, + tail_only, + metadata_id, + ) + # Calculate the difference between the oldest and newest sum + if oldest_sum is not None and newest_sum is not None: + result["change"] = newest_sum - oldest_sum + else: + result["change"] = None + + def no_conversion(val: float | None) -> float | None: + """Return val.""" + return val + + state_unit = unit = metadata[statistic_id][1]["unit_of_measurement"] + if state := hass.states.get(statistic_id): + state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + if unit is not None: + convert = _get_statistic_to_display_unit_converter(unit, state_unit, units) + else: + convert = no_conversion + + return {key: convert(value) for key, value in result.items()} + + def statistics_during_period( hass: HomeAssistant, start_time: datetime, @@ -1122,7 +1493,7 @@ def statistics_during_period( start_time_as_datetime: bool = False, units: dict[str, str] | None = None, ) -> dict[str, list[dict[str, Any]]]: - """Return statistics during UTC period start_time - end_time for the statistic_ids. + """Return statistic data points during UTC period start_time - end_time. If end_time is omitted, returns statistics newer than or equal to start_time. If statistic_ids is omitted, returns statistics for all statistics ids. diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 2079d9537b5..9b2ef417755 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -1,7 +1,7 @@ """The Recorder websocket API.""" from __future__ import annotations -from datetime import datetime as dt +from datetime import datetime as dt, timedelta import logging from typing import Any, Literal @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.websocket_api import messages from homeassistant.core import HomeAssistant, callback, valid_entity_id +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import JSON_DUMP from homeassistant.util import dt as dt_util @@ -31,6 +32,7 @@ from .statistics import ( async_change_statistics_unit, async_import_statistics, list_statistic_ids, + statistic_during_period, statistics_during_period, validate_statistics, ) @@ -47,6 +49,7 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_backup_start) websocket_api.async_register_command(hass, ws_change_statistics_unit) websocket_api.async_register_command(hass, ws_clear_statistics) + websocket_api.async_register_command(hass, ws_get_statistic_during_period) websocket_api.async_register_command(hass, ws_get_statistics_during_period) websocket_api.async_register_command(hass, ws_get_statistics_metadata) websocket_api.async_register_command(hass, ws_list_statistic_ids) @@ -56,6 +59,149 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_validate_statistics) +def _ws_get_statistic_during_period( + hass: HomeAssistant, + msg_id: int, + start_time: dt | None, + end_time: dt | None, + statistic_id: str, + types: set[str] | None, + units: dict[str, str], +) -> str: + """Fetch statistics and convert them to json in the executor.""" + return JSON_DUMP( + messages.result_message( + msg_id, + statistic_during_period( + hass, start_time, end_time, statistic_id, types, units=units + ), + ) + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "recorder/statistic_during_period", + vol.Exclusive("calendar", "period"): vol.Schema( + { + vol.Required("period"): vol.Any("hour", "day", "week", "month", "year"), + vol.Optional("offset"): int, + } + ), + vol.Exclusive("fixed_period", "period"): vol.Schema( + { + vol.Optional("start_time"): str, + vol.Optional("end_time"): str, + } + ), + vol.Exclusive("rolling_window", "period"): vol.Schema( + { + vol.Required("duration"): cv.time_period_dict, + vol.Optional("offset"): cv.time_period_dict, + } + ), + vol.Optional("statistic_id"): str, + vol.Optional("types"): vol.All([str], vol.Coerce(set)), + vol.Optional("units"): vol.Schema( + { + vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS), + vol.Optional("energy"): vol.In(EnergyConverter.VALID_UNITS), + vol.Optional("mass"): vol.In(MassConverter.VALID_UNITS), + vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS), + vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS), + vol.Optional("speed"): vol.In(SpeedConverter.VALID_UNITS), + vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS), + vol.Optional("volume"): vol.In(VolumeConverter.VALID_UNITS), + } + ), + } +) +@websocket_api.async_response +async def ws_get_statistic_during_period( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle statistics websocket command.""" + if ("start_time" in msg or "end_time" in msg) and "duration" in msg: + raise HomeAssistantError + if "offset" in msg and "duration" not in msg: + raise HomeAssistantError + + start_time = None + end_time = None + + if "calendar" in msg: + calendar_period = msg["calendar"]["period"] + start_of_day = dt_util.start_of_local_day() + offset = msg["calendar"].get("offset", 0) + if calendar_period == "hour": + start_time = dt_util.now().replace(minute=0, second=0, microsecond=0) + start_time += timedelta(hours=offset) + end_time = start_time + timedelta(hours=1) + elif calendar_period == "day": + start_time = start_of_day + start_time += timedelta(days=offset) + end_time = start_time + timedelta(days=1) + elif calendar_period == "week": + start_time = start_of_day - timedelta(days=start_of_day.weekday()) + start_time += timedelta(days=offset * 7) + end_time = start_time + timedelta(weeks=1) + elif calendar_period == "month": + start_time = start_of_day.replace(day=28) + # This works for up to 48 months of offset + start_time = (start_time + timedelta(days=offset * 31)).replace(day=1) + end_time = (start_time + timedelta(days=31)).replace(day=1) + else: # calendar_period = "year" + start_time = start_of_day.replace(month=12, day=31) + # This works for 100+ years of offset + start_time = (start_time + timedelta(days=offset * 366)).replace( + month=1, day=1 + ) + end_time = (start_time + timedelta(days=365)).replace(day=1) + + start_time = dt_util.as_utc(start_time) + end_time = dt_util.as_utc(end_time) + + elif "fixed_period" in msg: + if start_time_str := msg["fixed_period"].get("start_time"): + if start_time := dt_util.parse_datetime(start_time_str): + start_time = dt_util.as_utc(start_time) + else: + connection.send_error( + msg["id"], "invalid_start_time", "Invalid start_time" + ) + return + + if end_time_str := msg["fixed_period"].get("end_time"): + if end_time := dt_util.parse_datetime(end_time_str): + end_time = dt_util.as_utc(end_time) + else: + connection.send_error(msg["id"], "invalid_end_time", "Invalid end_time") + return + + elif "rolling_window" in msg: + duration = msg["rolling_window"]["duration"] + now = dt_util.utcnow() + start_time = now - duration + end_time = start_time + duration + + if offset := msg["rolling_window"].get("offset"): + start_time += offset + end_time += offset + + connection.send_message( + await get_instance(hass).async_add_executor_job( + _ws_get_statistic_during_period, + hass, + msg["id"], + start_time, + end_time, + msg.get("statistic_id"), + msg.get("types"), + msg.get("units"), + ) + ) + + def _ws_get_statistics_during_period( hass: HomeAssistant, msg_id: int, diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 58cf0e5c663..00e9d0d35b4 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -1,14 +1,17 @@ """The tests for sensor recorder platform.""" # pylint: disable=protected-access,invalid-name +import datetime from datetime import timedelta +from statistics import fmean import threading -from unittest.mock import patch +from unittest.mock import ANY, patch from freezegun import freeze_time import pytest from pytest import approx from homeassistant.components import recorder +from homeassistant.components.recorder.db_schema import Statistics, StatisticsShortTerm from homeassistant.components.recorder.statistics import ( async_add_external_statistics, get_last_statistics, @@ -178,6 +181,448 @@ async def test_statistics_during_period(recorder_mock, hass, hass_ws_client): } +@freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.timezone.utc)) +async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): + """Test statistic_during_period.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + now = dt_util.utcnow() + + await async_recorder_block_till_done(hass) + client = await hass_ws_client() + + zero = now + start = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=-3) + + imported_stats_5min = [ + { + "start": (start + timedelta(minutes=5 * i)), + "max": i * 2, + "mean": i, + "min": -76 + i * 2, + "sum": i, + } + for i in range(0, 39) + ] + imported_stats = [ + { + "start": imported_stats_5min[i * 12]["start"], + "max": max( + stat["max"] for stat in imported_stats_5min[i * 12 : (i + 1) * 12] + ), + "mean": fmean( + stat["mean"] for stat in imported_stats_5min[i * 12 : (i + 1) * 12] + ), + "min": min( + stat["min"] for stat in imported_stats_5min[i * 12 : (i + 1) * 12] + ), + "sum": imported_stats_5min[i * 12 + 11]["sum"], + } + for i in range(0, 3) + ] + imported_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "recorder", + "statistic_id": "sensor.test", + "unit_of_measurement": "kWh", + } + + recorder.get_instance(hass).async_import_statistics( + imported_metadata, + imported_stats, + Statistics, + ) + recorder.get_instance(hass).async_import_statistics( + imported_metadata, + imported_stats_5min, + StatisticsShortTerm, + ) + await async_wait_recording_done(hass) + + # No data for this period yet + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "fixed_period": { + "start_time": now.isoformat(), + "end_time": now.isoformat(), + }, + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": None, + "mean": None, + "min": None, + "change": None, + } + + # This should include imported_statistics_5min[:] + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[:]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[:]), + "min": min(stat["min"] for stat in imported_stats_5min[:]), + "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[0]["sum"], + } + + # This should also include imported_statistics_5min[:] + start_time = "2022-10-21T04:00:00+00:00" + end_time = "2022-10-21T07:15:00+00:00" + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + "fixed_period": { + "start_time": start_time, + "end_time": end_time, + }, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[:]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[:]), + "min": min(stat["min"] for stat in imported_stats_5min[:]), + "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[0]["sum"], + } + + # This should also include imported_statistics_5min[:] + start_time = "2022-10-20T04:00:00+00:00" + end_time = "2022-10-21T08:20:00+00:00" + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + "fixed_period": { + "start_time": start_time, + "end_time": end_time, + }, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[:]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[:]), + "min": min(stat["min"] for stat in imported_stats_5min[:]), + "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[0]["sum"], + } + + # This should include imported_statistics_5min[26:] + start_time = "2022-10-21T06:10:00+00:00" + assert imported_stats_5min[26]["start"].isoformat() == start_time + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "fixed_period": { + "start_time": start_time, + }, + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[26:]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[26:]), + "min": min(stat["min"] for stat in imported_stats_5min[26:]), + "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[25]["sum"], + } + + # This should also include imported_statistics_5min[26:] + start_time = "2022-10-21T06:09:00+00:00" + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "fixed_period": { + "start_time": start_time, + }, + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[26:]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[26:]), + "min": min(stat["min"] for stat in imported_stats_5min[26:]), + "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[25]["sum"], + } + + # This should include imported_statistics_5min[:26] + end_time = "2022-10-21T06:10:00+00:00" + assert imported_stats_5min[26]["start"].isoformat() == end_time + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "fixed_period": { + "end_time": end_time, + }, + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[:26]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[:26]), + "min": min(stat["min"] for stat in imported_stats_5min[:26]), + "change": imported_stats_5min[25]["sum"] - 0, + } + + # This should include imported_statistics_5min[26:32] (less than a full hour) + start_time = "2022-10-21T06:10:00+00:00" + assert imported_stats_5min[26]["start"].isoformat() == start_time + end_time = "2022-10-21T06:40:00+00:00" + assert imported_stats_5min[32]["start"].isoformat() == end_time + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "fixed_period": { + "start_time": start_time, + "end_time": end_time, + }, + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[26:32]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[26:32]), + "min": min(stat["min"] for stat in imported_stats_5min[26:32]), + "change": imported_stats_5min[31]["sum"] - imported_stats_5min[25]["sum"], + } + + # This should include imported_statistics[2:] + imported_statistics_5min[36:] + start_time = "2022-10-21T06:00:00+00:00" + assert imported_stats_5min[24]["start"].isoformat() == start_time + assert imported_stats[2]["start"].isoformat() == start_time + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "fixed_period": { + "start_time": start_time, + }, + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[24:]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[24:]), + "min": min(stat["min"] for stat in imported_stats_5min[24:]), + "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[23]["sum"], + } + + # This should also include imported_statistics[2:] + imported_statistics_5min[36:] + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "rolling_window": { + "duration": {"hours": 1, "minutes": 25}, + }, + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[24:]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[24:]), + "min": min(stat["min"] for stat in imported_stats_5min[24:]), + "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[23]["sum"], + } + + # This should include imported_statistics[2:3] + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "rolling_window": { + "duration": {"hours": 1}, + "offset": {"minutes": -25}, + }, + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[24:36]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[24:36]), + "min": min(stat["min"] for stat in imported_stats_5min[24:36]), + "change": imported_stats_5min[35]["sum"] - imported_stats_5min[23]["sum"], + } + + # Test we can get only selected types + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + "types": ["max", "change"], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[:]), + "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[0]["sum"], + } + + # Test we can convert units + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + "units": {"energy": "MWh"}, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[:]) / 1000, + "mean": fmean(stat["mean"] for stat in imported_stats_5min[:]) / 1000, + "min": min(stat["min"] for stat in imported_stats_5min[:]) / 1000, + "change": (imported_stats_5min[-1]["sum"] - imported_stats_5min[0]["sum"]) + / 1000, + } + + # Test we can automatically convert units + hass.states.async_set("sensor.test", None, attributes=ENERGY_SENSOR_WH_ATTRIBUTES) + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[:]) * 1000, + "mean": fmean(stat["mean"] for stat in imported_stats_5min[:]) * 1000, + "min": min(stat["min"] for stat in imported_stats_5min[:]) * 1000, + "change": (imported_stats_5min[-1]["sum"] - imported_stats_5min[0]["sum"]) + * 1000, + } + + +@freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.timezone.utc)) +@pytest.mark.parametrize( + "calendar_period, start_time, end_time", + ( + ( + {"period": "hour"}, + "2022-10-21T07:00:00+00:00", + "2022-10-21T08:00:00+00:00", + ), + ( + {"period": "hour", "offset": -1}, + "2022-10-21T06:00:00+00:00", + "2022-10-21T07:00:00+00:00", + ), + ( + {"period": "day"}, + "2022-10-21T07:00:00+00:00", + "2022-10-22T07:00:00+00:00", + ), + ( + {"period": "day", "offset": -1}, + "2022-10-20T07:00:00+00:00", + "2022-10-21T07:00:00+00:00", + ), + ( + {"period": "week"}, + "2022-10-17T07:00:00+00:00", + "2022-10-24T07:00:00+00:00", + ), + ( + {"period": "week", "offset": -1}, + "2022-10-10T07:00:00+00:00", + "2022-10-17T07:00:00+00:00", + ), + ( + {"period": "month"}, + "2022-10-01T07:00:00+00:00", + "2022-11-01T07:00:00+00:00", + ), + ( + {"period": "month", "offset": -1}, + "2022-09-01T07:00:00+00:00", + "2022-10-01T07:00:00+00:00", + ), + ( + {"period": "year"}, + "2022-01-01T08:00:00+00:00", + "2023-01-01T08:00:00+00:00", + ), + ( + {"period": "year", "offset": -1}, + "2021-01-01T08:00:00+00:00", + "2022-01-01T08:00:00+00:00", + ), + ), +) +async def test_statistic_during_period_calendar( + recorder_mock, hass, hass_ws_client, calendar_period, start_time, end_time +): + """Test statistic_during_period.""" + client = await hass_ws_client() + + # Try requesting data for the current hour + with patch( + "homeassistant.components.recorder.websocket_api.statistic_during_period", + return_value={}, + ) as statistic_during_period: + await client.send_json( + { + "id": 1, + "type": "recorder/statistic_during_period", + "calendar": calendar_period, + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + statistic_during_period.assert_called_once_with( + hass, ANY, ANY, "sensor.test", None, units=None + ) + assert statistic_during_period.call_args[0][1].isoformat() == start_time + assert statistic_during_period.call_args[0][2].isoformat() == end_time + assert response["success"] + + @pytest.mark.parametrize( "attributes, state, value, custom_units, converted_value", [ @@ -1595,20 +2040,20 @@ async def test_import_statistics( period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) period2 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=2) - external_statistics1 = { + imported_statistics1 = { "start": period1.isoformat(), "last_reset": None, "state": 0, "sum": 2, } - external_statistics2 = { + imported_statistics2 = { "start": period2.isoformat(), "last_reset": None, "state": 1, "sum": 3, } - external_metadata = { + imported_metadata = { "has_mean": False, "has_sum": True, "name": "Total imported energy", @@ -1621,8 +2066,8 @@ async def test_import_statistics( { "id": 1, "type": "recorder/import_statistics", - "metadata": external_metadata, - "stats": [external_statistics1, external_statistics2], + "metadata": imported_metadata, + "stats": [imported_statistics1, imported_statistics2], } ) response = await client.receive_json() @@ -1712,7 +2157,7 @@ async def test_import_statistics( { "id": 2, "type": "recorder/import_statistics", - "metadata": external_metadata, + "metadata": imported_metadata, "stats": [external_statistics], } ) @@ -1764,7 +2209,7 @@ async def test_import_statistics( { "id": 3, "type": "recorder/import_statistics", - "metadata": external_metadata, + "metadata": imported_metadata, "stats": [external_statistics], } ) @@ -1822,20 +2267,20 @@ async def test_adjust_sum_statistics_energy( period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) period2 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=2) - external_statistics1 = { + imported_statistics1 = { "start": period1.isoformat(), "last_reset": None, "state": 0, "sum": 2, } - external_statistics2 = { + imported_statistics2 = { "start": period2.isoformat(), "last_reset": None, "state": 1, "sum": 3, } - external_metadata = { + imported_metadata = { "has_mean": False, "has_sum": True, "name": "Total imported energy", @@ -1848,8 +2293,8 @@ async def test_adjust_sum_statistics_energy( { "id": 1, "type": "recorder/import_statistics", - "metadata": external_metadata, - "stats": [external_statistics1, external_statistics2], + "metadata": imported_metadata, + "stats": [imported_statistics1, imported_statistics2], } ) response = await client.receive_json() @@ -2018,20 +2463,20 @@ async def test_adjust_sum_statistics_gas( period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) period2 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=2) - external_statistics1 = { + imported_statistics1 = { "start": period1.isoformat(), "last_reset": None, "state": 0, "sum": 2, } - external_statistics2 = { + imported_statistics2 = { "start": period2.isoformat(), "last_reset": None, "state": 1, "sum": 3, } - external_metadata = { + imported_metadata = { "has_mean": False, "has_sum": True, "name": "Total imported energy", @@ -2044,8 +2489,8 @@ async def test_adjust_sum_statistics_gas( { "id": 1, "type": "recorder/import_statistics", - "metadata": external_metadata, - "stats": [external_statistics1, external_statistics2], + "metadata": imported_metadata, + "stats": [imported_statistics1, imported_statistics2], } ) response = await client.receive_json() @@ -2229,20 +2674,20 @@ async def test_adjust_sum_statistics_errors( period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) period2 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=2) - external_statistics1 = { + imported_statistics1 = { "start": period1.isoformat(), "last_reset": None, "state": 0, "sum": 2, } - external_statistics2 = { + imported_statistics2 = { "start": period2.isoformat(), "last_reset": None, "state": 1, "sum": 3, } - external_metadata = { + imported_metadata = { "has_mean": False, "has_sum": True, "name": "Total imported energy", @@ -2255,8 +2700,8 @@ async def test_adjust_sum_statistics_errors( { "id": 1, "type": "recorder/import_statistics", - "metadata": external_metadata, - "stats": [external_statistics1, external_statistics2], + "metadata": imported_metadata, + "stats": [imported_statistics1, imported_statistics2], } ) response = await client.receive_json() From b5615823bababfc17fa6b92ef95ce173f502e602 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Oct 2022 17:05:09 -0500 Subject: [PATCH 880/985] Bump aiohomekit to 2.2.5 (#81048) --- 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 0bad4ed9f7b..24b2eebe615 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.4"], + "requirements": ["aiohomekit==2.2.5"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 4a6b3708aae..709b686cbf1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.4 +aiohomekit==2.2.5 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b39523ca42..7992fe48b6e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.4 +aiohomekit==2.2.5 # homeassistant.components.emulated_hue # homeassistant.components.http From ad29bd55a45e7d04bc5f6f02f912ed0ccd7252c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Oct 2022 18:03:13 -0500 Subject: [PATCH 881/985] Bump zeroconf to 0.39.3 (#81049) --- 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 f0e2005b20e..967dd761ac7 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.39.2"], + "requirements": ["zeroconf==0.39.3"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c7dd51d76df..0a4ccf0f58e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -42,7 +42,7 @@ typing-extensions>=4.4.0,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 yarl==1.8.1 -zeroconf==0.39.2 +zeroconf==0.39.3 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 709b686cbf1..a5415a0e50f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2604,7 +2604,7 @@ zamg==0.1.1 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.39.2 +zeroconf==0.39.3 # homeassistant.components.zha zha-quirks==0.0.84 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7992fe48b6e..522a69b3b1c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1805,7 +1805,7 @@ youless-api==0.16 zamg==0.1.1 # homeassistant.components.zeroconf -zeroconf==0.39.2 +zeroconf==0.39.3 # homeassistant.components.zha zha-quirks==0.0.84 From 200f0fa92c876e2d92ecb433842c2da2d7fcb902 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 26 Oct 2022 21:47:38 -0400 Subject: [PATCH 882/985] Bump zigpy to 0.51.4 (#81050) Bump zigpy from 0.51.3 to 0.51.4 --- 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 4698c78d384..1c86fe52c5e 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.84", "zigpy-deconz==0.19.0", - "zigpy==0.51.3", + "zigpy==0.51.4", "zigpy-xbee==0.16.2", "zigpy-zigate==0.10.2", "zigpy-znp==0.9.1" diff --git a/requirements_all.txt b/requirements_all.txt index a5415a0e50f..387e9e33360 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2628,7 +2628,7 @@ zigpy-zigate==0.10.2 zigpy-znp==0.9.1 # homeassistant.components.zha -zigpy==0.51.3 +zigpy==0.51.4 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 522a69b3b1c..fd785aa01c3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1823,7 +1823,7 @@ zigpy-zigate==0.10.2 zigpy-znp==0.9.1 # homeassistant.components.zha -zigpy==0.51.3 +zigpy==0.51.4 # homeassistant.components.zwave_js zwave-js-server-python==0.43.0 From bb47935509dc492d55f7b1c09ef4588ca930d363 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 26 Oct 2022 21:29:48 -0400 Subject: [PATCH 883/985] Handle sending ZCL commands with empty bitmap options (#81051) Handle sending commands with empty bitmaps --- homeassistant/components/zha/core/helpers.py | 30 +++++++------------- tests/components/zha/test_helpers.py | 14 +++++++++ 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 409d45789b5..1ea9a2a4c9b 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -14,7 +14,6 @@ import enum import functools import itertools import logging -import operator from random import uniform import re from typing import TYPE_CHECKING, Any, TypeVar @@ -163,25 +162,16 @@ def convert_to_zcl_values( if field.name not in fields: continue value = fields[field.name] - if issubclass(field.type, enum.Flag): - if isinstance(value, list): - value = field.type( - functools.reduce( - operator.ior, - [ - field.type[flag.replace(" ", "_")] - if isinstance(flag, str) - else field.type(flag) - for flag in value - ], - ) - ) - else: - value = ( - field.type[value.replace(" ", "_")] - if isinstance(value, str) - else field.type(value) - ) + if issubclass(field.type, enum.Flag) and isinstance(value, list): + new_value = 0 + + for flag in value: + if isinstance(flag, str): + new_value |= field.type[flag.replace(" ", "_")] + else: + new_value |= flag + + value = field.type(new_value) elif issubclass(field.type, enum.Enum): value = ( field.type[value.replace(" ", "_")] diff --git a/tests/components/zha/test_helpers.py b/tests/components/zha/test_helpers.py index f5fb5c4f5c0..64f8c732ca9 100644 --- a/tests/components/zha/test_helpers.py +++ b/tests/components/zha/test_helpers.py @@ -195,3 +195,17 @@ async def test_zcl_schema_conversions(hass, device_light): assert isinstance(converted_data["start_hue"], uint16_t) assert converted_data["start_hue"] == 196 + + # This time, the update flags bitmap is empty + raw_data = { + "update_flags": [], + "action": 0x02, + "direction": 0x01, + "time": 20, + "start_hue": 196, + } + + converted_data = convert_to_zcl_values(raw_data, command_schema) + + # No flags are passed through + assert converted_data["update_flags"] == 0 From c10dd1b7028a7a1dde9d1cf426c917f991ab32c5 Mon Sep 17 00:00:00 2001 From: mezz64 <2854333+mezz64@users.noreply.github.com> Date: Thu, 27 Oct 2022 01:37:48 -0400 Subject: [PATCH 884/985] Eight Sleep catch missing keys (#81058) Catch missing keys --- homeassistant/components/eight_sleep/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index b184cd2496f..b07865d8591 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -146,7 +146,7 @@ def _get_breakdown_percent( """Get a breakdown percent.""" try: return round((attr["breakdown"][key] / denominator) * 100, 2) - except ZeroDivisionError: + except (ZeroDivisionError, KeyError): return 0 From 61d064ffd567b944fc2629e8c3e4d5d14304c1a7 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Thu, 27 Oct 2022 15:01:15 +1100 Subject: [PATCH 885/985] Bump aiolifx-themes to 0.2.0 (#81059) --- homeassistant/components/lifx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index da07a2ffc8b..fc5422757b9 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "aiolifx==0.8.6", "aiolifx_effects==0.3.0", - "aiolifx_themes==0.1.1" + "aiolifx_themes==0.2.0" ], "quality_scale": "platinum", "dependencies": ["network"], diff --git a/requirements_all.txt b/requirements_all.txt index 387e9e33360..d08127cc558 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -196,7 +196,7 @@ aiolifx==0.8.6 aiolifx_effects==0.3.0 # homeassistant.components.lifx -aiolifx_themes==0.1.1 +aiolifx_themes==0.2.0 # homeassistant.components.lookin aiolookin==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd785aa01c3..9a139e5b727 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -174,7 +174,7 @@ aiolifx==0.8.6 aiolifx_effects==0.3.0 # homeassistant.components.lifx -aiolifx_themes==0.1.1 +aiolifx_themes==0.2.0 # homeassistant.components.lookin aiolookin==0.1.1 From eec1015789839e4713d5901be3f7b91cfdc1cca2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Oct 2022 00:38:03 -0500 Subject: [PATCH 886/985] Bump nexia to 2.0.5 (#81061) fixes #80988 --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 77280b1f503..78576e06b8a 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==2.0.4"], + "requirements": ["nexia==2.0.5"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index d08127cc558..bbaed415da6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1135,7 +1135,7 @@ nettigo-air-monitor==1.4.2 neurio==0.3.1 # homeassistant.components.nexia -nexia==2.0.4 +nexia==2.0.5 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a139e5b727..0c7b4d8a9a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -825,7 +825,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.4.2 # homeassistant.components.nexia -nexia==2.0.4 +nexia==2.0.5 # homeassistant.components.discord nextcord==2.0.0a8 From a50fd6a259742caab5ab739ef9458cd0f351c5ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huryn?= Date: Thu, 27 Oct 2022 14:12:51 +0200 Subject: [PATCH 887/985] Update blebox_uniapi to 2.1.3 (#81071) fix: #80124 blebox_uniapi dependency version bump --- homeassistant/components/blebox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 328f15abdac..78c7186eb31 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -3,7 +3,7 @@ "name": "BleBox devices", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blebox", - "requirements": ["blebox_uniapi==2.1.0"], + "requirements": ["blebox_uniapi==2.1.3"], "codeowners": ["@bbx-a", "@riokuu"], "iot_class": "local_polling", "loggers": ["blebox_uniapi"] diff --git a/requirements_all.txt b/requirements_all.txt index bbaed415da6..047e767f606 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -419,7 +419,7 @@ bleak-retry-connector==2.4.2 bleak==0.19.0 # homeassistant.components.blebox -blebox_uniapi==2.1.0 +blebox_uniapi==2.1.3 # homeassistant.components.blink blinkpy==0.19.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c7b4d8a9a6..e3c4fccb6e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -343,7 +343,7 @@ bleak-retry-connector==2.4.2 bleak==0.19.0 # homeassistant.components.blebox -blebox_uniapi==2.1.0 +blebox_uniapi==2.1.3 # homeassistant.components.blink blinkpy==0.19.2 From cbd5e919cbddbb6afd5ac4582ed15fd6fcff1958 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 27 Oct 2022 20:42:16 +0200 Subject: [PATCH 888/985] Clean up superfluous Netatmo API calls (#81095) --- homeassistant/components/netatmo/data_handler.py | 5 ++++- homeassistant/components/netatmo/netatmo_entity_base.py | 8 +++++--- tests/components/netatmo/test_camera.py | 2 +- tests/components/netatmo/test_init.py | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index a376e6ee187..15d776c4529 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -252,7 +252,7 @@ class NetatmoDataHandler: self, signal_name: str, update_callback: CALLBACK_TYPE | None ) -> None: """Unsubscribe from publisher.""" - if update_callback in self.publisher[signal_name].subscriptions: + if update_callback not in self.publisher[signal_name].subscriptions: return self.publisher[signal_name].subscriptions.remove(update_callback) @@ -288,6 +288,9 @@ class NetatmoDataHandler: person.entity_id: person.pseudo for person in home.persons.values() } + await self.unsubscribe(WEATHER, None) + await self.unsubscribe(AIR_CARE, None) + def setup_air_care(self) -> None: """Set up home coach/air care modules.""" for module in self.account.modules.values(): diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index d0359d739fd..c434d370e27 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -63,9 +63,11 @@ class NetatmoBase(Entity): publisher["name"], signal_name, self.async_update_callback ) - for sub in self.data_handler.publisher[signal_name].subscriptions: - if sub is None: - await self.data_handler.unsubscribe(signal_name, None) + if any( + sub is None + for sub in self.data_handler.publisher[signal_name].subscriptions + ): + await self.data_handler.unsubscribe(signal_name, None) registry = dr.async_get(self.hass) if device := registry.async_get_device({(DOMAIN, self._id)}): diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index 027b0907d50..beb91c7565e 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -472,7 +472,7 @@ async def test_setup_component_no_devices(hass, config_entry): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert fake_post_hits == 9 + assert fake_post_hits == 11 async def test_camera_image_raises_exception(hass, config_entry, requests_mock): diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index 187a89afeb6..65cc991ec67 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -110,7 +110,7 @@ async def test_setup_component_with_config(hass, config_entry): await hass.async_block_till_done() - assert fake_post_hits == 8 + assert fake_post_hits == 10 mock_impl.assert_called_once() mock_webhook.assert_called_once() From 43164b5751e6eac914fab81431552263e96e49b9 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 27 Oct 2022 17:37:52 +0200 Subject: [PATCH 889/985] Bring back Netatmo force update code (#81098) --- homeassistant/components/netatmo/data_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 15d776c4529..1a322f8d8db 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -176,8 +176,8 @@ class NetatmoDataHandler: @callback def async_force_update(self, signal_name: str) -> None: """Prioritize data retrieval for given data class entry.""" - # self.publisher[signal_name].next_scan = time() - # self._queue.rotate(-(self._queue.index(self.publisher[signal_name]))) + self.publisher[signal_name].next_scan = time() + self._queue.rotate(-(self._queue.index(self.publisher[signal_name]))) async def handle_event(self, event: dict) -> None: """Handle webhook events.""" From 8751eaaf3ee94011e5c961a96063024f58a39e8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Oct 2022 14:38:53 -0500 Subject: [PATCH 890/985] Bump dbus-fast to 1.51.0 (#81109) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index a3348a1611b..60b260baf36 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.4.2", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.49.0" + "dbus-fast==1.51.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0a4ccf0f58e..d4a2dbc438b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.49.0 +dbus-fast==1.51.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 047e767f606..be8effeff81 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.49.0 +dbus-fast==1.51.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3c4fccb6e1..b6d067d2527 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.49.0 +dbus-fast==1.51.0 # homeassistant.components.debugpy debugpy==1.6.3 From 4927f4206aa534479f95a8a403f14c2286cc3e97 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Oct 2022 14:38:42 -0500 Subject: [PATCH 891/985] Add support for oralb IO Series 4 (#81110) --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/oralb/__init__.py | 11 ++++++++ tests/components/oralb/test_config_flow.py | 21 ++++++++++++++- tests/components/oralb/test_sensor.py | 27 +++++++++++++++++++- 6 files changed, 60 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index b3dfedde532..bf6879733f5 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.5.0"], + "requirements": ["oralb-ble==0.6.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index be8effeff81..f8004c149e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1238,7 +1238,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.5.0 +oralb-ble==0.6.0 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6d067d2527..fd84b1e511d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -883,7 +883,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.5.0 +oralb-ble==0.6.0 # homeassistant.components.ovo_energy ovoenergy==1.2.0 diff --git a/tests/components/oralb/__init__.py b/tests/components/oralb/__init__.py index 567b9d7328e..5525a859f21 100644 --- a/tests/components/oralb/__init__.py +++ b/tests/components/oralb/__init__.py @@ -22,3 +22,14 @@ ORALB_SERVICE_INFO = BluetoothServiceInfo( service_data={}, source="local", ) + + +ORALB_IO_SERIES_4_SERVICE_INFO = BluetoothServiceInfo( + name="GXB772CD\x00\x00\x00\x00\x00\x00\x00\x00\x00", + address="78:DB:2F:C2:48:BE", + rssi=-63, + manufacturer_data={220: b"\x074\x0c\x038\x00\x00\x02\x01\x00\x04"}, + service_uuids=[], + service_data={}, + source="local", +) diff --git a/tests/components/oralb/test_config_flow.py b/tests/components/oralb/test_config_flow.py index e4af11faddb..cb7f97a5089 100644 --- a/tests/components/oralb/test_config_flow.py +++ b/tests/components/oralb/test_config_flow.py @@ -6,7 +6,7 @@ from homeassistant import config_entries from homeassistant.components.oralb.const import DOMAIN from homeassistant.data_entry_flow import FlowResultType -from . import NOT_ORALB_SERVICE_INFO, ORALB_SERVICE_INFO +from . import NOT_ORALB_SERVICE_INFO, ORALB_IO_SERIES_4_SERVICE_INFO, ORALB_SERVICE_INFO from tests.common import MockConfigEntry @@ -30,6 +30,25 @@ async def test_async_step_bluetooth_valid_device(hass): assert result2["result"].unique_id == "78:DB:2F:C2:48:BE" +async def test_async_step_bluetooth_valid_io_series4_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=ORALB_IO_SERIES_4_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch("homeassistant.components.oralb.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "IO Series 4 48BE" + assert result2["data"] == {} + assert result2["result"].unique_id == "78:DB:2F:C2:48:BE" + + async def test_async_step_bluetooth_not_oralb(hass): """Test discovery via bluetooth not oralb.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/oralb/test_sensor.py b/tests/components/oralb/test_sensor.py index 4e37005f65a..2122ad9bbff 100644 --- a/tests/components/oralb/test_sensor.py +++ b/tests/components/oralb/test_sensor.py @@ -4,7 +4,7 @@ from homeassistant.components.oralb.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME -from . import ORALB_SERVICE_INFO +from . import ORALB_IO_SERIES_4_SERVICE_INFO, ORALB_SERVICE_INFO from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info @@ -38,3 +38,28 @@ async def test_sensors(hass, entity_registry_enabled_by_default): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_sensors_io_series_4(hass, entity_registry_enabled_by_default): + """Test setting up creates the sensors with an io series 4.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=ORALB_IO_SERIES_4_SERVICE_INFO.address, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 0 + inject_bluetooth_service_info(hass, ORALB_IO_SERIES_4_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 8 + + toothbrush_sensor = hass.states.get("sensor.io_series_4_48be_mode") + toothbrush_sensor_attrs = toothbrush_sensor.attributes + assert toothbrush_sensor.state == "gum care" + assert toothbrush_sensor_attrs[ATTR_FRIENDLY_NAME] == "IO Series 4 48BE Mode" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 233ad2b90b38e6b169dcbd1002e5ef1eeb5b3e36 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 27 Oct 2022 22:00:34 +0200 Subject: [PATCH 892/985] Migrate KNX to use kelvin for color temperature (#81112) --- homeassistant/components/knx/light.py | 61 +++++++++++++-------------- tests/components/knx/test_light.py | 36 ++++++++++++---- 2 files changed, 58 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 9268b53581b..e4260f5e868 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -9,7 +9,7 @@ from xknx.devices.light import Light as XknxLight, XYYColor from homeassistant import config_entries from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, + ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, ATTR_RGB_COLOR, ATTR_RGBW_COLOR, @@ -153,15 +153,8 @@ class KNXLight(KnxEntity, LightEntity): def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of KNX light.""" super().__init__(_create_light(xknx, config)) - self._max_kelvin: int = config[LightSchema.CONF_MAX_KELVIN] - self._min_kelvin: int = config[LightSchema.CONF_MIN_KELVIN] - - self._attr_max_mireds = color_util.color_temperature_kelvin_to_mired( - self._min_kelvin - ) - self._attr_min_mireds = color_util.color_temperature_kelvin_to_mired( - self._max_kelvin - ) + self._attr_max_color_temp_kelvin: int = config[LightSchema.CONF_MAX_KELVIN] + self._attr_min_color_temp_kelvin: int = config[LightSchema.CONF_MIN_KELVIN] self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = self._device_unique_id() @@ -242,21 +235,23 @@ class KNXLight(KnxEntity, LightEntity): return None @property - def color_temp(self) -> int | None: - """Return the color temperature in mireds.""" + def color_temp_kelvin(self) -> int | None: + """Return the color temperature in Kelvin.""" if self._device.supports_color_temperature: - kelvin = self._device.current_color_temperature - # Avoid division by zero if actuator reported 0 Kelvin (e.g., uninitialized DALI-Gateway) - if kelvin is not None and kelvin > 0: - return color_util.color_temperature_kelvin_to_mired(kelvin) + if kelvin := self._device.current_color_temperature: + return kelvin 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 - return color_util.color_temperature_kelvin_to_mired( - self._min_kelvin - + ((relative_ct / 255) * (self._max_kelvin - self._min_kelvin)) + return int( + self._attr_min_color_temp_kelvin + + ( + (relative_ct / 255) + * ( + self._attr_max_color_temp_kelvin + - self._attr_min_color_temp_kelvin + ) + ) ) return None @@ -288,7 +283,7 @@ class KNXLight(KnxEntity, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) - mireds = kwargs.get(ATTR_COLOR_TEMP) + color_temp = kwargs.get(ATTR_COLOR_TEMP_KELVIN) rgb = kwargs.get(ATTR_RGB_COLOR) rgbw = kwargs.get(ATTR_RGBW_COLOR) hs_color = kwargs.get(ATTR_HS_COLOR) @@ -297,7 +292,7 @@ class KNXLight(KnxEntity, LightEntity): if ( not self.is_on and brightness is None - and mireds is None + and color_temp is None and rgb is None and rgbw is None and hs_color is None @@ -335,17 +330,21 @@ class KNXLight(KnxEntity, LightEntity): await set_color(rgb, None, brightness) return - if mireds is not None: - kelvin = int(color_util.color_temperature_mired_to_kelvin(mireds)) - kelvin = min(self._max_kelvin, max(self._min_kelvin, kelvin)) - + if color_temp is not None: + color_temp = min( + self._attr_max_color_temp_kelvin, + max(self._attr_min_color_temp_kelvin, color_temp), + ) if self._device.supports_color_temperature: - await self._device.set_color_temperature(kelvin) + await self._device.set_color_temperature(color_temp) elif self._device.supports_tunable_white: - relative_ct = int( + relative_ct = round( 255 - * (kelvin - self._min_kelvin) - / (self._max_kelvin - self._min_kelvin) + * (color_temp - self._attr_min_color_temp_kelvin) + / ( + self._attr_max_color_temp_kelvin + - self._attr_min_color_temp_kelvin + ) ) await self._device.set_tunable_white(relative_ct) diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py index 56cf5b2c00a..2f7484fad8b 100644 --- a/tests/components/knx/test_light.py +++ b/tests/components/knx/test_light.py @@ -11,7 +11,7 @@ from homeassistant.components.knx.schema import LightSchema from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_NAME, - ATTR_COLOR_TEMP, + ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, ATTR_RGBW_COLOR, ColorMode, @@ -166,19 +166,25 @@ async def test_light_color_temp_absolute(hass: HomeAssistant, knx: KNXTestKit): brightness=255, color_mode=ColorMode.COLOR_TEMP, color_temp=370, + color_temp_kelvin=2700, ) # change color temperature from HA await hass.services.async_call( "light", "turn_on", - {"entity_id": "light.test", ATTR_COLOR_TEMP: 250}, # 4000 Kelvin - 0x0FA0 + {"entity_id": "light.test", ATTR_COLOR_TEMP_KELVIN: 4000}, # 4000 - 0x0FA0 blocking=True, ) await knx.assert_write(test_ct, (0x0F, 0xA0)) knx.assert_state("light.test", STATE_ON, color_temp=250) # change color temperature from KNX await knx.receive_write(test_ct_state, (0x17, 0x70)) # 6000 Kelvin - 166 Mired - knx.assert_state("light.test", STATE_ON, color_temp=166) + knx.assert_state( + "light.test", + STATE_ON, + color_temp=166, + color_temp_kelvin=6000, + ) async def test_light_color_temp_relative(hass: HomeAssistant, knx: KNXTestKit): @@ -222,19 +228,33 @@ async def test_light_color_temp_relative(hass: HomeAssistant, knx: KNXTestKit): brightness=255, color_mode=ColorMode.COLOR_TEMP, color_temp=250, + color_temp_kelvin=4000, ) # change color temperature from HA await hass.services.async_call( "light", "turn_on", - {"entity_id": "light.test", ATTR_COLOR_TEMP: 300}, # 3333 Kelvin - 33 % - 0x54 + { + "entity_id": "light.test", + ATTR_COLOR_TEMP_KELVIN: 3333, # 3333 Kelvin - 33.3 % - 0x55 + }, blocking=True, ) - await knx.assert_write(test_ct, (0x54,)) - knx.assert_state("light.test", STATE_ON, color_temp=300) + await knx.assert_write(test_ct, (0x55,)) + knx.assert_state( + "light.test", + STATE_ON, + color_temp=300, + color_temp_kelvin=3333, + ) # change color temperature from KNX - await knx.receive_write(test_ct_state, (0xE6,)) # 3900 Kelvin - 90 % - 256 Mired - knx.assert_state("light.test", STATE_ON, color_temp=256) + await knx.receive_write(test_ct_state, (0xE6,)) # 3901 Kelvin - 90.1 % - 256 Mired + knx.assert_state( + "light.test", + STATE_ON, + color_temp=256, + color_temp_kelvin=3901, + ) async def test_light_hs_color(hass: HomeAssistant, knx: KNXTestKit): From 6d973a1793f2edf718e18bd18cb018b95630829b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 27 Oct 2022 23:13:43 +0200 Subject: [PATCH 893/985] Update frontend to 20221027.0 (#81114) --- 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 390b0ccfc18..c8d3645435f 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==20221026.0"], + "requirements": ["home-assistant-frontend==20221027.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d4a2dbc438b..79e6340ed41 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.51.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 -home-assistant-frontend==20221026.0 +home-assistant-frontend==20221027.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index f8004c149e9..78ee7727e28 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,7 +868,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221026.0 +home-assistant-frontend==20221027.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd84b1e511d..f177eae1636 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,7 +648,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221026.0 +home-assistant-frontend==20221027.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 1c8a7fe8e8d1e57f47c9b50ab482212424fd8808 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 27 Oct 2022 17:30:32 -0400 Subject: [PATCH 894/985] Bumped version to 2022.11.0b1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c194782ed29..d9964b32b9b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index a869da99baa..cf6598589ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.0b0" +version = "2022.11.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From aeecc93ad653cb7504bed01ff1074cb14b3b4721 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Sat, 29 Oct 2022 03:01:53 +0200 Subject: [PATCH 895/985] Allow empty string for filters for waze_travel_time (#80953) * Allow empty string for filters Signed-off-by: Kevin Stillhammer * Apply PR feedback Signed-off-by: Kevin Stillhammer Signed-off-by: Kevin Stillhammer --- .../waze_travel_time/config_flow.py | 2 +- .../components/waze_travel_time/sensor.py | 4 +- .../waze_travel_time/test_config_flow.py | 54 +++++++++++++++++-- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py index fd6747cc1c8..b26732e4cb1 100644 --- a/homeassistant/components/waze_travel_time/config_flow.py +++ b/homeassistant/components/waze_travel_time/config_flow.py @@ -52,7 +52,7 @@ class WazeOptionsFlow(config_entries.OptionsFlow): if user_input is not None: return self.async_create_entry( title="", - data={k: v for k, v in user_input.items() if v not in (None, "")}, + data=user_input, ) return self.async_show_form( diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 942c1bccb36..c8d3e308435 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -185,14 +185,14 @@ class WazeTravelTimeData: ) routes = params.calc_all_routes_info(real_time=realtime) - if incl_filter is not None: + if incl_filter not in {None, ""}: routes = { k: v for k, v in routes.items() if incl_filter.lower() in k.lower() } - if excl_filter is not None: + if excl_filter not in {None, ""}: routes = { k: v for k, v in routes.items() diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py index 51bf1ae8319..d58f8d9a34d 100644 --- a/tests/components/waze_travel_time/test_config_flow.py +++ b/tests/components/waze_travel_time/test_config_flow.py @@ -19,6 +19,7 @@ from homeassistant.components.waze_travel_time.const import ( IMPERIAL_UNITS, ) from homeassistant.const import CONF_NAME, CONF_REGION +from homeassistant.core import HomeAssistant from .const import MOCK_CONFIG @@ -26,7 +27,7 @@ from tests.common import MockConfigEntry @pytest.mark.usefixtures("validate_config_entry") -async def test_minimum_fields(hass): +async def test_minimum_fields(hass: HomeAssistant) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -50,7 +51,7 @@ async def test_minimum_fields(hass): } -async def test_options(hass): +async def test_options(hass: HomeAssistant) -> None: """Test options flow.""" entry = MockConfigEntry( domain=DOMAIN, @@ -105,7 +106,7 @@ async def test_options(hass): @pytest.mark.usefixtures("validate_config_entry") -async def test_dupe(hass): +async def test_dupe(hass: HomeAssistant) -> None: """Test setting up the same entry data twice is OK.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -138,7 +139,9 @@ async def test_dupe(hass): @pytest.mark.usefixtures("invalidate_config_entry") -async def test_invalid_config_entry(hass, caplog): +async def test_invalid_config_entry( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -154,3 +157,46 @@ async def test_invalid_config_entry(hass, caplog): assert result2["errors"] == {"base": "cannot_connect"} assert "Error trying to validate entry" in caplog.text + + +@pytest.mark.usefixtures("mock_update") +async def test_reset_filters(hass: HomeAssistant) -> None: + """Test resetting inclusive and exclusive filters to empty string.""" + options = {**DEFAULT_OPTIONS} + options[CONF_INCL_FILTER] = "test" + options[CONF_EXCL_FILTER] = "test" + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=options, entry_id="test" + ) + config_entry.add_to_hass(hass) + 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, data=None + ) + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "", + CONF_INCL_FILTER: "", + CONF_REALTIME: False, + CONF_UNITS: IMPERIAL_UNITS, + CONF_VEHICLE_TYPE: "taxi", + }, + ) + + assert config_entry.options == { + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "", + CONF_INCL_FILTER: "", + CONF_REALTIME: False, + CONF_UNITS: IMPERIAL_UNITS, + CONF_VEHICLE_TYPE: "taxi", + } From 1ef9e9e19aa3a8577f6467c3a85c04a70e4b0cc5 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 28 Oct 2022 19:48:27 +0300 Subject: [PATCH 896/985] Fix Shelly Plus H&T sleep period on external power state change (#81121) --- .../components/shelly/coordinator.py | 30 ++++++++++++++++++- homeassistant/components/shelly/utils.py | 5 ++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 014355116c1..23f905b0fd9 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -41,7 +41,12 @@ from .const import ( SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, ) -from .utils import device_update_info, get_block_device_name, get_rpc_device_name +from .utils import ( + device_update_info, + get_block_device_name, + get_rpc_device_name, + get_rpc_device_wakeup_period, +) @dataclass @@ -355,6 +360,24 @@ class ShellyRpcCoordinator(DataUpdateCoordinator): LOGGER.debug("Reloading entry %s", self.name) await self.hass.config_entries.async_reload(self.entry.entry_id) + def update_sleep_period(self) -> bool: + """Check device sleep period & update if changed.""" + if ( + not self.device.initialized + or not (wakeup_period := get_rpc_device_wakeup_period(self.device.status)) + or wakeup_period == self.entry.data.get(CONF_SLEEP_PERIOD) + ): + return False + + data = {**self.entry.data} + data[CONF_SLEEP_PERIOD] = wakeup_period + self.hass.config_entries.async_update_entry(self.entry, data=data) + + update_interval = SLEEP_PERIOD_MULTIPLIER * wakeup_period + self.update_interval = timedelta(seconds=update_interval) + + return True + @callback def _async_device_updates_handler(self) -> None: """Handle device updates.""" @@ -365,6 +388,8 @@ class ShellyRpcCoordinator(DataUpdateCoordinator): ): return + self.update_sleep_period() + self._last_event = self.device.event for event in self.device.event["events"]: @@ -393,6 +418,9 @@ class ShellyRpcCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> None: """Fetch data.""" + if self.update_sleep_period(): + return + if sleep_period := self.entry.data.get(CONF_SLEEP_PERIOD): # Sleeping device, no point polling it, just mark it unavailable raise UpdateFailed( diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 79f5a5848f0..c3b6d24752f 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -272,6 +272,11 @@ def get_rpc_device_sleep_period(config: dict[str, Any]) -> int: return cast(int, config["sys"].get("sleep", {}).get("wakeup_period", 0)) +def get_rpc_device_wakeup_period(status: dict[str, Any]) -> int: + """Return the device wakeup period in seconds or 0 for non sleeping devices.""" + return cast(int, status["sys"].get("wakeup_period", 0)) + + def get_info_auth(info: dict[str, Any]) -> bool: """Return true if device has authorization enabled.""" return cast(bool, info.get("auth") or info.get("auth_en")) From 3f55d037f813775155d1237b00e414596ce2085f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Oct 2022 05:31:50 -0500 Subject: [PATCH 897/985] Bump oralb-ble to 0.8.0 (#81123) --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index bf6879733f5..e25f407add1 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.6.0"], + "requirements": ["oralb-ble==0.8.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 78ee7727e28..b69582d458e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1238,7 +1238,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.6.0 +oralb-ble==0.8.0 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f177eae1636..3b2557660f0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -883,7 +883,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.6.0 +oralb-ble==0.8.0 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From 9de89c97a4be8c918f6c3f7d8ae155e7dfd9e2b1 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Fri, 28 Oct 2022 13:02:33 +0200 Subject: [PATCH 898/985] Bump pyoverkiz to 1.5.6 (#81129) --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index f09142c86f0..d19495d82a2 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -3,7 +3,7 @@ "name": "Overkiz", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", - "requirements": ["pyoverkiz==1.5.5"], + "requirements": ["pyoverkiz==1.5.6"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index b69582d458e..4a81c2ef089 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1789,7 +1789,7 @@ pyotgw==2.1.1 pyotp==2.7.0 # homeassistant.components.overkiz -pyoverkiz==1.5.5 +pyoverkiz==1.5.6 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b2557660f0..a6c67d0dd2e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1266,7 +1266,7 @@ pyotgw==2.1.1 pyotp==2.7.0 # homeassistant.components.overkiz -pyoverkiz==1.5.5 +pyoverkiz==1.5.6 # homeassistant.components.openweathermap pyowm==3.2.0 From 2bfd4e79d23e5d1b26f555b61e3de9216d85a382 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Oct 2022 12:05:48 -0500 Subject: [PATCH 899/985] Bump aiohomekit to 2.2.6 (#81144) --- 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 24b2eebe615..34d47d6d835 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.5"], + "requirements": ["aiohomekit==2.2.6"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 4a81c2ef089..40fdb8475ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.5 +aiohomekit==2.2.6 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6c67d0dd2e..c964bcb29b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.5 +aiohomekit==2.2.6 # homeassistant.components.emulated_hue # homeassistant.components.http From 4dc2d885cfb38ba40504ba38f6eb0879c0bf32fe Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 29 Oct 2022 05:05:21 +0300 Subject: [PATCH 900/985] Add diagnostics to Switcher (#81146) --- .../components/switcher_kis/diagnostics.py | 28 +++++++++ .../switcher_kis/test_diagnostics.py | 59 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 homeassistant/components/switcher_kis/diagnostics.py create mode 100644 tests/components/switcher_kis/test_diagnostics.py diff --git a/homeassistant/components/switcher_kis/diagnostics.py b/homeassistant/components/switcher_kis/diagnostics.py new file mode 100644 index 00000000000..93b3c36bd21 --- /dev/null +++ b/homeassistant/components/switcher_kis/diagnostics.py @@ -0,0 +1,28 @@ +"""Diagnostics support for Switcher.""" +from __future__ import annotations + +from dataclasses import asdict +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DATA_DEVICE, DOMAIN + +TO_REDACT = {"device_id", "ip_address", "mac_address"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + devices = hass.data[DOMAIN][DATA_DEVICE] + + return async_redact_data( + { + "entry": entry.as_dict(), + "devices": [asdict(devices[d].data) for d in devices], + }, + TO_REDACT, + ) diff --git a/tests/components/switcher_kis/test_diagnostics.py b/tests/components/switcher_kis/test_diagnostics.py new file mode 100644 index 00000000000..8655ba7ee1f --- /dev/null +++ b/tests/components/switcher_kis/test_diagnostics.py @@ -0,0 +1,59 @@ +"""Tests for the diagnostics data provided by Switcher.""" +from aiohttp import ClientSession + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.core import HomeAssistant + +from . import init_integration +from .consts import DUMMY_WATER_HEATER_DEVICE + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, hass_client: ClientSession, mock_bridge, monkeypatch +) -> None: + """Test diagnostics.""" + entry = await init_integration(hass) + device = DUMMY_WATER_HEATER_DEVICE + monkeypatch.setattr(device, "last_data_update", "2022-09-28T16:42:12.706017") + mock_bridge.mock_callbacks([device]) + await hass.async_block_till_done() + + assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == { + "devices": [ + { + "auto_shutdown": "02:00:00", + "device_id": REDACTED, + "device_state": { + "__type": "", + "repr": "", + }, + "device_type": { + "__type": "", + "repr": ")>", + }, + "electric_current": 12.8, + "ip_address": REDACTED, + "last_data_update": "2022-09-28T16:42:12.706017", + "mac_address": REDACTED, + "name": "Heater FE12", + "power_consumption": 2780, + "remaining_time": "01:29:32", + } + ], + "entry": { + "entry_id": entry.entry_id, + "version": 1, + "domain": "switcher_kis", + "title": "Mock Title", + "data": {}, + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": "switcher_kis", + "disabled_by": None, + }, + } From 089bbe839157d34e324fe035ecee1eb0ddc684f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Oct 2022 20:01:03 -0500 Subject: [PATCH 901/985] Bump dbus-fast to 1.54.0 (#81148) * Bump dbus-fast to 1.53.0 changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.51.0...v1.53.0 * 54 --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 60b260baf36..a706d777bc6 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.4.2", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.51.0" + "dbus-fast==1.54.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 79e6340ed41..d8aef241616 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.51.0 +dbus-fast==1.54.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 40fdb8475ce..63da7ae10d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.51.0 +dbus-fast==1.54.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c964bcb29b7..597bafd4b28 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.51.0 +dbus-fast==1.54.0 # homeassistant.components.debugpy debugpy==1.6.3 From 09fc492d80c204259c2764a50d95beb148b0d843 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 28 Oct 2022 18:25:44 -0400 Subject: [PATCH 902/985] Bump aiopyarr to 22.10.0 (#81153) --- homeassistant/components/lidarr/manifest.json | 2 +- homeassistant/components/radarr/manifest.json | 2 +- homeassistant/components/sonarr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/radarr/fixtures/movie.json | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/lidarr/manifest.json b/homeassistant/components/lidarr/manifest.json index 7d4e9bcede7..4c07e0e1762 100644 --- a/homeassistant/components/lidarr/manifest.json +++ b/homeassistant/components/lidarr/manifest.json @@ -2,7 +2,7 @@ "domain": "lidarr", "name": "Lidarr", "documentation": "https://www.home-assistant.io/integrations/lidarr", - "requirements": ["aiopyarr==22.9.0"], + "requirements": ["aiopyarr==22.10.0"], "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/radarr/manifest.json b/homeassistant/components/radarr/manifest.json index 5bc15b24069..9b140def96a 100644 --- a/homeassistant/components/radarr/manifest.json +++ b/homeassistant/components/radarr/manifest.json @@ -2,7 +2,7 @@ "domain": "radarr", "name": "Radarr", "documentation": "https://www.home-assistant.io/integrations/radarr", - "requirements": ["aiopyarr==22.9.0"], + "requirements": ["aiopyarr==22.10.0"], "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/sonarr/manifest.json b/homeassistant/components/sonarr/manifest.json index 0c5b68a7949..daf9e20586b 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": ["aiopyarr==22.9.0"], + "requirements": ["aiopyarr==22.10.0"], "config_flow": true, "quality_scale": "silver", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 63da7ae10d0..bafcf1dbfba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -237,7 +237,7 @@ aiopvpc==3.0.0 # homeassistant.components.lidarr # homeassistant.components.radarr # homeassistant.components.sonarr -aiopyarr==22.9.0 +aiopyarr==22.10.0 # homeassistant.components.qnap_qsw aioqsw==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 597bafd4b28..00bafb48aba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aiopvpc==3.0.0 # homeassistant.components.lidarr # homeassistant.components.radarr # homeassistant.components.sonarr -aiopyarr==22.9.0 +aiopyarr==22.10.0 # homeassistant.components.qnap_qsw aioqsw==0.2.2 diff --git a/tests/components/radarr/fixtures/movie.json b/tests/components/radarr/fixtures/movie.json index 0f974859631..b33ff6fc481 100644 --- a/tests/components/radarr/fixtures/movie.json +++ b/tests/components/radarr/fixtures/movie.json @@ -21,8 +21,8 @@ "sortTitle": "string", "sizeOnDisk": 0, "overview": "string", - "inCinemas": "string", - "physicalRelease": "string", + "inCinemas": "2020-11-06T00:00:00Z", + "physicalRelease": "2019-03-19T00:00:00Z", "images": [ { "coverType": "poster", @@ -50,7 +50,7 @@ "certification": "string", "genres": ["string"], "tags": [0], - "added": "string", + "added": "2018-12-28T05:56:49Z", "ratings": { "votes": 0, "value": 0 From 230993b7c0ae01947f50adc147a46e757ec66ec5 Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Fri, 28 Oct 2022 23:32:57 +0100 Subject: [PATCH 903/985] Growatt version bump - fixes #80950 (#81161) --- homeassistant/components/growatt_server/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index 4127b48ae64..f3f17804fc1 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -3,7 +3,7 @@ "name": "Growatt", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/growatt_server/", - "requirements": ["growattServer==1.2.2"], + "requirements": ["growattServer==1.2.3"], "codeowners": ["@indykoning", "@muppet3000", "@JasperPlant"], "iot_class": "cloud_polling", "loggers": ["growattServer"] diff --git a/requirements_all.txt b/requirements_all.txt index bafcf1dbfba..e01d30e9b13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -804,7 +804,7 @@ greenwavereality==0.5.1 gridnet==4.0.0 # homeassistant.components.growatt_server -growattServer==1.2.2 +growattServer==1.2.3 # homeassistant.components.google_sheets gspread==5.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00bafb48aba..bd3bd8f97b8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -599,7 +599,7 @@ greeneye_monitor==3.0.3 gridnet==4.0.0 # homeassistant.components.growatt_server -growattServer==1.2.2 +growattServer==1.2.3 # homeassistant.components.google_sheets gspread==5.5.0 From f5fe3ec50e74ee4edd4e6163c0228c0bb324e667 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Oct 2022 17:31:30 -0500 Subject: [PATCH 904/985] Bump aiohomekit to 2.2.7 (#81163) changelog: https://github.com/Jc2k/aiohomekit/compare/2.2.6...2.2.7 --- 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 34d47d6d835..5aaae67d1d3 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.6"], + "requirements": ["aiohomekit==2.2.7"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index e01d30e9b13..9f0ce1f59f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.6 +aiohomekit==2.2.7 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd3bd8f97b8..b31882c1b37 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.6 +aiohomekit==2.2.7 # homeassistant.components.emulated_hue # homeassistant.components.http From d52323784e4b226709355907f92ec4ef8832808b Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 28 Oct 2022 18:31:53 -0400 Subject: [PATCH 905/985] Bump zigpy to 0.51.5 (#81164) Bump zigpy from 0.51.4 to 0.51.5 --- 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 1c86fe52c5e..79980d763e7 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.84", "zigpy-deconz==0.19.0", - "zigpy==0.51.4", + "zigpy==0.51.5", "zigpy-xbee==0.16.2", "zigpy-zigate==0.10.2", "zigpy-znp==0.9.1" diff --git a/requirements_all.txt b/requirements_all.txt index 9f0ce1f59f2..487b407a181 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2628,7 +2628,7 @@ zigpy-zigate==0.10.2 zigpy-znp==0.9.1 # homeassistant.components.zha -zigpy==0.51.4 +zigpy==0.51.5 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b31882c1b37..2f6238b4c11 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1823,7 +1823,7 @@ zigpy-zigate==0.10.2 zigpy-znp==0.9.1 # homeassistant.components.zha -zigpy==0.51.4 +zigpy==0.51.5 # homeassistant.components.zwave_js zwave-js-server-python==0.43.0 From 6000cc087be63441ef04f882fb76ecd61c3d3afc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Oct 2022 17:32:38 -0500 Subject: [PATCH 906/985] Bump oralb-ble to 0.9.0 (#81166) * Bump oralb-ble to 0.9.0 changelog: https://github.com/Bluetooth-Devices/oralb-ble/compare/v0.8.0...v0.9.0 * empty --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index e25f407add1..8f694946804 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.8.0"], + "requirements": ["oralb-ble==0.9.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 487b407a181..d5806b536a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1238,7 +1238,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.8.0 +oralb-ble==0.9.0 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f6238b4c11..5b6298814d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -883,7 +883,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.8.0 +oralb-ble==0.9.0 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From 6036443d4a9858462c0b75043d83f486df155171 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Oct 2022 22:08:26 -0400 Subject: [PATCH 907/985] Bumped version to 2022.11.0b2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d9964b32b9b..5e864997636 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index cf6598589ae..9ef2808bf19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.0b1" +version = "2022.11.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 6f3b7d009d4577a480d33d52c89f2f5dd52f92a9 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 28 Oct 2022 15:48:16 +0300 Subject: [PATCH 908/985] Add diagnostics to webostv (#81133) --- .../components/webostv/diagnostics.py | 52 ++++++++++++++++ tests/components/webostv/conftest.py | 2 + tests/components/webostv/test_diagnostics.py | 61 +++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 homeassistant/components/webostv/diagnostics.py create mode 100644 tests/components/webostv/test_diagnostics.py diff --git a/homeassistant/components/webostv/diagnostics.py b/homeassistant/components/webostv/diagnostics.py new file mode 100644 index 00000000000..ce62f51b540 --- /dev/null +++ b/homeassistant/components/webostv/diagnostics.py @@ -0,0 +1,52 @@ +"""Diagnostics support for LG webOS Smart TV.""" +from __future__ import annotations + +from typing import Any + +from aiowebostv import WebOsClient + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST, CONF_UNIQUE_ID +from homeassistant.core import HomeAssistant + +from .const import DATA_CONFIG_ENTRY, DOMAIN + +TO_REDACT = { + CONF_CLIENT_SECRET, + CONF_UNIQUE_ID, + CONF_HOST, + "device_id", + "deviceUUID", + "icon", + "largeIcon", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + client: WebOsClient = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id].client + + client_data = { + "is_registered": client.is_registered(), + "is_connected": client.is_connected(), + "current_app_id": client.current_app_id, + "current_channel": client.current_channel, + "apps": client.apps, + "inputs": client.inputs, + "system_info": client.system_info, + "software_info": client.software_info, + "hello_info": client.hello_info, + "sound_output": client.sound_output, + "is_on": client.is_on, + } + + return async_redact_data( + { + "entry": entry.as_dict(), + "client": client_data, + }, + TO_REDACT, + ) diff --git a/tests/components/webostv/conftest.py b/tests/components/webostv/conftest.py index 05f1be66d00..c8333c84447 100644 --- a/tests/components/webostv/conftest.py +++ b/tests/components/webostv/conftest.py @@ -39,6 +39,8 @@ def client_fixture(): client.sound_output = "speaker" client.muted = False client.is_on = True + client.is_registered = Mock(return_value=True) + client.is_connected = Mock(return_value=True) async def mock_state_update_callback(): await client.register_state_update_callback.call_args[0][0](client) diff --git a/tests/components/webostv/test_diagnostics.py b/tests/components/webostv/test_diagnostics.py new file mode 100644 index 00000000000..707f83b2fcf --- /dev/null +++ b/tests/components/webostv/test_diagnostics.py @@ -0,0 +1,61 @@ +"""Tests for the diagnostics data provided by LG webOS Smart TV.""" +from aiohttp import ClientSession + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.core import HomeAssistant + +from . import setup_webostv + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, hass_client: ClientSession, client +) -> None: + """Test diagnostics.""" + entry = await setup_webostv(hass) + assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == { + "client": { + "is_registered": True, + "is_connected": True, + "current_app_id": "com.webos.app.livetv", + "current_channel": { + "channelId": "ch1id", + "channelName": "Channel 1", + "channelNumber": "1", + }, + "apps": { + "com.webos.app.livetv": { + "icon": REDACTED, + "id": "com.webos.app.livetv", + "largeIcon": REDACTED, + "title": "Live TV", + } + }, + "inputs": { + "in1": {"appId": "app0", "id": "in1", "label": "Input01"}, + "in2": {"appId": "app1", "id": "in2", "label": "Input02"}, + }, + "system_info": {"modelName": "TVFAKE"}, + "software_info": {"major_ver": "major", "minor_ver": "minor"}, + "hello_info": {"deviceUUID": "**REDACTED**"}, + "sound_output": "speaker", + "is_on": True, + }, + "entry": { + "entry_id": entry.entry_id, + "version": 1, + "domain": "webostv", + "title": "fake_webos", + "data": { + "client_secret": "**REDACTED**", + "host": "**REDACTED**", + }, + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": REDACTED, + "disabled_by": None, + }, + } From 1b7524a79e80dec127a4993b7a1842bf7993d5f6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 29 Oct 2022 14:26:12 -0400 Subject: [PATCH 909/985] SSDP to allow more URLs (#81171) Co-authored-by: J. Nick Koston --- homeassistant/components/ssdp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 195bebb8321..d081ef877de 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -697,7 +697,7 @@ class Server: udn = await self._async_get_instance_udn() system_info = await async_get_system_info(self.hass) model_name = system_info["installation_type"] - presentation_url = get_url(self.hass) + presentation_url = get_url(self.hass, allow_ip=True, prefer_external=False) serial_number = await async_get_instance_id(self.hass) HassUpnpServiceDevice.DEVICE_DEFINITION = ( HassUpnpServiceDevice.DEVICE_DEFINITION._replace( From 85545e9740df0714388360520da6947077173fe6 Mon Sep 17 00:00:00 2001 From: mezz64 <2854333+mezz64@users.noreply.github.com> Date: Sat, 29 Oct 2022 03:09:12 -0400 Subject: [PATCH 910/985] Bump pyEight to 0.3.2 (#81172) Co-authored-by: J. Nick Koston --- homeassistant/components/eight_sleep/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index c1833b222df..4f97b99b2e7 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -2,7 +2,7 @@ "domain": "eight_sleep", "name": "Eight Sleep", "documentation": "https://www.home-assistant.io/integrations/eight_sleep", - "requirements": ["pyeight==0.3.0"], + "requirements": ["pyeight==0.3.2"], "codeowners": ["@mezz64", "@raman325"], "iot_class": "cloud_polling", "loggers": ["pyeight"], diff --git a/requirements_all.txt b/requirements_all.txt index d5806b536a0..8d8e134b191 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1538,7 +1538,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.eight_sleep -pyeight==0.3.0 +pyeight==0.3.2 # homeassistant.components.emby pyemby==1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b6298814d1..15bf428459d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1081,7 +1081,7 @@ pyeconet==0.1.15 pyefergy==22.1.1 # homeassistant.components.eight_sleep -pyeight==0.3.0 +pyeight==0.3.2 # homeassistant.components.everlights pyeverlights==0.1.0 From 3323bf4ae9062481bfeeda6d3ba2ce971dc58dca Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Oct 2022 23:58:02 -0400 Subject: [PATCH 911/985] Set date in test to fixed one (#81175) --- tests/components/history_stats/test_sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index b384b7c730b..6bae61b5fd8 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -1388,7 +1388,9 @@ async def test_measure_cet(recorder_mock, hass): async def test_end_time_with_microseconds_zeroed(time_zone, recorder_mock, hass): """Test the history statistics sensor that has the end time microseconds zeroed out.""" hass.config.set_time_zone(time_zone) - start_of_today = dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) + start_of_today = dt_util.now().replace( + day=9, month=7, year=1986, hour=0, minute=0, second=0, microsecond=0 + ) start_time = start_of_today + timedelta(minutes=60) t0 = start_time + timedelta(minutes=20) t1 = t0 + timedelta(minutes=10) From 2dd8797f671e86d63f5d0d3ff311cf7f408bea28 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Oct 2022 02:40:40 -0500 Subject: [PATCH 912/985] Bump dbus-fast to 1.56.0 (#81177) * Bump dbus-fast to 1.56.0 Addes optimized readers for manufacturer data and interfaces added messages changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.55.0...v1.56.0 * empty --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index a706d777bc6..3ac8ac513c1 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.4.2", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.54.0" + "dbus-fast==1.56.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d8aef241616..b50deca16bd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.54.0 +dbus-fast==1.56.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 8d8e134b191..ff133e8e0c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.54.0 +dbus-fast==1.56.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 15bf428459d..00648b45794 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.54.0 +dbus-fast==1.56.0 # homeassistant.components.debugpy debugpy==1.6.3 From 43b1dd54d577428527f6eac06e6ce102c29183b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Sat, 29 Oct 2022 17:04:05 +0200 Subject: [PATCH 913/985] Bump pysma to 0.7.2 (#81188) --- homeassistant/components/sma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index c65f3b81d3b..83bf4258a95 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "SMA Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.7.1"], + "requirements": ["pysma==0.7.2"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling", "loggers": ["pysma"] diff --git a/requirements_all.txt b/requirements_all.txt index ff133e8e0c4..1f0b35e9706 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1890,7 +1890,7 @@ pysignalclirestapi==0.3.18 pyskyqhub==0.1.4 # homeassistant.components.sma -pysma==0.7.1 +pysma==0.7.2 # homeassistant.components.smappee pysmappee==0.2.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00648b45794..c1ec57f8d67 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1331,7 +1331,7 @@ pysiaalarm==3.0.2 pysignalclirestapi==0.3.18 # homeassistant.components.sma -pysma==0.7.1 +pysma==0.7.2 # homeassistant.components.smappee pysmappee==0.2.29 From 62635c2a96d30a926010b05ef9474b223e08faa7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Oct 2022 13:22:46 -0500 Subject: [PATCH 914/985] Bump dbus-fast to 1.58.0 (#81195) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 3ac8ac513c1..0db0433de2b 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.4.2", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.56.0" + "dbus-fast==1.58.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b50deca16bd..2d33e11a547 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.56.0 +dbus-fast==1.58.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1f0b35e9706..ff96787ca76 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.56.0 +dbus-fast==1.58.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c1ec57f8d67..4e996aebab6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.56.0 +dbus-fast==1.58.0 # homeassistant.components.debugpy debugpy==1.6.3 From bf04f94e0535b49fed9d6981f2d2482eca65a4eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Oct 2022 13:25:35 -0500 Subject: [PATCH 915/985] Update to bleak 0.19.1 and bleak-retry-connector 2.5.0 (#81198) --- homeassistant/components/bluetooth/manifest.json | 4 ++-- homeassistant/package_constraints.txt | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 0db0433de2b..442759382d7 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,8 +6,8 @@ "after_dependencies": ["hassio"], "quality_scale": "internal", "requirements": [ - "bleak==0.19.0", - "bleak-retry-connector==2.4.2", + "bleak==0.19.1", + "bleak-retry-connector==2.5.0", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.58.0" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2d33e11a547..e8e520c29ba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,8 +10,8 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.4.2 -bleak==0.19.0 +bleak-retry-connector==2.5.0 +bleak==0.19.1 bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 diff --git a/requirements_all.txt b/requirements_all.txt index ff96787ca76..a099a826559 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,10 +413,10 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.4.2 +bleak-retry-connector==2.5.0 # homeassistant.components.bluetooth -bleak==0.19.0 +bleak==0.19.1 # homeassistant.components.blebox blebox_uniapi==2.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e996aebab6..d81b9483727 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,10 +337,10 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.4.2 +bleak-retry-connector==2.5.0 # homeassistant.components.bluetooth -bleak==0.19.0 +bleak==0.19.1 # homeassistant.components.blebox blebox_uniapi==2.1.3 From 16fe7df19e2c295f682860a089a7118473fe291b Mon Sep 17 00:00:00 2001 From: Menco Bolt Date: Sat, 29 Oct 2022 20:25:46 +0200 Subject: [PATCH 916/985] Today's Consumption is INCREASING (#81204) Co-authored-by: Paulus Schoutsen --- homeassistant/components/enphase_envoy/const.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index c79c3af604b..7c493168526 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -34,7 +34,7 @@ SENSORS = ( key="seven_days_production", name="Last Seven Days Energy Production", native_unit_of_measurement=ENERGY_WATT_HOUR, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, ), SensorEntityDescription( @@ -54,14 +54,14 @@ SENSORS = ( key="daily_consumption", name="Today's Energy Consumption", native_unit_of_measurement=ENERGY_WATT_HOUR, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, ), SensorEntityDescription( key="seven_days_consumption", name="Last Seven Days Energy Consumption", native_unit_of_measurement=ENERGY_WATT_HOUR, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, ), SensorEntityDescription( From d0a0285dd9b90f75a84dccd0f4d0df62772d3a26 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Oct 2022 14:05:59 -0500 Subject: [PATCH 917/985] Restore homekit_controller BLE broadcast_key from disk (#81211) * Restore homekit_controller BLE broadcast_key from disk Some accessories will sleep for a long time and only send broadcasted events which makes them have very long connection intervals to save battery. Since we need to connect to get a new broadcast key we now save the broadcast key between restarts to ensure we can decrypt the advertisments coming in even though we cannot make a connection to the device during startup. When we get a disconnected event later we will try again to connect and the device will be awake which will trigger a full sync * bump bump --- .../homekit_controller/config_flow.py | 3 ++- .../homekit_controller/manifest.json | 2 +- .../components/homekit_controller/storage.py | 27 ++++++++----------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 16 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 62144077a94..da4ccfe9f9a 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -15,7 +15,7 @@ from aiohomekit.controller.abstract import ( from aiohomekit.exceptions import AuthenticationError from aiohomekit.model.categories import Categories from aiohomekit.model.status_flags import StatusFlags -from aiohomekit.utils import domain_supported, domain_to_name +from aiohomekit.utils import domain_supported, domain_to_name, serialize_broadcast_key import voluptuous as vol from homeassistant import config_entries @@ -577,6 +577,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): pairing.id, accessories_state.config_num, accessories_state.accessories.serialize(), + serialize_broadcast_key(accessories_state.broadcast_key), ) return self.async_create_entry(title=name, data=pairing_data) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 5aaae67d1d3..224b24f6077 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.7"], + "requirements": ["aiohomekit==2.2.8"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index 51d8ce4ffd3..a5afb07620a 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -3,7 +3,9 @@ from __future__ import annotations import logging -from typing import Any, TypedDict +from typing import Any + +from aiohomekit.characteristic_cache import Pairing, StorageLayout from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.storage import Store @@ -16,19 +18,6 @@ ENTITY_MAP_SAVE_DELAY = 10 _LOGGER = logging.getLogger(__name__) -class Pairing(TypedDict): - """A versioned map of entity metadata as presented by aiohomekit.""" - - config_num: int - accessories: list[Any] - - -class StorageLayout(TypedDict): - """Cached pairing metadata needed by aiohomekit.""" - - pairings: dict[str, Pairing] - - class EntityMapStorage: """ Holds a cache of entity structure data from a paired HomeKit device. @@ -67,11 +56,17 @@ class EntityMapStorage: @callback def async_create_or_update_map( - self, homekit_id: str, config_num: int, accessories: list[Any] + self, + homekit_id: str, + config_num: int, + accessories: list[Any], + broadcast_key: str | None = None, ) -> Pairing: """Create a new pairing cache.""" _LOGGER.debug("Creating or updating entity map for %s", homekit_id) - data = Pairing(config_num=config_num, accessories=accessories) + data = Pairing( + config_num=config_num, accessories=accessories, broadcast_key=broadcast_key + ) self.storage_data[homekit_id] = data self._async_schedule_save() return data diff --git a/requirements_all.txt b/requirements_all.txt index a099a826559..13904065e33 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.7 +aiohomekit==2.2.8 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d81b9483727..ec6e1853a3b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.7 +aiohomekit==2.2.8 # homeassistant.components.emulated_hue # homeassistant.components.http From 7e740b7c9d7cfa8b546fcf556c720e625fa4b30a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Oct 2022 14:06:17 -0500 Subject: [PATCH 918/985] Bump dbus-fast to 1.59.0 (#81215) * Bump dbus-fast to 1.59.0 changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.58.0...v1.59.0 * empty --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 442759382d7..a5ea8c171d8 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.5.0", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.58.0" + "dbus-fast==1.59.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e8e520c29ba..413a86be041 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.58.0 +dbus-fast==1.59.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 13904065e33..a4469cfef3b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.58.0 +dbus-fast==1.59.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec6e1853a3b..e0388c86ed9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.58.0 +dbus-fast==1.59.0 # homeassistant.components.debugpy debugpy==1.6.3 From 96cdb2975566cfb6da59cada48553f990d2fe62f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 29 Oct 2022 15:07:25 -0400 Subject: [PATCH 919/985] Bumped version to 2022.11.0b3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5e864997636..3f25ea89c09 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 9ef2808bf19..5a9507f8dfa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.0b2" +version = "2022.11.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 0465510ed7c2e3457d171b8fc023f28887bc7a94 Mon Sep 17 00:00:00 2001 From: Raj Laud <50647620+rajlaud@users.noreply.github.com> Date: Sun, 30 Oct 2022 01:23:46 -0400 Subject: [PATCH 920/985] Fix Squeezebox media browsing (#81197) * Squeezebox media browser fix icons * Update pysqueezebox to 0.6.1 --- homeassistant/components/squeezebox/browse_media.py | 1 - homeassistant/components/squeezebox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index 979b4c36a98..c66bc8af9a5 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -156,7 +156,6 @@ async def library_payload(hass, player): media_content_type=item, can_play=True, can_expand=True, - thumbnail="https://brands.home-assistant.io/_/squeezebox/logo.png", ) ) diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json index 018333d420b..2c1692b6085 100644 --- a/homeassistant/components/squeezebox/manifest.json +++ b/homeassistant/components/squeezebox/manifest.json @@ -3,7 +3,7 @@ "name": "Squeezebox (Logitech Media Server)", "documentation": "https://www.home-assistant.io/integrations/squeezebox", "codeowners": ["@rajlaud"], - "requirements": ["pysqueezebox==0.6.0"], + "requirements": ["pysqueezebox==0.6.1"], "config_flow": true, "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index a4469cfef3b..410dde0b5e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1920,7 +1920,7 @@ pysoma==0.0.10 pyspcwebgw==0.4.0 # homeassistant.components.squeezebox -pysqueezebox==0.6.0 +pysqueezebox==0.6.1 # homeassistant.components.stiebel_eltron pystiebeleltron==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e0388c86ed9..5dcb1bf40ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1355,7 +1355,7 @@ pysoma==0.0.10 pyspcwebgw==0.4.0 # homeassistant.components.squeezebox -pysqueezebox==0.6.0 +pysqueezebox==0.6.1 # homeassistant.components.switchbee pyswitchbee==1.5.5 From 8d3ed60986bcff4f89aa6db77460bea5c3ef93c4 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Sat, 29 Oct 2022 23:51:53 +0200 Subject: [PATCH 921/985] Fix Danfoss thermostat support in devolo Home Control (#81200) Fix Danfoss thermostat --- homeassistant/components/devolo_home_control/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index 95e0628d534..6c566aa45e3 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -35,6 +35,7 @@ async def async_setup_entry( "devolo.model.Thermostat:Valve", "devolo.model.Room:Thermostat", "devolo.model.Eurotronic:Spirit:Device", + "unk.model.Danfoss:Thermostat", ): entities.append( DevoloClimateDeviceEntity( From be138adb2336418d49423dacdbf098edae68e4ec Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Sat, 29 Oct 2022 23:51:11 +0200 Subject: [PATCH 922/985] Add missing string for option traffic_mode for google_travel_time (#81213) Add missing string for option traffic_mode --- homeassistant/components/google_travel_time/strings.json | 1 + homeassistant/components/google_travel_time/translations/en.json | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/google_travel_time/strings.json b/homeassistant/components/google_travel_time/strings.json index 22a122b9a53..78b84038c7f 100644 --- a/homeassistant/components/google_travel_time/strings.json +++ b/homeassistant/components/google_travel_time/strings.json @@ -30,6 +30,7 @@ "time_type": "Time Type", "time": "Time", "avoid": "Avoid", + "traffic_mode": "Traffic Mode", "transit_mode": "Transit Mode", "transit_routing_preference": "Transit Routing Preference", "units": "Units" diff --git a/homeassistant/components/google_travel_time/translations/en.json b/homeassistant/components/google_travel_time/translations/en.json index 8e91fbf1df0..dd03dca1d2f 100644 --- a/homeassistant/components/google_travel_time/translations/en.json +++ b/homeassistant/components/google_travel_time/translations/en.json @@ -28,6 +28,7 @@ "mode": "Travel Mode", "time": "Time", "time_type": "Time Type", + "traffic_mode": "Traffic Mode", "transit_mode": "Transit Mode", "transit_routing_preference": "Transit Routing Preference", "units": "Units" From 24b3d218153fe6c692384609dedc34342237a47f Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 30 Oct 2022 00:04:01 +0200 Subject: [PATCH 923/985] Mute superfluous exception when no Netatmo webhook is to be dropped (#81221) * Mute superfluous exception when no webhook is to be droped * Update homeassistant/components/netatmo/__init__.py Co-authored-by: Paulus Schoutsen --- homeassistant/components/netatmo/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index eb0e93c4b38..aa8728d548d 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -271,7 +271,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if CONF_WEBHOOK_ID in entry.data: webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) - await data[entry.entry_id][AUTH].async_dropwebhook() + try: + await data[entry.entry_id][AUTH].async_dropwebhook() + except pyatmo.ApiError: + _LOGGER.debug("No webhook to be dropped") _LOGGER.info("Unregister Netatmo webhook") unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) From a6bb7a083201664ab3d6514265305f2199aa533e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 05:32:57 -0500 Subject: [PATCH 924/985] Bump dbus-fast to 1.59.1 (#81229) * Bump dbus-fast to 1.59.1 fixes incorrect logging of an exception when it was already handled changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.59.0...v1.59.1 * empty --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index a5ea8c171d8..f8d1867035d 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.5.0", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.59.0" + "dbus-fast==1.59.1" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 413a86be041..d48b85e346b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.59.0 +dbus-fast==1.59.1 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 410dde0b5e5..4914538c575 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.59.0 +dbus-fast==1.59.1 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5dcb1bf40ab..e2f3f0b8685 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.59.0 +dbus-fast==1.59.1 # homeassistant.components.debugpy debugpy==1.6.3 From 11bdddc1dc983e75b453f07851c11b05e90c7576 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 30 Oct 2022 20:01:10 +0100 Subject: [PATCH 925/985] Catch `ApiError` while checking credentials in NAM integration (#81243) * Catch ApiError while checking credentials * Update tests * Suggested change --- homeassistant/components/nam/__init__.py | 2 ++ tests/components/nam/test_init.py | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 25615db6eed..0fbc9384634 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -56,6 +56,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await nam.async_check_credentials() + except ApiError as err: + raise ConfigEntryNotReady from err except AuthFailed as err: raise ConfigEntryAuthFailed from err diff --git a/tests/components/nam/test_init.py b/tests/components/nam/test_init.py index b6f278d4e94..a6d11305599 100644 --- a/tests/components/nam/test_init.py +++ b/tests/components/nam/test_init.py @@ -32,12 +32,30 @@ async def test_config_not_ready(hass): unique_id="aa:bb:cc:dd:ee:ff", data={"host": "10.10.2.3"}, ) + entry.add_to_hass(hass) with patch( "homeassistant.components.nam.NettigoAirMonitor.initialize", side_effect=ApiError("API Error"), ): - entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_config_not_ready_while_checking_credentials(hass): + """Test for setup failure if the connection fails while checking credentials.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="10.10.2.3", + unique_id="aa:bb:cc:dd:ee:ff", + data={"host": "10.10.2.3"}, + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + side_effect=ApiError("API Error"), + ): await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.SETUP_RETRY @@ -50,12 +68,12 @@ async def test_config_auth_failed(hass): unique_id="aa:bb:cc:dd:ee:ff", data={"host": "10.10.2.3"}, ) + entry.add_to_hass(hass) with patch( "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", side_effect=AuthFailed("Authorization has failed"), ): - entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.SETUP_ERROR From 90a36894896fbfd5db4023871d4d2e623b7149c2 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 30 Oct 2022 13:27:42 +0100 Subject: [PATCH 926/985] Make Netatmo/Legrande/BTicino lights and switches optimistic (#81246) * Make Netatmo lights optimistic * Same for switches --- homeassistant/components/netatmo/light.py | 7 +++++-- homeassistant/components/netatmo/switch.py | 4 ++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index b3e352eb7d8..e3bd8952b55 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -193,17 +193,20 @@ class NetatmoLight(NetatmoBase, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn light on.""" - _LOGGER.debug("Turn light '%s' on", self.name) if ATTR_BRIGHTNESS in kwargs: await self._dimmer.async_set_brightness(kwargs[ATTR_BRIGHTNESS]) else: await self._dimmer.async_on() + self._attr_is_on = True + self.async_write_ha_state() + async def async_turn_off(self, **kwargs: Any) -> None: """Turn light off.""" - _LOGGER.debug("Turn light '%s' off", self.name) await self._dimmer.async_off() + self._attr_is_on = False + self.async_write_ha_state() @callback def async_update_callback(self) -> None: diff --git a/homeassistant/components/netatmo/switch.py b/homeassistant/components/netatmo/switch.py index 338d073c205..a2e2e67db39 100644 --- a/homeassistant/components/netatmo/switch.py +++ b/homeassistant/components/netatmo/switch.py @@ -77,7 +77,11 @@ class NetatmoSwitch(NetatmoBase, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the zone on.""" await self._switch.async_on() + self._attr_is_on = True + self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the zone off.""" await self._switch.async_off() + self._attr_is_on = False + self.async_write_ha_state() From 9d88c953147ef24877b0aff6a2aae9a2eb649a81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 10:35:39 -0500 Subject: [PATCH 927/985] Bump aiohomekit to 2.2.9 (#81254) --- 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 224b24f6077..58e258294a0 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.8"], + "requirements": ["aiohomekit==2.2.9"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 4914538c575..b3b5a00e8b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.8 +aiohomekit==2.2.9 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2f3f0b8685..54ae8f373e3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.8 +aiohomekit==2.2.9 # homeassistant.components.emulated_hue # homeassistant.components.http From 5f81f968ee3761f3fcc28cb95c27111e225a0b36 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sun, 30 Oct 2022 15:32:19 +0000 Subject: [PATCH 928/985] Set the correct state class for Eve Energy in homekit_controller (#81255) --- homeassistant/components/homekit_controller/sensor.py | 2 +- .../homekit_controller/specific_devices/test_eve_energy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 150f2badc6b..49047b28eae 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -183,7 +183,7 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { key=CharacteristicsTypes.VENDOR_EVE_ENERGY_KW_HOUR, name="Energy kWh", device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), CharacteristicsTypes.VENDOR_EVE_ENERGY_VOLTAGE: HomeKitSensorEntityDescription( diff --git a/tests/components/homekit_controller/specific_devices/test_eve_energy.py b/tests/components/homekit_controller/specific_devices/test_eve_energy.py index 65e5c16179f..e678b3bbbaa 100644 --- a/tests/components/homekit_controller/specific_devices/test_eve_energy.py +++ b/tests/components/homekit_controller/specific_devices/test_eve_energy.py @@ -70,7 +70,7 @@ async def test_eve_energy_setup(hass): entity_id="sensor.eve_energy_50ff_energy_kwh", unique_id="00:00:00:00:00:00_1_28_35", friendly_name="Eve Energy 50FF Energy kWh", - capabilities={"state_class": SensorStateClass.MEASUREMENT}, + capabilities={"state_class": SensorStateClass.TOTAL_INCREASING}, unit_of_measurement=ENERGY_KILO_WATT_HOUR, state="0.28999999165535", ), From 0af69a1014e28ed582a65a1b6faae98d7680422d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 08:35:08 -0500 Subject: [PATCH 929/985] Significantly reduce clock_gettime syscalls on platforms with broken vdso (#81257) --- .../bluetooth/active_update_coordinator.py | 6 ++--- homeassistant/components/bluetooth/manager.py | 4 +-- homeassistant/components/bluetooth/scanner.py | 4 +-- homeassistant/components/bluetooth/util.py | 4 +-- .../components/esphome/bluetooth/scanner.py | 3 ++- homeassistant/util/dt.py | 26 +++++++++++++++++++ tests/util/test_dt.py | 6 +++++ 7 files changed, 43 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/bluetooth/active_update_coordinator.py b/homeassistant/components/bluetooth/active_update_coordinator.py index 37f049d3e07..ab26a0260f3 100644 --- a/homeassistant/components/bluetooth/active_update_coordinator.py +++ b/homeassistant/components/bluetooth/active_update_coordinator.py @@ -3,13 +3,13 @@ from __future__ import annotations from collections.abc import Callable, Coroutine import logging -import time from typing import Any, Generic, TypeVar from bleak import BleakError from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer +from homeassistant.util.dt import monotonic_time_coarse from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .passive_update_processor import PassiveBluetoothProcessorCoordinator @@ -94,7 +94,7 @@ class ActiveBluetoothProcessorCoordinator( """Return true if time to try and poll.""" poll_age: float | None = None if self._last_poll: - poll_age = time.monotonic() - self._last_poll + poll_age = monotonic_time_coarse() - self._last_poll return self._needs_poll_method(service_info, poll_age) async def _async_poll_data( @@ -124,7 +124,7 @@ class ActiveBluetoothProcessorCoordinator( self.last_poll_successful = False return finally: - self._last_poll = time.monotonic() + self._last_poll = monotonic_time_coarse() if not self.last_poll_successful: self.logger.debug("%s: Polling recovered") diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index aaefd3dcfc4..c3a0e0998f1 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -7,7 +7,6 @@ from dataclasses import replace from datetime import datetime, timedelta import itertools import logging -import time from typing import TYPE_CHECKING, Any, Final from bleak.backends.scanner import AdvertisementDataCallback @@ -22,6 +21,7 @@ from homeassistant.core import ( ) from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.dt import monotonic_time_coarse from .advertisement_tracker import AdvertisementTracker from .const import ( @@ -69,7 +69,7 @@ APPLE_START_BYTES_WANTED: Final = { APPLE_DEVICE_ID_START_BYTE, } -MONOTONIC_TIME: Final = time.monotonic +MONOTONIC_TIME: Final = monotonic_time_coarse _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index fe795f7ace5..6b23cae0218 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -6,7 +6,6 @@ from collections.abc import Callable from datetime import datetime import logging import platform -import time from typing import Any import async_timeout @@ -22,6 +21,7 @@ from dbus_fast import InvalidMessageError from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.dt import monotonic_time_coarse from homeassistant.util.package import is_docker_env from .const import ( @@ -35,7 +35,7 @@ from .models import BaseHaScanner, BluetoothScanningMode, BluetoothServiceInfoBl from .util import adapter_human_name, async_reset_adapter OriginalBleakScanner = bleak.BleakScanner -MONOTONIC_TIME = time.monotonic +MONOTONIC_TIME = monotonic_time_coarse # or_patterns is a workaround for the fact that passive scanning # needs at least one matcher to be set. The below matcher diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 860428a6106..181796d3d2d 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -2,11 +2,11 @@ from __future__ import annotations import platform -import time from bluetooth_auto_recovery import recover_adapter from homeassistant.core import callback +from homeassistant.util.dt import monotonic_time_coarse from .const import ( DEFAULT_ADAPTER_BY_PLATFORM, @@ -29,7 +29,7 @@ async def async_load_history_from_system() -> dict[str, BluetoothServiceInfoBlea bluez_dbus = BlueZDBusObjects() await bluez_dbus.load() - now = time.monotonic() + now = monotonic_time_coarse() return { address: BluetoothServiceInfoBleak( name=history.advertisement_data.local_name diff --git a/homeassistant/components/esphome/bluetooth/scanner.py b/homeassistant/components/esphome/bluetooth/scanner.py index 284e605fdfa..7c8064d5583 100644 --- a/homeassistant/components/esphome/bluetooth/scanner.py +++ b/homeassistant/components/esphome/bluetooth/scanner.py @@ -19,6 +19,7 @@ from homeassistant.components.bluetooth import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.dt import monotonic_time_coarse TWO_CHAR = re.compile("..") @@ -84,7 +85,7 @@ class ESPHomeScanner(BaseHaScanner): @callback def async_on_advertisement(self, adv: BluetoothLEAdvertisement) -> None: """Call the registered callback.""" - now = time.monotonic() + now = monotonic_time_coarse() address = ":".join(TWO_CHAR.findall("%012X" % adv.address)) # must be upper name = adv.name if prev_discovery := self._discovered_device_advertisement_datas.get(address): diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 80b322c1a14..44e4403d689 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -4,7 +4,9 @@ from __future__ import annotations import bisect from contextlib import suppress import datetime as dt +import platform import re +import time from typing import Any import zoneinfo @@ -13,6 +15,7 @@ import ciso8601 DATE_STR_FORMAT = "%Y-%m-%d" UTC = dt.timezone.utc DEFAULT_TIME_ZONE: dt.tzinfo = dt.timezone.utc +CLOCK_MONOTONIC_COARSE = 6 # EPOCHORDINAL is not exposed as a constant # https://github.com/python/cpython/blob/3.10/Lib/zoneinfo/_zoneinfo.py#L12 @@ -461,3 +464,26 @@ def _datetime_ambiguous(dattim: dt.datetime) -> bool: assert dattim.tzinfo is not None opposite_fold = dattim.replace(fold=not dattim.fold) return _datetime_exists(dattim) and dattim.utcoffset() != opposite_fold.utcoffset() + + +def __monotonic_time_coarse() -> float: + """Return a monotonic time in seconds. + + This is the coarse version of time_monotonic, which is faster but less accurate. + + Since many arm64 and 32-bit platforms don't support VDSO with time.monotonic + because of errata, we can't rely on the kernel to provide a fast + monotonic time. + + https://lore.kernel.org/lkml/20170404171826.25030-1-marc.zyngier@arm.com/ + """ + return time.clock_gettime(CLOCK_MONOTONIC_COARSE) + + +monotonic_time_coarse = time.monotonic +with suppress(Exception): + if ( + platform.system() == "Linux" + and abs(time.monotonic() - __monotonic_time_coarse()) < 1 + ): + monotonic_time_coarse = __monotonic_time_coarse diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 79cd4e5e0df..e902176bb35 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import datetime, timedelta +import time import pytest @@ -719,3 +720,8 @@ def test_find_next_time_expression_tenth_second_pattern_does_not_drift_entering_ assert (next_target - prev_target).total_seconds() == 60 assert next_target.second == 10 prev_target = next_target + + +def test_monotonic_time_coarse(): + """Test monotonic time coarse.""" + assert abs(time.monotonic() - dt_util.monotonic_time_coarse()) < 1 From c36260dd17e2ac4e64362d796076e35b79260401 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 18:02:54 -0500 Subject: [PATCH 930/985] Move esphome gatt services cache to be per device (#81265) --- .../components/esphome/bluetooth/client.py | 6 +++--- .../components/esphome/domain_data.py | 20 ------------------- .../components/esphome/entry_data.py | 20 ++++++++++++++++++- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 9094186226f..6be722976c5 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -255,9 +255,9 @@ class ESPHomeClient(BaseBleakClient): A :py:class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree. """ address_as_int = self._address_as_int - domain_data = self.domain_data + entry_data = self.entry_data if dangerous_use_bleak_cache and ( - cached_services := domain_data.get_gatt_services_cache(address_as_int) + cached_services := entry_data.get_gatt_services_cache(address_as_int) ): _LOGGER.debug( "Cached services hit for %s - %s", @@ -301,7 +301,7 @@ class ESPHomeClient(BaseBleakClient): self._ble_device.name, self._ble_device.address, ) - domain_data.set_gatt_services_cache(address_as_int, services) + entry_data.set_gatt_services_cache(address_as_int, services) return services def _resolve_characteristic( diff --git a/homeassistant/components/esphome/domain_data.py b/homeassistant/components/esphome/domain_data.py index acaa76185e7..01f0a4d6b13 100644 --- a/homeassistant/components/esphome/domain_data.py +++ b/homeassistant/components/esphome/domain_data.py @@ -1,13 +1,9 @@ """Support for esphome domain data.""" from __future__ import annotations -from collections.abc import MutableMapping from dataclasses import dataclass, field from typing import TypeVar, cast -from bleak.backends.service import BleakGATTServiceCollection -from lru import LRU # pylint: disable=no-name-in-module - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.json import JSONEncoder @@ -17,7 +13,6 @@ from .entry_data import RuntimeEntryData STORAGE_VERSION = 1 DOMAIN = "esphome" -MAX_CACHED_SERVICES = 128 _DomainDataSelfT = TypeVar("_DomainDataSelfT", bound="DomainData") @@ -29,21 +24,6 @@ class DomainData: _entry_datas: dict[str, RuntimeEntryData] = field(default_factory=dict) _stores: dict[str, Store] = field(default_factory=dict) _entry_by_unique_id: dict[str, ConfigEntry] = field(default_factory=dict) - _gatt_services_cache: MutableMapping[int, BleakGATTServiceCollection] = field( - default_factory=lambda: LRU(MAX_CACHED_SERVICES) # type: ignore[no-any-return] - ) - - def get_gatt_services_cache( - self, address: int - ) -> BleakGATTServiceCollection | None: - """Get the BleakGATTServiceCollection for the given address.""" - return self._gatt_services_cache.get(address) - - def set_gatt_services_cache( - self, address: int, services: BleakGATTServiceCollection - ) -> None: - """Set the BleakGATTServiceCollection for the given address.""" - self._gatt_services_cache[address] = services def get_by_unique_id(self, unique_id: str) -> ConfigEntry: """Get the config entry by its unique ID.""" diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index ac2a148d899..5d474b0fb15 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, MutableMapping from dataclasses import dataclass, field import logging from typing import Any, cast @@ -30,6 +30,8 @@ from aioesphomeapi import ( UserService, ) from aioesphomeapi.model import ButtonInfo +from bleak.backends.service import BleakGATTServiceCollection +from lru import LRU # pylint: disable=no-name-in-module from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -57,6 +59,7 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { SwitchInfo: Platform.SWITCH, TextSensorInfo: Platform.SENSOR, } +MAX_CACHED_SERVICES = 128 @dataclass @@ -92,6 +95,21 @@ class RuntimeEntryData: _ble_connection_free_futures: list[asyncio.Future[int]] = field( default_factory=list ) + _gatt_services_cache: MutableMapping[int, BleakGATTServiceCollection] = field( + default_factory=lambda: LRU(MAX_CACHED_SERVICES) # type: ignore[no-any-return] + ) + + def get_gatt_services_cache( + self, address: int + ) -> BleakGATTServiceCollection | None: + """Get the BleakGATTServiceCollection for the given address.""" + return self._gatt_services_cache.get(address) + + def set_gatt_services_cache( + self, address: int, services: BleakGATTServiceCollection + ) -> None: + """Set the BleakGATTServiceCollection for the given address.""" + self._gatt_services_cache[address] = services @callback def async_update_ble_connection_limits(self, free: int, limit: int) -> None: From 5e3fb6ee9fe038a0ad29dd8e5d5d9119de363708 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 17:43:09 -0500 Subject: [PATCH 931/985] Provide a human readable error when an esphome ble proxy connection fails (#81266) --- homeassistant/components/esphome/bluetooth/client.py | 12 +++++++++++- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 6be722976c5..918d93f3d2c 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -7,6 +7,7 @@ import logging from typing import Any, TypeVar, cast import uuid +from aioesphomeapi import ESP_CONNECTION_ERROR_DESCRIPTION, BLEConnectionError from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError import async_timeout from bleak.backends.characteristic import BleakGATTCharacteristic @@ -182,8 +183,17 @@ class ESPHomeClient(BaseBleakClient): return if error: + try: + ble_connection_error = BLEConnectionError(error) + ble_connection_error_name = ble_connection_error.name + human_error = ESP_CONNECTION_ERROR_DESCRIPTION[ble_connection_error] + except (KeyError, ValueError): + ble_connection_error_name = str(error) + human_error = f"Unknown error code {error}" connected_future.set_exception( - BleakError(f"Error while connecting: {error}") + BleakError( + f"Error {ble_connection_error_name} while connecting: {human_error}" + ) ) return diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index ab33ed8585a..c0230ce8410 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==11.2.0"], + "requirements": ["aioesphomeapi==11.3.0"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index b3b5a00e8b5..8fb8163c6c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -153,7 +153,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.2.0 +aioesphomeapi==11.3.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 54ae8f373e3..b15626d1ba2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.2.0 +aioesphomeapi==11.3.0 # homeassistant.components.flo aioflo==2021.11.0 From 94f92e7f8aa702efaed7679959abcdeaa6ce3dde Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 08:27:04 -0500 Subject: [PATCH 932/985] Try to switch to a different esphome BLE proxy if we run out of slots while connecting (#81268) --- homeassistant/components/bluetooth/models.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index a2e50fe1182..a63a704baf6 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -264,6 +264,7 @@ class HaBleakClientWrapper(BleakClient): self.__address = address_or_ble_device self.__disconnected_callback = disconnected_callback self.__timeout = timeout + self.__ble_device: BLEDevice | None = None self._backend: BaseBleakClient | None = None # type: ignore[assignment] @property @@ -283,14 +284,21 @@ class HaBleakClientWrapper(BleakClient): async def connect(self, **kwargs: Any) -> bool: """Connect to the specified GATT server.""" - if not self._backend: + if ( + not self._backend + or not self.__ble_device + or not self._async_get_backend_for_ble_device(self.__ble_device) + ): assert MANAGER is not None wrapped_backend = ( self._async_get_backend() or self._async_get_fallback_backend() ) - self._backend = wrapped_backend.client( + self.__ble_device = ( await freshen_ble_device(wrapped_backend.device) - or wrapped_backend.device, + or wrapped_backend.device + ) + self._backend = wrapped_backend.client( + self.__ble_device, disconnected_callback=self.__disconnected_callback, timeout=self.__timeout, hass=MANAGER.hass, From 8bafb56f0422ce5d8ec8e56278526e8e4e973c50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 17:38:09 -0500 Subject: [PATCH 933/985] Bump bleak-retry-connector to 2.6.0 (#81270) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index f8d1867035d..261b4480671 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.1", - "bleak-retry-connector==2.5.0", + "bleak-retry-connector==2.6.0", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.59.1" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d48b85e346b..6762357d58d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.5.0 +bleak-retry-connector==2.6.0 bleak==0.19.1 bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index 8fb8163c6c7..823073fc317 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.5.0 +bleak-retry-connector==2.6.0 # homeassistant.components.bluetooth bleak==0.19.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b15626d1ba2..51b4e5ad7ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,7 +337,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.5.0 +bleak-retry-connector==2.6.0 # homeassistant.components.bluetooth bleak==0.19.1 From e26149d0c34653ce21c879a38428f4d5940d8bf8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 19:24:14 -0500 Subject: [PATCH 934/985] Bump aioesphomeapi to 11.4.0 (#81277) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index c0230ce8410..cab81882788 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==11.3.0"], + "requirements": ["aioesphomeapi==11.4.0"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index 823073fc317..893c4c05eff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -153,7 +153,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.3.0 +aioesphomeapi==11.4.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51b4e5ad7ce..41e2cc92f63 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.3.0 +aioesphomeapi==11.4.0 # homeassistant.components.flo aioflo==2021.11.0 From 9fac632dcd064f6f895ef9a5cc3f34b3fbb5cfaa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 19:24:32 -0500 Subject: [PATCH 935/985] Bump bleak-retry-connector to 2.7.0 (#81280) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 261b4480671..6b799e94e55 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.1", - "bleak-retry-connector==2.6.0", + "bleak-retry-connector==2.7.0", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.59.1" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6762357d58d..f75f7ba60da 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.6.0 +bleak-retry-connector==2.7.0 bleak==0.19.1 bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index 893c4c05eff..e3fdcfb3746 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.6.0 +bleak-retry-connector==2.7.0 # homeassistant.components.bluetooth bleak==0.19.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 41e2cc92f63..baf6bff2e32 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,7 +337,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.6.0 +bleak-retry-connector==2.7.0 # homeassistant.components.bluetooth bleak==0.19.1 From eccf61a546a4654c20544b371d2856ea1cdfa58b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 20:39:34 -0500 Subject: [PATCH 936/985] Bump aioesphomeapi to 11.4.1 (#81282) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index cab81882788..c27e3b8dc3e 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==11.4.0"], + "requirements": ["aioesphomeapi==11.4.1"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index e3fdcfb3746..0e001a54c7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -153,7 +153,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.0 +aioesphomeapi==11.4.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index baf6bff2e32..885b26e24bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.0 +aioesphomeapi==11.4.1 # homeassistant.components.flo aioflo==2021.11.0 From 81dde5cfdf6c1fc5ef5ccc82b01abb05b9b94251 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 20:40:01 -0500 Subject: [PATCH 937/985] Bump bleak-retry-connector to 2.8.0 (#81283) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 6b799e94e55..660345606c8 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.1", - "bleak-retry-connector==2.7.0", + "bleak-retry-connector==2.8.0", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.59.1" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f75f7ba60da..994a8d44019 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.7.0 +bleak-retry-connector==2.8.0 bleak==0.19.1 bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index 0e001a54c7e..e35cdbeee77 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.7.0 +bleak-retry-connector==2.8.0 # homeassistant.components.bluetooth bleak==0.19.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 885b26e24bc..23f1b6122d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,7 +337,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.7.0 +bleak-retry-connector==2.8.0 # homeassistant.components.bluetooth bleak==0.19.1 From 1f70941f6daea91af735749e07be6f3c9e519aee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 22:10:30 -0500 Subject: [PATCH 938/985] Do not fire the esphome ble disconnected callback if we were not connected (#81286) --- homeassistant/components/esphome/bluetooth/client.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 918d93f3d2c..68f1788afdb 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -127,13 +127,15 @@ class ESPHomeClient(BaseBleakClient): def _async_ble_device_disconnected(self) -> None: """Handle the BLE device disconnecting from the ESP.""" - _LOGGER.debug("%s: BLE device disconnected", self._source) - self._is_connected = False + was_connected = self._is_connected self.services = BleakGATTServiceCollection() # type: ignore[no-untyped-call] + self._is_connected = False if self._disconnected_event: self._disconnected_event.set() self._disconnected_event = None - self._async_call_bleak_disconnected_callback() + if was_connected: + _LOGGER.debug("%s: BLE device disconnected", self._source) + self._async_call_bleak_disconnected_callback() self._unsubscribe_connection_state() def _async_esp_disconnected(self) -> None: From 3cf63ec88ed0dd7d37318083293d59da3dc8dc51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 00:31:37 -0500 Subject: [PATCH 939/985] Include esphome device name in BLE logs (#81284) * Include esphome device name in BLE logs This makes it easier to debug what is going on when there are multiple esphome proxies * revert unintended change --- homeassistant/components/esphome/__init__.py | 2 + .../components/esphome/bluetooth/__init__.py | 8 +-- .../components/esphome/bluetooth/client.py | 53 +++++++++++++++---- .../components/esphome/entry_data.py | 17 ++++-- 4 files changed, 65 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index a5428f7d6c5..23b6a6550e4 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -249,6 +249,8 @@ async def async_setup_entry( # noqa: C901 async def on_disconnect() -> None: """Run disconnect callbacks on API disconnect.""" + name = entry_data.device_info.name if entry_data.device_info else host + _LOGGER.debug("%s: %s disconnected, running disconnected callbacks", name, host) for disconnect_cb in entry_data.disconnect_callbacks: disconnect_cb() entry_data.disconnect_callbacks = [] diff --git a/homeassistant/components/esphome/bluetooth/__init__.py b/homeassistant/components/esphome/bluetooth/__init__.py index b4d5fdbd04d..b5be5362474 100644 --- a/homeassistant/components/esphome/bluetooth/__init__.py +++ b/homeassistant/components/esphome/bluetooth/__init__.py @@ -30,13 +30,15 @@ def _async_can_connect_factory( @hass_callback def _async_can_connect() -> bool: """Check if a given source can make another connection.""" + can_connect = bool(entry_data.available and entry_data.ble_connections_free) _LOGGER.debug( - "Checking if %s can connect, available=%s, ble_connections_free=%s", + "%s: Checking can connect, available=%s, ble_connections_free=%s result=%s", source, entry_data.available, entry_data.ble_connections_free, + can_connect, ) - return bool(entry_data.available and entry_data.ble_connections_free) + return can_connect return _async_can_connect @@ -55,7 +57,7 @@ async def async_connect_scanner( version = entry_data.device_info.bluetooth_proxy_version connectable = version >= 2 _LOGGER.debug( - "Connecting scanner for %s, version=%s, connectable=%s", + "%s: Connecting scanner version=%s, connectable=%s", source, version, connectable, diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 68f1788afdb..5f20a73f4d6 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -61,7 +61,7 @@ def verify_connected(func: _WrapFuncType) -> _WrapFuncType: if disconnected_event.is_set(): task.cancel() raise BleakError( - f"{self._ble_device.name} ({self._ble_device.address}): " # pylint: disable=protected-access + f"{self._source}: {self._ble_device.name} - {self._ble_device.address}: " # pylint: disable=protected-access "Disconnected during operation" ) return next(iter(done)).result() @@ -120,7 +120,10 @@ class ESPHomeClient(BaseBleakClient): self._cancel_connection_state() except (AssertionError, ValueError) as ex: _LOGGER.debug( - "Failed to unsubscribe from connection state (likely connection dropped): %s", + "%s: %s - %s: Failed to unsubscribe from connection state (likely connection dropped): %s", + self._source, + self._ble_device.name, + self._ble_device.address, ex, ) self._cancel_connection_state = None @@ -134,13 +137,23 @@ class ESPHomeClient(BaseBleakClient): self._disconnected_event.set() self._disconnected_event = None if was_connected: - _LOGGER.debug("%s: BLE device disconnected", self._source) + _LOGGER.debug( + "%s: %s - %s: BLE device disconnected", + self._source, + self._ble_device.name, + self._ble_device.address, + ) self._async_call_bleak_disconnected_callback() self._unsubscribe_connection_state() def _async_esp_disconnected(self) -> None: """Handle the esp32 client disconnecting from hass.""" - _LOGGER.debug("%s: ESP device disconnected", self._source) + _LOGGER.debug( + "%s: %s - %s: ESP device disconnected", + self._source, + self._ble_device.name, + self._ble_device.address, + ) self.entry_data.disconnect_callbacks.remove(self._async_esp_disconnected) self._async_ble_device_disconnected() @@ -170,7 +183,10 @@ class ESPHomeClient(BaseBleakClient): ) -> None: """Handle a connect or disconnect.""" _LOGGER.debug( - "Connection state changed: connected=%s mtu=%s error=%s", + "%s: %s - %s: Connection state changed to connected=%s mtu=%s error=%s", + self._source, + self._ble_device.name, + self._ble_device.address, connected, mtu, error, @@ -203,6 +219,12 @@ class ESPHomeClient(BaseBleakClient): connected_future.set_exception(BleakError("Disconnected")) return + _LOGGER.debug( + "%s: %s - %s: connected, registering for disconnected callbacks", + self._source, + self._ble_device.name, + self._ble_device.address, + ) self.entry_data.disconnect_callbacks.append(self._async_esp_disconnected) connected_future.set_result(connected) @@ -230,7 +252,10 @@ class ESPHomeClient(BaseBleakClient): if self.entry_data.ble_connections_free: return _LOGGER.debug( - "%s: Out of connection slots, waiting for a free one", self._source + "%s: %s - %s: Out of connection slots, waiting for a free one", + self._source, + self._ble_device.name, + self._ble_device.address, ) async with async_timeout.timeout(timeout): await self.entry_data.wait_for_ble_connections_free() @@ -272,20 +297,29 @@ class ESPHomeClient(BaseBleakClient): cached_services := entry_data.get_gatt_services_cache(address_as_int) ): _LOGGER.debug( - "Cached services hit for %s - %s", + "%s: %s - %s: Cached services hit", + self._source, self._ble_device.name, self._ble_device.address, ) self.services = cached_services return self.services _LOGGER.debug( - "Cached services miss for %s - %s", + "%s: %s - %s: Cached services miss", + self._source, self._ble_device.name, self._ble_device.address, ) esphome_services = await self._client.bluetooth_gatt_get_services( address_as_int ) + _LOGGER.debug( + "%s: %s - %s: Got services: %s", + self._source, + self._ble_device.name, + self._ble_device.address, + esphome_services, + ) max_write_without_response = self.mtu_size - GATT_HEADER_SIZE services = BleakGATTServiceCollection() # type: ignore[no-untyped-call] for service in esphome_services.services: @@ -309,7 +343,8 @@ class ESPHomeClient(BaseBleakClient): ) self.services = services _LOGGER.debug( - "Cached services saved for %s - %s", + "%s: %s - %s: Cached services saved", + self._source, self._ble_device.name, self._ble_device.address, ) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 5d474b0fb15..faa9074b880 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -99,6 +99,11 @@ class RuntimeEntryData: default_factory=lambda: LRU(MAX_CACHED_SERVICES) # type: ignore[no-any-return] ) + @property + def name(self) -> str: + """Return the name of the device.""" + return self.device_info.name if self.device_info else self.entry_id + def get_gatt_services_cache( self, address: int ) -> BleakGATTServiceCollection | None: @@ -114,8 +119,13 @@ class RuntimeEntryData: @callback def async_update_ble_connection_limits(self, free: int, limit: int) -> None: """Update the BLE connection limits.""" - name = self.device_info.name if self.device_info else self.entry_id - _LOGGER.debug("%s: BLE connection limits: %s/%s", name, free, limit) + _LOGGER.debug( + "%s: BLE connection limits: used=%s free=%s limit=%s", + self.name, + limit - free, + free, + limit, + ) self.ble_connections_free = free self.ble_connections_limit = limit if free: @@ -186,7 +196,8 @@ class RuntimeEntryData: subscription_key = (type(state), state.key) self.state[type(state)][state.key] = state _LOGGER.debug( - "Dispatching update with key %s: %s", + "%s: dispatching update with key %s: %s", + self.name, subscription_key, state, ) From 13562d271e664668c7372c8e082e8b2132a64222 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 00:28:38 -0500 Subject: [PATCH 940/985] Bump bleak-retry-connector to 2.8.1 (#81285) * Bump bleak-retry-connector to 2.8.1 reduces logging now that we have found the problem with esphome devices not disconnecting ble devices after timeout changelog: https://github.com/Bluetooth-Devices/bleak-retry-connector/compare/v2.8.0...v2.8.1 * empty --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 660345606c8..091962fbc83 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.1", - "bleak-retry-connector==2.8.0", + "bleak-retry-connector==2.8.1", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.59.1" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 994a8d44019..914731a8164 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.8.0 +bleak-retry-connector==2.8.1 bleak==0.19.1 bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index e35cdbeee77..332e98af7ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.0 +bleak-retry-connector==2.8.1 # homeassistant.components.bluetooth bleak==0.19.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 23f1b6122d8..d7d7692aa35 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,7 +337,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.0 +bleak-retry-connector==2.8.1 # homeassistant.components.bluetooth bleak==0.19.1 From 8f843b3046ee9d381fd7cf904af50cf7f51aca81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 22:10:30 -0500 Subject: [PATCH 941/985] Do not fire the esphome ble disconnected callback if we were not connected (#81286) From 8eef55ed60d3ac945e51a282f0d02a22759f8a52 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Mon, 31 Oct 2022 03:23:05 -0500 Subject: [PATCH 942/985] Bump pyipp to 0.12.1 (#81287) bump pyipp to 0.12.1 --- homeassistant/components/ipp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index aadfdc8feea..b673a2d5a6d 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -3,7 +3,7 @@ "name": "Internet Printing Protocol (IPP)", "documentation": "https://www.home-assistant.io/integrations/ipp", "integration_type": "device", - "requirements": ["pyipp==0.12.0"], + "requirements": ["pyipp==0.12.1"], "codeowners": ["@ctalkington"], "config_flow": true, "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 332e98af7ed..8e21a9ad125 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1631,7 +1631,7 @@ pyintesishome==1.8.0 pyipma==3.0.5 # homeassistant.components.ipp -pyipp==0.12.0 +pyipp==0.12.1 # homeassistant.components.iqvia pyiqvia==2022.04.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d7d7692aa35..4c909787a9f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1147,7 +1147,7 @@ pyinsteon==1.2.0 pyipma==3.0.5 # homeassistant.components.ipp -pyipp==0.12.0 +pyipp==0.12.1 # homeassistant.components.iqvia pyiqvia==2022.04.0 From 4fbbb7ba6dfd7bdecd662e47977bb7abe92f25ac Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 31 Oct 2022 11:09:15 +0100 Subject: [PATCH 943/985] Bump pyatmo to 7.3.0 (#81290) * Bump pyatmo to 7.3.0 * Update test fixture data and tests --- .../components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../netatmo/fixtures/getstationsdata.json | 222 ++++-- .../netatmo/fixtures/homesdata.json | 252 +++---- .../homestatus_91763b24c43d3e344f424e8b.json | 676 ++---------------- .../homestatus_91763b24c43d3e344f424e8c.json | 18 +- tests/components/netatmo/test_camera.py | 21 +- tests/components/netatmo/test_climate.py | 18 +- tests/components/netatmo/test_light.py | 12 +- tests/components/netatmo/test_sensor.py | 34 +- 11 files changed, 381 insertions(+), 878 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 5ad0fca3d7a..e34156ff589 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -2,7 +2,7 @@ "domain": "netatmo", "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==7.2.0"], + "requirements": ["pyatmo==7.3.0"], "after_dependencies": ["cloud", "media_source"], "dependencies": ["application_credentials", "webhook"], "codeowners": ["@cgtobi"], diff --git a/requirements_all.txt b/requirements_all.txt index 8e21a9ad125..35856c010b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1442,7 +1442,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.2.0 +pyatmo==7.3.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c909787a9f..25febf43e63 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1030,7 +1030,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.2.0 +pyatmo==7.3.0 # homeassistant.components.apple_tv pyatv==0.10.3 diff --git a/tests/components/netatmo/fixtures/getstationsdata.json b/tests/components/netatmo/fixtures/getstationsdata.json index 822a4c11a50..10c3ca85e06 100644 --- a/tests/components/netatmo/fixtures/getstationsdata.json +++ b/tests/components/netatmo/fixtures/getstationsdata.json @@ -114,7 +114,7 @@ "battery_percent": 79 }, { - "_id": "12:34:56:03:1b:e4", + "_id": "12:34:56:03:1b:e5", "type": "NAModule2", "module_name": "Garden", "data_type": ["Wind"], @@ -430,63 +430,203 @@ "modules": [] }, { - "_id": "12:34:56:58:c8:54", - "date_setup": 1605594014, - "last_setup": 1605594014, + "_id": "12:34:56:80:bb:26", + "station_name": "MYHOME (Palier)", + "date_setup": 1558709904, + "last_setup": 1558709904, "type": "NAMain", - "last_status_store": 1605878352, - "firmware": 178, - "wifi_status": 47, + "last_status_store": 1644582700, + "module_name": "Palier", + "firmware": 181, + "last_upgrade": 1558709906, + "wifi_status": 57, "reachable": true, "co2_calibrating": false, "data_type": ["Temperature", "CO2", "Humidity", "Noise", "Pressure"], "place": { - "altitude": 65, - "city": "Njurunda District", - "country": "SE", - "timezone": "Europe/Stockholm", - "location": [17.123456, 62.123456] + "altitude": 329, + "city": "Someplace", + "country": "FR", + "timezone": "Europe/Paris", + "location": [6.1234567, 46.123456] }, - "station_name": "Njurunda (Indoor)", - "home_id": "5fb36b9ec68fd10c6467ca65", - "home_name": "Njurunda", + "home_id": "91763b24c43d3e344f424e8b", + "home_name": "MYHOME", "dashboard_data": { - "time_utc": 1605878349, - "Temperature": 19.7, - "CO2": 993, - "Humidity": 40, - "Noise": 40, - "Pressure": 1015.6, - "AbsolutePressure": 1007.8, - "min_temp": 19.7, - "max_temp": 20.4, - "date_max_temp": 1605826917, - "date_min_temp": 1605873207, + "time_utc": 1644582694, + "Temperature": 21.1, + "CO2": 1339, + "Humidity": 45, + "Noise": 35, + "Pressure": 1026.8, + "AbsolutePressure": 974.5, + "min_temp": 21, + "max_temp": 21.8, + "date_max_temp": 1644534255, + "date_min_temp": 1644550420, "temp_trend": "stable", "pressure_trend": "up" }, "modules": [ { - "_id": "12:34:56:58:e6:38", + "_id": "12:34:56:80:1c:42", "type": "NAModule1", - "last_setup": 1605594034, + "module_name": "Outdoor", + "last_setup": 1558709954, "data_type": ["Temperature", "Humidity"], - "battery_percent": 100, + "battery_percent": 27, "reachable": true, "firmware": 50, - "last_message": 1605878347, - "last_seen": 1605878328, - "rf_status": 62, - "battery_vp": 6198, + "last_message": 1644582699, + "last_seen": 1644582699, + "rf_status": 68, + "battery_vp": 4678, "dashboard_data": { - "time_utc": 1605878328, - "Temperature": 0.6, - "Humidity": 77, - "min_temp": -2.1, - "max_temp": 1.5, - "date_max_temp": 1605865920, - "date_min_temp": 1605826904, - "temp_trend": "down" + "time_utc": 1644582648, + "Temperature": 9.4, + "Humidity": 57, + "min_temp": 6.7, + "max_temp": 9.8, + "date_max_temp": 1644534223, + "date_min_temp": 1644569369, + "temp_trend": "up" + } + }, + { + "_id": "12:34:56:80:c1:ea", + "type": "NAModule3", + "module_name": "Rain", + "last_setup": 1563734531, + "data_type": ["Rain"], + "battery_percent": 21, + "reachable": true, + "firmware": 12, + "last_message": 1644582699, + "last_seen": 1644582699, + "rf_status": 79, + "battery_vp": 4256, + "dashboard_data": { + "time_utc": 1644582686, + "Rain": 3.7, + "sum_rain_1": 0, + "sum_rain_24": 6.9 + } + }, + { + "_id": "12:34:56:80:44:92", + "type": "NAModule4", + "module_name": "Bedroom", + "last_setup": 1575915890, + "data_type": ["Temperature", "CO2", "Humidity"], + "battery_percent": 28, + "reachable": true, + "firmware": 51, + "last_message": 1644582699, + "last_seen": 1644582654, + "rf_status": 67, + "battery_vp": 4695, + "dashboard_data": { + "time_utc": 1644582654, + "Temperature": 19.3, + "CO2": 1076, + "Humidity": 53, + "min_temp": 19.2, + "max_temp": 19.7, + "date_max_temp": 1644534243, + "date_min_temp": 1644553418, + "temp_trend": "stable" + } + }, + { + "_id": "12:34:56:80:7e:18", + "type": "NAModule4", + "module_name": "Bathroom", + "last_setup": 1575915955, + "data_type": ["Temperature", "CO2", "Humidity"], + "battery_percent": 55, + "reachable": true, + "firmware": 51, + "last_message": 1644582699, + "last_seen": 1644582654, + "rf_status": 59, + "battery_vp": 5184, + "dashboard_data": { + "time_utc": 1644582654, + "Temperature": 19.4, + "CO2": 1930, + "Humidity": 55, + "min_temp": 19.4, + "max_temp": 21.8, + "date_max_temp": 1644534224, + "date_min_temp": 1644582039, + "temp_trend": "stable" + } + }, + { + "_id": "12:34:56:03:1b:e4", + "type": "NAModule2", + "module_name": "Garden", + "data_type": ["Wind"], + "last_setup": 1549193862, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413170, + "WindStrength": 4, + "WindAngle": 217, + "GustStrength": 9, + "GustAngle": 206, + "max_wind_str": 21, + "max_wind_angle": 217, + "date_max_wind_str": 1559386669 + }, + "firmware": 19, + "last_message": 1559413177, + "last_seen": 1559413177, + "rf_status": 59, + "battery_vp": 5689, + "battery_percent": 85 + } + ] + }, + { + "_id": "00:11:22:2c:be:c8", + "station_name": "Zuhause (Kinderzimmer)", + "type": "NAMain", + "last_status_store": 1649146022, + "reachable": true, + "favorite": true, + "data_type": ["Pressure"], + "place": { + "altitude": 127, + "city": "Wiesbaden", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [8.238054275512695, 50.07585525512695] + }, + "read_only": true, + "dashboard_data": { + "time_utc": 1649146022, + "Pressure": 1015.6, + "AbsolutePressure": 1000.4, + "pressure_trend": "stable" + }, + "modules": [ + { + "_id": "00:11:22:2c:ce:b6", + "type": "NAModule1", + "data_type": ["Temperature", "Humidity"], + "reachable": true, + "last_message": 1649146022, + "last_seen": 1649145996, + "dashboard_data": { + "time_utc": 1649145996, + "Temperature": 7.8, + "Humidity": 87, + "min_temp": 6.5, + "max_temp": 7.8, + "date_max_temp": 1649145996, + "date_min_temp": 1649118465, + "temp_trend": "up" } } ] diff --git a/tests/components/netatmo/fixtures/homesdata.json b/tests/components/netatmo/fixtures/homesdata.json index 93c04388f4c..6b24a7f8f9d 100644 --- a/tests/components/netatmo/fixtures/homesdata.json +++ b/tests/components/netatmo/fixtures/homesdata.json @@ -23,7 +23,6 @@ "12:34:56:00:f1:62", "12:34:56:10:f1:66", "12:34:56:00:e3:9b", - "12:34:56:00:86:99", "0009999992" ] }, @@ -39,12 +38,6 @@ "type": "kitchen", "module_ids": ["12:34:56:03:a0:ac"] }, - { - "id": "2940411588", - "name": "Child", - "type": "custom", - "module_ids": ["12:34:56:26:cc:01"] - }, { "id": "222452125", "name": "Bureau", @@ -76,6 +69,12 @@ "name": "Corridor", "type": "corridor", "module_ids": ["10:20:30:bd:b8:1e"] + }, + { + "id": "100007520", + "name": "Toilettes", + "type": "toilets", + "module_ids": ["00:11:22:33:00:11:45:fe"] } ], "modules": [ @@ -120,15 +119,29 @@ "name": "Hall", "setup_date": 1544828430, "room_id": "3688132631", - "reachable": true, "modules_bridged": ["12:34:56:00:86:99", "12:34:56:00:e3:9b"] }, { - "id": "12:34:56:00:a5:a4", + "id": "12:34:56:10:f1:66", + "type": "NDB", + "name": "Netatmo-Doorbell", + "setup_date": 1602691361, + "room_id": "3688132631", + "reachable": true, + "hk_device_id": "123456007df1", + "customer_id": "1000010", + "network_lock": false, + "quick_display_zone": 62 + }, + { + "id": "12:34:56:10:b9:0e", "type": "NOC", - "name": "Garden", - "setup_date": 1544828430, - "reachable": true + "name": "Front", + "setup_date": 1509290599, + "reachable": true, + "customer_id": "A00010", + "network_lock": false, + "use_pincode": false }, { "id": "12:34:56:20:f5:44", @@ -155,33 +168,6 @@ "room_id": "222452125", "bridge": "12:34:56:20:f5:44" }, - { - "id": "12:34:56:10:f1:66", - "type": "NDB", - "name": "Netatmo-Doorbell", - "setup_date": 1602691361, - "room_id": "3688132631", - "reachable": true, - "hk_device_id": "123456007df1", - "customer_id": "1000010", - "network_lock": false, - "quick_display_zone": 62 - }, - { - "id": "12:34:56:00:e3:9b", - "type": "NIS", - "setup_date": 1620479901, - "bridge": "12:34:56:00:f1:62", - "name": "Sirene in hall" - }, - { - "id": "12:34:56:00:86:99", - "type": "NACamDoorTag", - "name": "Window Hall", - "setup_date": 1581177375, - "bridge": "12:34:56:00:f1:62", - "category": "window" - }, { "id": "12:34:56:30:d5:d4", "type": "NBG", @@ -199,16 +185,17 @@ "bridge": "12:34:56:30:d5:d4" }, { - "id": "12:34:56:37:11:ca", + "id": "12:34:56:80:bb:26", "type": "NAMain", - "name": "NetatmoIndoor", + "name": "Villa", "setup_date": 1419453350, + "room_id": "4122897288", "reachable": true, "modules_bridged": [ - "12:34:56:07:bb:3e", - "12:34:56:03:1b:e4", - "12:34:56:36:fc:de", - "12:34:56:05:51:20" + "12:34:56:80:44:92", + "12:34:56:80:7e:18", + "12:34:56:80:1c:42", + "12:34:56:80:c1:ea" ], "customer_id": "C00016", "hardware_version": 251, @@ -271,48 +258,46 @@ "module_offset": { "12:34:56:80:bb:26": { "a": 0.1 + }, + "03:00:00:03:1b:0e": { + "a": 0 } } }, { - "id": "12:34:56:36:fc:de", + "id": "12:34:56:80:1c:42", "type": "NAModule1", "name": "Outdoor", "setup_date": 1448565785, - "bridge": "12:34:56:37:11:ca" + "bridge": "12:34:56:80:bb:26" + }, + { + "id": "12:34:56:80:c1:ea", + "type": "NAModule3", + "name": "Rain", + "setup_date": 1591770206, + "bridge": "12:34:56:80:bb:26" + }, + { + "id": "12:34:56:80:44:92", + "type": "NAModule4", + "name": "Bedroom", + "setup_date": 1484997703, + "bridge": "12:34:56:80:bb:26" + }, + { + "id": "12:34:56:80:7e:18", + "type": "NAModule4", + "name": "Bathroom", + "setup_date": 1543579864, + "bridge": "12:34:56:80:bb:26" }, { "id": "12:34:56:03:1b:e4", "type": "NAModule2", "name": "Garden", "setup_date": 1543579864, - "bridge": "12:34:56:37:11:ca" - }, - { - "id": "12:34:56:05:51:20", - "type": "NAModule3", - "name": "Rain", - "setup_date": 1591770206, - "bridge": "12:34:56:37:11:ca" - }, - { - "id": "12:34:56:07:bb:3e", - "type": "NAModule4", - "name": "Bedroom", - "setup_date": 1484997703, - "bridge": "12:34:56:37:11:ca" - }, - { - "id": "12:34:56:26:68:92", - "type": "NHC", - "name": "Indoor", - "setup_date": 1571342643 - }, - { - "id": "12:34:56:26:cc:01", - "type": "BNS", - "name": "Child", - "setup_date": 1571634243 + "bridge": "12:34:56:80:bb:26" }, { "id": "12:34:56:80:60:40", @@ -324,7 +309,8 @@ "12:34:56:80:00:12:ac:f2", "12:34:56:80:00:c3:69:3c", "12:34:56:00:00:a1:4c:da", - "12:34:56:00:01:01:01:a1" + "12:34:56:00:01:01:01:a1", + "00:11:22:33:00:11:45:fe" ] }, { @@ -342,6 +328,21 @@ "setup_date": 1641841262, "bridge": "12:34:56:80:60:40" }, + { + "id": "12:34:56:00:86:99", + "type": "NACamDoorTag", + "name": "Window Hall", + "setup_date": 1581177375, + "bridge": "12:34:56:00:f1:62", + "category": "window" + }, + { + "id": "12:34:56:00:e3:9b", + "type": "NIS", + "setup_date": 1620479901, + "bridge": "12:34:56:00:f1:62", + "name": "Sirene in hall" + }, { "id": "12:34:56:00:16:0e", "type": "NLE", @@ -440,6 +441,24 @@ "room_id": "100008999", "bridge": "12:34:56:80:60:40" }, + { + "id": "10:20:30:bd:b8:1e", + "type": "BNS", + "name": "Smarther", + "setup_date": 1638022197, + "room_id": "1002003001" + }, + { + "id": "00:11:22:33:00:11:45:fe", + "type": "NLF", + "on": false, + "brightness": 63, + "firmware_revision": 57, + "last_seen": 1657086939, + "power": 0, + "reachable": true, + "bridge": "12:34:56:80:60:40" + }, { "id": "12:34:56:00:01:01:01:a1", "type": "NLFN", @@ -761,80 +780,13 @@ "therm_mode": "schedule" }, { - "id": "111111111111111111111401", - "name": "Home with no modules", - "altitude": 9, - "coordinates": [1.23456789, 50.0987654], - "country": "BE", - "timezone": "Europe/Brussels", - "rooms": [ - { - "id": "1111111401", - "name": "Livingroom", - "type": "livingroom" - } - ], - "temperature_control_mode": "heating", - "therm_mode": "away", - "therm_setpoint_default_duration": 120, - "cooling_mode": "schedule", - "schedules": [ - { - "away_temp": 14, - "hg_temp": 7, - "name": "Week", - "timetable": [ - { - "zone_id": 1, - "m_offset": 0 - }, - { - "zone_id": 6, - "m_offset": 420 - } - ], - "zones": [ - { - "type": 0, - "name": "Comfort", - "rooms_temp": [], - "id": 0, - "rooms": [] - }, - { - "type": 1, - "name": "Nacht", - "rooms_temp": [], - "id": 1, - "rooms": [] - }, - { - "type": 5, - "name": "Eco", - "rooms_temp": [], - "id": 4, - "rooms": [] - }, - { - "type": 4, - "name": "Tussenin", - "rooms_temp": [], - "id": 5, - "rooms": [] - }, - { - "type": 4, - "name": "Ochtend", - "rooms_temp": [], - "id": 6, - "rooms": [] - } - ], - "id": "700000000000000000000401", - "selected": true, - "type": "therm" - } - ] + "id": "91763b24c43d3e344f424e8c", + "altitude": 112, + "coordinates": [52.516263, 13.377726], + "country": "DE", + "timezone": "Europe/Berlin", + "therm_setpoint_default_duration": 180, + "therm_mode": "schedule" } ], "user": { @@ -845,6 +797,8 @@ "unit_pressure": 0, "unit_system": 0, "unit_wind": 0, + "all_linked": false, + "type": "netatmo", "id": "91763b24c43d3e344f424e8b" } }, diff --git a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json index 4cd5dceec3b..736d70be11c 100644 --- a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json +++ b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json @@ -14,25 +14,6 @@ "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.123.45/609e27de5699fb18147ab47d06846631/MTRPn_BeWCav5RBq4U1OMDruTW4dkQ0NuMwNDAw11g,,", "is_local": true }, - { - "type": "NOC", - "firmware_revision": 3002000, - "monitoring": "on", - "sd_status": 4, - "connection": "wifi", - "homekit_status": "upgradable", - "floodlight": "auto", - "timelapse_available": true, - "id": "12:34:56:00:a5:a4", - "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.41/333333333333/444444444444,,", - "is_local": false, - "network_lock": false, - "firmware_name": "3.2.0", - "wifi_strength": 62, - "alim_status": 2, - "locked": false, - "wifi_state": "high" - }, { "id": "12:34:56:00:fa:d0", "type": "NAPlug", @@ -46,6 +27,7 @@ "type": "NATherm1", "firmware_revision": 65, "rf_strength": 58, + "battery_level": 3793, "boiler_valve_comfort_boost": false, "boiler_status": false, "anticipating": false, @@ -58,6 +40,7 @@ "type": "NRV", "firmware_revision": 79, "rf_strength": 51, + "battery_level": 3025, "bridge": "12:34:56:00:fa:d0", "battery_state": "full" }, @@ -67,18 +50,10 @@ "type": "NRV", "firmware_revision": 79, "rf_strength": 59, + "battery_level": 3029, "bridge": "12:34:56:00:fa:d0", "battery_state": "full" }, - { - "id": "12:34:56:26:cc:01", - "type": "BNS", - "firmware_revision": 32, - "wifi_strength": 50, - "boiler_valve_comfort_boost": false, - "boiler_status": true, - "cooler_status": false - }, { "type": "NDB", "last_ftp_event": { @@ -100,6 +75,25 @@ "wifi_strength": 66, "wifi_state": "medium" }, + { + "type": "NOC", + "firmware_revision": 3002000, + "monitoring": "on", + "sd_status": 4, + "connection": "wifi", + "homekit_status": "upgradable", + "floodlight": "auto", + "timelapse_available": true, + "id": "12:34:56:10:b9:0e", + "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.41/333333333333/444444444444,,", + "is_local": false, + "network_lock": false, + "firmware_name": "3.2.0", + "wifi_strength": 62, + "alim_status": 2, + "locked": false, + "wifi_state": "high" + }, { "boiler_control": "onoff", "dhw_control": "none", @@ -264,609 +258,23 @@ "bridge": "12:34:56:80:60:40" }, { - "id": "12:34:56:00:01:01:01:a1", - "brightness": 100, - "firmware_revision": 52, - "last_seen": 1604940167, - "on": false, - "power": 0, - "reachable": true, - "type": "NLFN", - "bridge": "12:34:56:80:60:40" - }, - { - "type": "NDB", - "last_ftp_event": { - "type": 3, - "time": 1631444443, - "id": 3 - }, - "id": "12:34:56:10:f1:66", - "websocket_connected": true, - "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.40/1111111111111/2222222222222,,", - "is_local": false, - "alim_status": 2, - "connection": "wifi", - "firmware_name": "2.18.0", - "firmware_revision": 2018000, - "homekit_status": "configured", - "max_peers_reached": false, - "sd_status": 4, - "wifi_strength": 66, - "wifi_state": "medium" - }, - { - "boiler_control": "onoff", - "dhw_control": "none", - "firmware_revision": 22, - "hardware_version": 222, - "id": "12:34:56:20:f5:44", - "outdoor_temperature": 8.2, - "sequence_id": 19764, - "type": "OTH", - "wifi_strength": 57 - }, - { - "battery_level": 4176, - "boiler_status": false, + "id": "10:20:30:bd:b8:1e", + "type": "BNS", + "firmware_revision": 32, + "wifi_strength": 49, "boiler_valve_comfort_boost": false, - "firmware_revision": 6, - "id": "12:34:56:20:f5:8c", - "last_message": 1637684297, - "last_seen": 1637684297, - "radio_id": 2, - "reachable": true, - "rf_strength": 64, - "type": "OTM", - "bridge": "12:34:56:20:f5:44", - "battery_state": "full" + "boiler_status": true, + "cooler_status": false }, { - "id": "12:34:56:30:d5:d4", - "type": "NBG", - "firmware_revision": 39, - "wifi_strength": 65, - "reachable": true - }, - { - "id": "0009999992", - "type": "NBR", - "current_position": 0, - "target_position": 0, - "target_position_step": 100, - "firmware_revision": 16, - "rf_strength": 0, - "last_seen": 1638353156, - "reachable": true, - "therm_measured_temperature": 5, - "heating_power_request": 1, - "therm_setpoint_temperature": 7, - "therm_setpoint_mode": "away", - "therm_setpoint_start_time": 0, - "therm_setpoint_end_time": 0, - "anticipating": false, - "open_window": false - }, - { - "id": "12:34:56:00:86:99", - "type": "NACamDoorTag", - "battery_state": "high", - "battery_level": 5240, - "firmware_revision": 58, - "rf_state": "full", - "rf_strength": 58, - "last_seen": 1642698124, - "last_activity": 1627757310, - "reachable": false, - "bridge": "12:34:56:00:f1:62", - "status": "no_news" - }, - { - "id": "12:34:56:00:e3:9b", - "type": "NIS", - "battery_state": "low", - "battery_level": 5438, - "firmware_revision": 209, - "rf_state": "medium", - "rf_strength": 62, - "last_seen": 1644569790, - "reachable": true, - "bridge": "12:34:56:00:f1:62", - "status": "no_sound", - "monitoring": "off" - }, - { - "id": "12:34:56:80:60:40", - "type": "NLG", - "offload": false, - "firmware_revision": 211, - "last_seen": 1644567372, - "wifi_strength": 51, - "reachable": true - }, - { - "id": "12:34:56:80:00:12:ac:f2", - "type": "NLP", - "on": true, - "offload": false, - "firmware_revision": 62, - "last_seen": 1644569425, - "power": 0, - "reachable": true, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:80:00:c3:69:3c", - "type": "NLT", - "battery_state": "full", - "battery_level": 3300, - "firmware_revision": 42, - "last_seen": 0, - "reachable": false, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:00:16:0e", - "type": "NLE", - "firmware_revision": 14, - "wifi_strength": 38 - }, - { - "id": "12:34:56:00:16:0e#0", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#1", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#2", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#3", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#4", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#5", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#6", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#7", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#8", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:00:a1:4c:da", - "type": "NLPC", - "firmware_revision": 62, - "last_seen": 1646511241, - "power": 476, - "reachable": true, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:00:01:01:01:a1", - "brightness": 100, - "firmware_revision": 52, - "last_seen": 1604940167, + "id": "00:11:22:33:00:11:45:fe", + "type": "NLF", "on": false, + "brightness": 63, + "firmware_revision": 57, + "last_seen": 1657086939, "power": 0, "reachable": true, - "type": "NLFN", - "bridge": "12:34:56:80:60:40" - }, - { - "type": "NDB", - "last_ftp_event": { - "type": 3, - "time": 1631444443, - "id": 3 - }, - "id": "12:34:56:10:f1:66", - "websocket_connected": true, - "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.40/1111111111111/2222222222222,,", - "is_local": false, - "alim_status": 2, - "connection": "wifi", - "firmware_name": "2.18.0", - "firmware_revision": 2018000, - "homekit_status": "configured", - "max_peers_reached": false, - "sd_status": 4, - "wifi_strength": 66, - "wifi_state": "medium" - }, - { - "boiler_control": "onoff", - "dhw_control": "none", - "firmware_revision": 22, - "hardware_version": 222, - "id": "12:34:56:20:f5:44", - "outdoor_temperature": 8.2, - "sequence_id": 19764, - "type": "OTH", - "wifi_strength": 57 - }, - { - "battery_level": 4176, - "boiler_status": false, - "boiler_valve_comfort_boost": false, - "firmware_revision": 6, - "id": "12:34:56:20:f5:8c", - "last_message": 1637684297, - "last_seen": 1637684297, - "radio_id": 2, - "reachable": true, - "rf_strength": 64, - "type": "OTM", - "bridge": "12:34:56:20:f5:44", - "battery_state": "full" - }, - { - "id": "12:34:56:30:d5:d4", - "type": "NBG", - "firmware_revision": 39, - "wifi_strength": 65, - "reachable": true - }, - { - "id": "0009999992", - "type": "NBR", - "current_position": 0, - "target_position": 0, - "target_position_step": 100, - "firmware_revision": 16, - "rf_strength": 0, - "last_seen": 1638353156, - "reachable": true, - "therm_measured_temperature": 5, - "heating_power_request": 1, - "therm_setpoint_temperature": 7, - "therm_setpoint_mode": "away", - "therm_setpoint_start_time": 0, - "therm_setpoint_end_time": 0, - "anticipating": false, - "open_window": false - }, - { - "id": "12:34:56:00:86:99", - "type": "NACamDoorTag", - "battery_state": "high", - "battery_level": 5240, - "firmware_revision": 58, - "rf_state": "full", - "rf_strength": 58, - "last_seen": 1642698124, - "last_activity": 1627757310, - "reachable": false, - "bridge": "12:34:56:00:f1:62", - "status": "no_news" - }, - { - "id": "12:34:56:00:e3:9b", - "type": "NIS", - "battery_state": "low", - "battery_level": 5438, - "firmware_revision": 209, - "rf_state": "medium", - "rf_strength": 62, - "last_seen": 1644569790, - "reachable": true, - "bridge": "12:34:56:00:f1:62", - "status": "no_sound", - "monitoring": "off" - }, - { - "id": "12:34:56:80:60:40", - "type": "NLG", - "offload": false, - "firmware_revision": 211, - "last_seen": 1644567372, - "wifi_strength": 51, - "reachable": true - }, - { - "id": "12:34:56:80:00:12:ac:f2", - "type": "NLP", - "on": true, - "offload": false, - "firmware_revision": 62, - "last_seen": 1644569425, - "power": 0, - "reachable": true, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:80:00:c3:69:3c", - "type": "NLT", - "battery_state": "full", - "battery_level": 3300, - "firmware_revision": 42, - "last_seen": 0, - "reachable": false, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:00:16:0e", - "type": "NLE", - "firmware_revision": 14, - "wifi_strength": 38 - }, - { - "id": "12:34:56:00:16:0e#0", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#1", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#2", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#3", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#4", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#5", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#6", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#7", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#8", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:00:a1:4c:da", - "type": "NLPC", - "firmware_revision": 62, - "last_seen": 1646511241, - "power": 476, - "reachable": true, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:00:01:01:01:a1", - "brightness": 100, - "firmware_revision": 52, - "last_seen": 1604940167, - "on": false, - "power": 0, - "reachable": true, - "type": "NLFN", - "bridge": "12:34:56:80:60:40" - }, - { - "type": "NDB", - "last_ftp_event": { - "type": 3, - "time": 1631444443, - "id": 3 - }, - "id": "12:34:56:10:f1:66", - "websocket_connected": true, - "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.40/1111111111111/2222222222222,,", - "is_local": false, - "alim_status": 2, - "connection": "wifi", - "firmware_name": "2.18.0", - "firmware_revision": 2018000, - "homekit_status": "configured", - "max_peers_reached": false, - "sd_status": 4, - "wifi_strength": 66, - "wifi_state": "medium" - }, - { - "boiler_control": "onoff", - "dhw_control": "none", - "firmware_revision": 22, - "hardware_version": 222, - "id": "12:34:56:20:f5:44", - "outdoor_temperature": 8.2, - "sequence_id": 19764, - "type": "OTH", - "wifi_strength": 57 - }, - { - "battery_level": 4176, - "boiler_status": false, - "boiler_valve_comfort_boost": false, - "firmware_revision": 6, - "id": "12:34:56:20:f5:8c", - "last_message": 1637684297, - "last_seen": 1637684297, - "radio_id": 2, - "reachable": true, - "rf_strength": 64, - "type": "OTM", - "bridge": "12:34:56:20:f5:44", - "battery_state": "full" - }, - { - "id": "12:34:56:30:d5:d4", - "type": "NBG", - "firmware_revision": 39, - "wifi_strength": 65, - "reachable": true - }, - { - "id": "0009999992", - "type": "NBR", - "current_position": 0, - "target_position": 0, - "target_position_step": 100, - "firmware_revision": 16, - "rf_strength": 0, - "last_seen": 1638353156, - "reachable": true, - "therm_measured_temperature": 5, - "heating_power_request": 1, - "therm_setpoint_temperature": 7, - "therm_setpoint_mode": "away", - "therm_setpoint_start_time": 0, - "therm_setpoint_end_time": 0, - "anticipating": false, - "open_window": false - }, - { - "id": "12:34:56:00:86:99", - "type": "NACamDoorTag", - "battery_state": "high", - "battery_level": 5240, - "firmware_revision": 58, - "rf_state": "full", - "rf_strength": 58, - "last_seen": 1642698124, - "last_activity": 1627757310, - "reachable": false, - "bridge": "12:34:56:00:f1:62", - "status": "no_news" - }, - { - "id": "12:34:56:00:e3:9b", - "type": "NIS", - "battery_state": "low", - "battery_level": 5438, - "firmware_revision": 209, - "rf_state": "medium", - "rf_strength": 62, - "last_seen": 1644569790, - "reachable": true, - "bridge": "12:34:56:00:f1:62", - "status": "no_sound", - "monitoring": "off" - }, - { - "id": "12:34:56:80:60:40", - "type": "NLG", - "offload": false, - "firmware_revision": 211, - "last_seen": 1644567372, - "wifi_strength": 51, - "reachable": true - }, - { - "id": "12:34:56:80:00:12:ac:f2", - "type": "NLP", - "on": true, - "offload": false, - "firmware_revision": 62, - "last_seen": 1644569425, - "power": 0, - "reachable": true, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:80:00:c3:69:3c", - "type": "NLT", - "battery_state": "full", - "battery_level": 3300, - "firmware_revision": 42, - "last_seen": 0, - "reachable": false, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:00:16:0e", - "type": "NLE", - "firmware_revision": 14, - "wifi_strength": 38 - }, - { - "id": "12:34:56:00:16:0e#0", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#1", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#2", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#3", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#4", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#5", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#6", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#7", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#8", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:00:a1:4c:da", - "type": "NLPC", - "firmware_revision": 62, - "last_seen": 1646511241, - "power": 476, - "reachable": true, "bridge": "12:34:56:80:60:40" } ], @@ -876,17 +284,17 @@ "reachable": true, "therm_measured_temperature": 19.8, "therm_setpoint_temperature": 12, - "therm_setpoint_mode": "schedule", + "therm_setpoint_mode": "away", "therm_setpoint_start_time": 1559229567, "therm_setpoint_end_time": 0 }, { "id": "2940411577", "reachable": true, - "therm_measured_temperature": 5, - "heating_power_request": 1, + "therm_measured_temperature": 27, + "heating_power_request": 0, "therm_setpoint_temperature": 7, - "therm_setpoint_mode": "away", + "therm_setpoint_mode": "hg", "therm_setpoint_start_time": 0, "therm_setpoint_end_time": 0, "anticipating": false, @@ -905,15 +313,15 @@ "open_window": false }, { - "id": "2940411588", + "id": "1002003001", "reachable": true, "anticipating": false, "heating_power_request": 0, "open_window": false, - "humidity": 68, - "therm_measured_temperature": 19.9, - "therm_setpoint_temperature": 21.5, - "therm_setpoint_start_time": 1647793285, + "humidity": 67, + "therm_measured_temperature": 22, + "therm_setpoint_temperature": 22, + "therm_setpoint_start_time": 1647462737, "therm_setpoint_end_time": null, "therm_setpoint_mode": "home" } diff --git a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json index d950c82a6a5..406e24bc107 100644 --- a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json +++ b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json @@ -1,12 +1,20 @@ { "status": "ok", - "time_server": 1559292041, + "time_server": 1642952130, "body": { "home": { - "modules": [], - "rooms": [], - "id": "91763b24c43d3e344f424e8c", - "persons": [] + "persons": [ + { + "id": "abcdef12-1111-0000-0000-000111222333", + "last_seen": 1489050910, + "out_of_sight": true + }, + { + "id": "abcdef12-2222-0000-0000-000111222333", + "last_seen": 1489078776, + "out_of_sight": true + } + ] } } } diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index beb91c7565e..76397988187 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -33,7 +33,7 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): await hass.async_block_till_done() camera_entity_indoor = "camera.hall" - camera_entity_outdoor = "camera.garden" + camera_entity_outdoor = "camera.front" assert hass.states.get(camera_entity_indoor).state == "streaming" response = { "event_type": "off", @@ -59,8 +59,8 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): response = { "event_type": "light_mode", - "device_id": "12:34:56:00:a5:a4", - "camera_id": "12:34:56:00:a5:a4", + "device_id": "12:34:56:10:b9:0e", + "camera_id": "12:34:56:10:b9:0e", "event_id": "601dce1560abca1ebad9b723", "push_type": "NOC-light_mode", "sub_type": "on", @@ -72,8 +72,8 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): response = { "event_type": "light_mode", - "device_id": "12:34:56:00:a5:a4", - "camera_id": "12:34:56:00:a5:a4", + "device_id": "12:34:56:10:b9:0e", + "camera_id": "12:34:56:10:b9:0e", "event_id": "601dce1560abca1ebad9b723", "push_type": "NOC-light_mode", "sub_type": "auto", @@ -84,7 +84,7 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): response = { "event_type": "light_mode", - "device_id": "12:34:56:00:a5:a4", + "device_id": "12:34:56:10:b9:0e", "event_id": "601dce1560abca1ebad9b723", "push_type": "NOC-light_mode", } @@ -166,7 +166,7 @@ async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth) uri = "https://prodvpn-eu-6.netatmo.net/10.20.30.41/333333333333/444444444444,," stream_uri = uri + "/live/files/high/index.m3u8" - camera_entity_indoor = "camera.garden" + camera_entity_indoor = "camera.front" cam = hass.states.get(camera_entity_indoor) assert cam is not None @@ -304,14 +304,14 @@ async def test_service_set_camera_light(hass, config_entry, netatmo_auth): await hass.async_block_till_done() data = { - "entity_id": "camera.garden", + "entity_id": "camera.front", "camera_light_mode": "on", } expected_data = { "modules": [ { - "id": "12:34:56:00:a5:a4", + "id": "12:34:56:10:b9:0e", "floodlight": "on", }, ], @@ -353,7 +353,6 @@ async def test_service_set_camera_light_invalid_type(hass, config_entry, netatmo assert excinfo.value.args == ("NACamera does not have a floodlight",) -@pytest.mark.skip async def test_camera_reconnect_webhook(hass, config_entry): """Test webhook event on camera reconnect.""" fake_post_hits = 0 @@ -406,7 +405,7 @@ async def test_camera_reconnect_webhook(hass, config_entry): dt.utcnow() + timedelta(seconds=60), ) await hass.async_block_till_done() - assert fake_post_hits > calls + assert fake_post_hits >= calls async def test_webhook_person_event(hass, config_entry, netatmo_auth): diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index d37bab929e1..afe85049f95 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -36,8 +36,7 @@ async def test_webhook_event_handling_thermostats(hass, config_entry, netatmo_au assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( - hass.states.get(climate_entity_livingroom).attributes["preset_mode"] - == "Schedule" + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" ) assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 12 @@ -80,8 +79,7 @@ async def test_webhook_event_handling_thermostats(hass, config_entry, netatmo_au assert hass.states.get(climate_entity_livingroom).state == "heat" assert ( - hass.states.get(climate_entity_livingroom).attributes["preset_mode"] - == "Schedule" + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" ) assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 21 @@ -194,8 +192,7 @@ async def test_webhook_event_handling_thermostats(hass, config_entry, netatmo_au assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( - hass.states.get(climate_entity_livingroom).attributes["preset_mode"] - == "Schedule" + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" ) @@ -213,8 +210,7 @@ async def test_service_preset_mode_frost_guard_thermostat( assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( - hass.states.get(climate_entity_livingroom).attributes["preset_mode"] - == "Schedule" + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" ) # Test service setting the preset mode to "frost guard" @@ -269,8 +265,7 @@ async def test_service_preset_mode_frost_guard_thermostat( assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( - hass.states.get(climate_entity_livingroom).attributes["preset_mode"] - == "Schedule" + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" ) @@ -286,8 +281,7 @@ async def test_service_preset_modes_thermostat(hass, config_entry, netatmo_auth) assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( - hass.states.get(climate_entity_livingroom).attributes["preset_mode"] - == "Schedule" + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" ) # Test service setting the preset mode to "away" diff --git a/tests/components/netatmo/test_light.py b/tests/components/netatmo/test_light.py index b1a5270745c..526fb2fe518 100644 --- a/tests/components/netatmo/test_light.py +++ b/tests/components/netatmo/test_light.py @@ -27,14 +27,14 @@ async def test_camera_light_setup_and_services(hass, config_entry, netatmo_auth) await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) await hass.async_block_till_done() - light_entity = "light.garden" + light_entity = "light.front" assert hass.states.get(light_entity).state == "unavailable" # Trigger light mode change response = { "event_type": "light_mode", - "device_id": "12:34:56:00:a5:a4", - "camera_id": "12:34:56:00:a5:a4", + "device_id": "12:34:56:10:b9:0e", + "camera_id": "12:34:56:10:b9:0e", "event_id": "601dce1560abca1ebad9b723", "push_type": "NOC-light_mode", "sub_type": "on", @@ -46,7 +46,7 @@ async def test_camera_light_setup_and_services(hass, config_entry, netatmo_auth) # Trigger light mode change with erroneous webhook data response = { "event_type": "light_mode", - "device_id": "12:34:56:00:a5:a4", + "device_id": "12:34:56:10:b9:0e", } await simulate_webhook(hass, webhook_id, response) @@ -62,7 +62,7 @@ async def test_camera_light_setup_and_services(hass, config_entry, netatmo_auth) ) await hass.async_block_till_done() mock_set_state.assert_called_once_with( - {"modules": [{"id": "12:34:56:00:a5:a4", "floodlight": "auto"}]} + {"modules": [{"id": "12:34:56:10:b9:0e", "floodlight": "auto"}]} ) # Test turning light on @@ -75,7 +75,7 @@ async def test_camera_light_setup_and_services(hass, config_entry, netatmo_auth) ) await hass.async_block_till_done() mock_set_state.assert_called_once_with( - {"modules": [{"id": "12:34:56:00:a5:a4", "floodlight": "on"}]} + {"modules": [{"id": "12:34:56:10:b9:0e", "floodlight": "on"}]} ) diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index d3ea8fb8167..9ef56372316 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -16,12 +16,12 @@ async def test_weather_sensor(hass, config_entry, netatmo_auth): await hass.async_block_till_done() - prefix = "sensor.netatmoindoor_" + prefix = "sensor.parents_bedroom_" - assert hass.states.get(f"{prefix}temperature").state == "24.6" - assert hass.states.get(f"{prefix}humidity").state == "36" - assert hass.states.get(f"{prefix}co2").state == "749" - assert hass.states.get(f"{prefix}pressure").state == "1017.3" + assert hass.states.get(f"{prefix}temperature").state == "20.3" + assert hass.states.get(f"{prefix}humidity").state == "63" + assert hass.states.get(f"{prefix}co2").state == "494" + assert hass.states.get(f"{prefix}pressure").state == "1014.5" async def test_public_weather_sensor(hass, config_entry, netatmo_auth): @@ -104,25 +104,25 @@ async def test_process_health(health, expected): @pytest.mark.parametrize( "uid, name, expected", [ - ("12:34:56:37:11:ca-reachable", "mystation_reachable", "True"), - ("12:34:56:03:1b:e4-rf_status", "mystation_yard_radio", "Full"), + ("12:34:56:03:1b:e4-reachable", "villa_garden_reachable", "True"), + ("12:34:56:03:1b:e4-rf_status", "villa_garden_radio", "Full"), ( - "12:34:56:37:11:ca-wifi_status", - "mystation_wifi_strength", - "Full", + "12:34:56:80:bb:26-wifi_status", + "villa_wifi_strength", + "High", ), ( - "12:34:56:37:11:ca-temp_trend", - "mystation_temperature_trend", + "12:34:56:80:bb:26-temp_trend", + "villa_temperature_trend", "stable", ), ( - "12:34:56:37:11:ca-pressure_trend", - "netatmo_mystation_pressure_trend", - "down", + "12:34:56:80:bb:26-pressure_trend", + "villa_pressure_trend", + "up", ), - ("12:34:56:05:51:20-sum_rain_1", "netatmo_mystation_yard_rain_last_hour", "0"), - ("12:34:56:05:51:20-sum_rain_24", "netatmo_mystation_yard_rain_today", "0"), + ("12:34:56:80:c1:ea-sum_rain_1", "villa_rain_rain_last_hour", "0"), + ("12:34:56:80:c1:ea-sum_rain_24", "villa_rain_rain_today", "6.9"), ("12:34:56:03:1b:e4-windangle", "netatmoindoor_garden_direction", "SW"), ( "12:34:56:03:1b:e4-windangle_value", From f3a96ce14b3d1cba01c06b267b47cda93c3cfc7e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 08:18:49 -0500 Subject: [PATCH 944/985] Bump dbus-fast to 1.60.0 (#81296) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 091962fbc83..bca2f7f9a8d 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.8.1", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.59.1" + "dbus-fast==1.60.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 914731a8164..e2c57d87c19 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.59.1 +dbus-fast==1.60.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 35856c010b8..bfa8a46021c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.59.1 +dbus-fast==1.60.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 25febf43e63..6fbc1a59d74 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.59.1 +dbus-fast==1.60.0 # homeassistant.components.debugpy debugpy==1.6.3 From 0a476baf16014f26cd67ad9fbbdf166bb3d3d7b8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 Oct 2022 09:54:14 -0400 Subject: [PATCH 945/985] Bumped version to 2022.11.0b4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3f25ea89c09..5acf294fb68 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 5a9507f8dfa..16ff4bc6bbe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.0b3" +version = "2022.11.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 3ddcc637da42bd5c19c41441261fdfffe4ee794e Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Mon, 31 Oct 2022 09:57:54 -0400 Subject: [PATCH 946/985] Create repairs for unsupported and unhealthy (#80747) --- homeassistant/components/hassio/__init__.py | 6 + homeassistant/components/hassio/const.py | 21 +- homeassistant/components/hassio/handler.py | 8 + homeassistant/components/hassio/repairs.py | 138 ++++++ homeassistant/components/hassio/strings.json | 10 + tests/components/hassio/test_binary_sensor.py | 13 + tests/components/hassio/test_diagnostics.py | 13 + tests/components/hassio/test_init.py | 43 +- tests/components/hassio/test_repairs.py | 395 ++++++++++++++++++ tests/components/hassio/test_sensor.py | 13 + tests/components/hassio/test_update.py | 13 + tests/components/hassio/test_websocket_api.py | 13 + tests/components/http/test_ban.py | 12 +- tests/components/onboarding/test_views.py | 13 + 14 files changed, 690 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/hassio/repairs.py create mode 100644 tests/components/hassio/test_repairs.py diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 8535a0c3cc6..c811b35812e 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -77,6 +77,7 @@ from .discovery import HassioServiceInfo, async_setup_discovery_view # noqa: F4 from .handler import HassIO, HassioAPIError, api_data from .http import HassIOView from .ingress import async_setup_ingress_view +from .repairs import SupervisorRepairs from .websocket_api import async_load_websocket_api _LOGGER = logging.getLogger(__name__) @@ -103,6 +104,7 @@ DATA_SUPERVISOR_INFO = "hassio_supervisor_info" DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs" DATA_ADDONS_INFO = "hassio_addons_info" DATA_ADDONS_STATS = "hassio_addons_stats" +DATA_SUPERVISOR_REPAIRS = "supervisor_repairs" HASSIO_UPDATE_INTERVAL = timedelta(minutes=5) ADDONS_COORDINATOR = "hassio_addons_coordinator" @@ -758,6 +760,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"}) ) + # Start listening for problems with supervisor and making repairs + hass.data[DATA_SUPERVISOR_REPAIRS] = repairs = SupervisorRepairs(hass, hassio) + await repairs.setup() + return True diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index e37a31ddbd6..64ef7a718a5 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -11,19 +11,26 @@ ATTR_CONFIG = "config" ATTR_DATA = "data" ATTR_DISCOVERY = "discovery" ATTR_ENABLE = "enable" +ATTR_ENDPOINT = "endpoint" ATTR_FOLDERS = "folders" +ATTR_HEALTHY = "healthy" ATTR_HOMEASSISTANT = "homeassistant" ATTR_INPUT = "input" +ATTR_METHOD = "method" ATTR_PANELS = "panels" ATTR_PASSWORD = "password" +ATTR_RESULT = "result" +ATTR_SUPPORTED = "supported" +ATTR_TIMEOUT = "timeout" ATTR_TITLE = "title" +ATTR_UNHEALTHY = "unhealthy" +ATTR_UNHEALTHY_REASONS = "unhealthy_reasons" +ATTR_UNSUPPORTED = "unsupported" +ATTR_UNSUPPORTED_REASONS = "unsupported_reasons" +ATTR_UPDATE_KEY = "update_key" ATTR_USERNAME = "username" ATTR_UUID = "uuid" ATTR_WS_EVENT = "event" -ATTR_ENDPOINT = "endpoint" -ATTR_METHOD = "method" -ATTR_RESULT = "result" -ATTR_TIMEOUT = "timeout" X_AUTH_TOKEN = "X-Supervisor-Token" X_INGRESS_PATH = "X-Ingress-Path" @@ -38,6 +45,11 @@ WS_TYPE_EVENT = "supervisor/event" WS_TYPE_SUBSCRIBE = "supervisor/subscribe" EVENT_SUPERVISOR_EVENT = "supervisor_event" +EVENT_SUPERVISOR_UPDATE = "supervisor_update" +EVENT_HEALTH_CHANGED = "health_changed" +EVENT_SUPPORTED_CHANGED = "supported_changed" + +UPDATE_KEY_SUPERVISOR = "supervisor" ATTR_AUTO_UPDATE = "auto_update" ATTR_VERSION = "version" @@ -51,7 +63,6 @@ ATTR_STARTED = "started" ATTR_URL = "url" ATTR_REPOSITORY = "repository" - DATA_KEY_ADDONS = "addons" DATA_KEY_OS = "os" DATA_KEY_SUPERVISOR = "supervisor" diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 7b3ed697227..ee16bdf8158 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -190,6 +190,14 @@ class HassIO: """ return self.send_command(f"/discovery/{uuid}", method="get") + @api_data + def get_resolution_info(self): + """Return data for Supervisor resolution center. + + This method return a coroutine. + """ + return self.send_command("/resolution/info", method="get") + @_api_bool async def update_hass_api(self, http_config, refresh_token): """Update Home Assistant API data on Hass.io.""" diff --git a/homeassistant/components/hassio/repairs.py b/homeassistant/components/hassio/repairs.py new file mode 100644 index 00000000000..a8c6788f4d5 --- /dev/null +++ b/homeassistant/components/hassio/repairs.py @@ -0,0 +1,138 @@ +"""Supervisor events monitor.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) + +from .const import ( + ATTR_DATA, + ATTR_HEALTHY, + ATTR_SUPPORTED, + ATTR_UNHEALTHY, + ATTR_UNHEALTHY_REASONS, + ATTR_UNSUPPORTED, + ATTR_UNSUPPORTED_REASONS, + ATTR_UPDATE_KEY, + ATTR_WS_EVENT, + DOMAIN, + EVENT_HEALTH_CHANGED, + EVENT_SUPERVISOR_EVENT, + EVENT_SUPERVISOR_UPDATE, + EVENT_SUPPORTED_CHANGED, + UPDATE_KEY_SUPERVISOR, +) +from .handler import HassIO + +ISSUE_ID_UNHEALTHY = "unhealthy_system" +ISSUE_ID_UNSUPPORTED = "unsupported_system" + +INFO_URL_UNHEALTHY = "https://www.home-assistant.io/more-info/unhealthy" +INFO_URL_UNSUPPORTED = "https://www.home-assistant.io/more-info/unsupported" + + +class SupervisorRepairs: + """Create repairs from supervisor events.""" + + def __init__(self, hass: HomeAssistant, client: HassIO) -> None: + """Initialize supervisor repairs.""" + self._hass = hass + self._client = client + self._unsupported_reasons: set[str] = set() + self._unhealthy_reasons: set[str] = set() + + @property + def unhealthy_reasons(self) -> set[str]: + """Get unhealthy reasons. Returns empty set if system is healthy.""" + return self._unhealthy_reasons + + @unhealthy_reasons.setter + def unhealthy_reasons(self, reasons: set[str]) -> None: + """Set unhealthy reasons. Create or delete repairs as necessary.""" + for unhealthy in reasons - self.unhealthy_reasons: + async_create_issue( + self._hass, + DOMAIN, + f"{ISSUE_ID_UNHEALTHY}_{unhealthy}", + is_fixable=False, + learn_more_url=f"{INFO_URL_UNHEALTHY}/{unhealthy}", + severity=IssueSeverity.CRITICAL, + translation_key="unhealthy", + translation_placeholders={"reason": unhealthy}, + ) + + for fixed in self.unhealthy_reasons - reasons: + async_delete_issue(self._hass, DOMAIN, f"{ISSUE_ID_UNHEALTHY}_{fixed}") + + self._unhealthy_reasons = reasons + + @property + def unsupported_reasons(self) -> set[str]: + """Get unsupported reasons. Returns empty set if system is supported.""" + return self._unsupported_reasons + + @unsupported_reasons.setter + def unsupported_reasons(self, reasons: set[str]) -> None: + """Set unsupported reasons. Create or delete repairs as necessary.""" + for unsupported in reasons - self.unsupported_reasons: + async_create_issue( + self._hass, + DOMAIN, + f"{ISSUE_ID_UNSUPPORTED}_{unsupported}", + is_fixable=False, + learn_more_url=f"{INFO_URL_UNSUPPORTED}/{unsupported}", + severity=IssueSeverity.WARNING, + translation_key="unsupported", + translation_placeholders={"reason": unsupported}, + ) + + for fixed in self.unsupported_reasons - reasons: + async_delete_issue(self._hass, DOMAIN, f"{ISSUE_ID_UNSUPPORTED}_{fixed}") + + self._unsupported_reasons = reasons + + async def setup(self) -> None: + """Create supervisor events listener.""" + await self.update() + + async_dispatcher_connect( + self._hass, EVENT_SUPERVISOR_EVENT, self._supervisor_events_to_repairs + ) + + async def update(self) -> None: + """Update repairs from Supervisor resolution center.""" + data = await self._client.get_resolution_info() + self.unhealthy_reasons = set(data[ATTR_UNHEALTHY]) + self.unsupported_reasons = set(data[ATTR_UNSUPPORTED]) + + @callback + def _supervisor_events_to_repairs(self, event: dict[str, Any]) -> None: + """Create repairs from supervisor events.""" + if ATTR_WS_EVENT not in event: + return + + if ( + event[ATTR_WS_EVENT] == EVENT_SUPERVISOR_UPDATE + and event.get(ATTR_UPDATE_KEY) == UPDATE_KEY_SUPERVISOR + ): + self._hass.async_create_task(self.update()) + + elif event[ATTR_WS_EVENT] == EVENT_HEALTH_CHANGED: + self.unhealthy_reasons = ( + set() + if event[ATTR_DATA][ATTR_HEALTHY] + else set(event[ATTR_DATA][ATTR_UNHEALTHY_REASONS]) + ) + + elif event[ATTR_WS_EVENT] == EVENT_SUPPORTED_CHANGED: + self.unsupported_reasons = ( + set() + if event[ATTR_DATA][ATTR_SUPPORTED] + else set(event[ATTR_DATA][ATTR_UNSUPPORTED_REASONS]) + ) diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 90142bd453f..81b5ce01b79 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -15,5 +15,15 @@ "update_channel": "Update Channel", "version_api": "Version API" } + }, + "issues": { + "unhealthy": { + "title": "Unhealthy system - {reason}", + "description": "System is currently unhealthy due to '{reason}'. Use the link to learn more about what is wrong and how to fix it." + }, + "unsupported": { + "title": "Unsupported system - {reason}", + "description": "System is unsupported due to '{reason}'. Use the link to learn more about what this means and how to return to a supported system." + } } } diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py index a601f98f1c5..c2dab178ad8 100644 --- a/tests/components/hassio/test_binary_sensor.py +++ b/tests/components/hassio/test_binary_sensor.py @@ -133,6 +133,19 @@ def mock_all(aioclient_mock, request): "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) @pytest.mark.parametrize( diff --git a/tests/components/hassio/test_diagnostics.py b/tests/components/hassio/test_diagnostics.py index 1f915e17e61..9eaaf5f97d9 100644 --- a/tests/components/hassio/test_diagnostics.py +++ b/tests/components/hassio/test_diagnostics.py @@ -139,6 +139,19 @@ def mock_all(aioclient_mock, request): "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) async def test_diagnostics( diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index f0f94661d50..371398e32c9 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -183,6 +183,19 @@ def mock_all(aioclient_mock, request, os_info): "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) async def test_setup_api_ping(hass, aioclient_mock): @@ -191,7 +204,7 @@ async def test_setup_api_ping(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {}) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0" assert hass.components.hassio.is_hassio() @@ -230,7 +243,7 @@ async def test_setup_api_push_api_data(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert aioclient_mock.mock_calls[1][2]["watchdog"] @@ -246,7 +259,7 @@ async def test_setup_api_push_api_data_server_host(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert not aioclient_mock.mock_calls[1][2]["watchdog"] @@ -258,7 +271,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"] @@ -325,7 +338,7 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token @@ -339,7 +352,7 @@ async def test_setup_core_push_timezone(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone" with patch("homeassistant.util.dt.set_default_time_zone"): @@ -356,7 +369,7 @@ async def test_setup_hassio_no_additional_data(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456" @@ -426,14 +439,14 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert aioclient_mock.mock_calls[-1][2] == "test" await hass.services.async_call("hassio", "host_shutdown", {}) await hass.services.async_call("hassio", "host_reboot", {}) await hass.async_block_till_done() - assert aioclient_mock.call_count == 11 + assert aioclient_mock.call_count == 12 await hass.services.async_call("hassio", "backup_full", {}) await hass.services.async_call( @@ -448,7 +461,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 13 + assert aioclient_mock.call_count == 14 assert aioclient_mock.mock_calls[-1][2] == { "homeassistant": True, "addons": ["test"], @@ -472,7 +485,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert aioclient_mock.mock_calls[-1][2] == { "addons": ["test"], "folders": ["ssl"], @@ -491,12 +504,12 @@ async def test_service_calls_core(hassio_env, hass, aioclient_mock): await hass.services.async_call("homeassistant", "stop") await hass.async_block_till_done() - assert aioclient_mock.call_count == 5 + assert aioclient_mock.call_count == 6 await hass.services.async_call("homeassistant", "check_config") await hass.async_block_till_done() - assert aioclient_mock.call_count == 5 + assert aioclient_mock.call_count == 6 with patch( "homeassistant.config.async_check_ha_config_file", return_value=None @@ -505,7 +518,7 @@ async def test_service_calls_core(hassio_env, hass, aioclient_mock): await hass.async_block_till_done() assert mock_check_config.called - assert aioclient_mock.call_count == 6 + assert aioclient_mock.call_count == 7 async def test_entry_load_and_unload(hass): @@ -758,7 +771,7 @@ async def test_setup_hardware_integration(hass, aioclient_mock, integration): assert result await hass.async_block_till_done() - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py new file mode 100644 index 00000000000..ebaf46be3b5 --- /dev/null +++ b/tests/components/hassio/test_repairs.py @@ -0,0 +1,395 @@ +"""Test repairs from supervisor issues.""" + +from __future__ import annotations + +import os +from typing import Any +from unittest.mock import ANY, patch + +import pytest + +from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .test_init import MOCK_ENVIRON + +from tests.test_util.aiohttp import AiohttpClientMocker + + +@pytest.fixture(autouse=True) +async def setup_repairs(hass): + """Set up the repairs integration.""" + assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) + + +@pytest.fixture(autouse=True) +def mock_all(aioclient_mock: AiohttpClientMocker, request: pytest.FixtureRequest): + """Mock all setup requests.""" + aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) + aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) + aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/info", + json={ + "result": "ok", + "data": { + "supervisor": "222", + "homeassistant": "0.110.0", + "hassos": "1.2.3", + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/store", + json={ + "result": "ok", + "data": {"addons": [], "repositories": []}, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/host/info", + json={ + "result": "ok", + "data": { + "result": "ok", + "data": { + "chassis": "vm", + "operating_system": "Debian GNU/Linux 10 (buster)", + "kernel": "4.19.0-6-amd64", + }, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/core/info", + json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}}, + ) + aioclient_mock.get( + "http://127.0.0.1/os/info", + json={ + "result": "ok", + "data": { + "version_latest": "1.0.0", + "version": "1.0.0", + "update_available": False, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", + json={ + "result": "ok", + "data": { + "result": "ok", + "version": "1.0.0", + "version_latest": "1.0.0", + "auto_update": True, + "addons": [], + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} + ) + aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) + + +@pytest.fixture(autouse=True) +async def fixture_supervisor_environ(): + """Mock os environ for supervisor.""" + with patch.dict(os.environ, MOCK_ENVIRON): + yield + + +def mock_resolution_info( + aioclient_mock: AiohttpClientMocker, + unsupported: list[str] | None = None, + unhealthy: list[str] | None = None, +): + """Mock resolution/info endpoint with unsupported/unhealthy reasons.""" + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": unsupported or [], + "unhealthy": unhealthy or [], + "suggestions": [], + "issues": [], + "checks": [ + {"enabled": True, "slug": "supervisor_trust"}, + {"enabled": True, "slug": "free_space"}, + ], + }, + }, + ) + + +def assert_repair_in_list(issues: list[dict[str, Any]], unhealthy: bool, reason: str): + """Assert repair for unhealthy/unsupported in list.""" + repair_type = "unhealthy" if unhealthy else "unsupported" + assert { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "hassio", + "ignored": False, + "is_fixable": False, + "issue_id": f"{repair_type}_system_{reason}", + "issue_domain": None, + "learn_more_url": f"https://www.home-assistant.io/more-info/{repair_type}/{reason}", + "severity": "critical" if unhealthy else "warning", + "translation_key": repair_type, + "translation_placeholders": { + "reason": reason, + }, + } in issues + + +async def test_unhealthy_repairs( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Test repairs added for unhealthy systems.""" + mock_resolution_info(aioclient_mock, unhealthy=["docker", "setup"]) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 2 + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker") + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="setup") + + +async def test_unsupported_repairs( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Test repairs added for unsupported systems.""" + mock_resolution_info(aioclient_mock, unsupported=["content_trust", "os"]) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 2 + assert_repair_in_list( + msg["result"]["issues"], unhealthy=False, reason="content_trust" + ) + assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os") + + +async def test_unhealthy_repairs_add_remove( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Test unhealthy repairs added and removed from dispatches.""" + mock_resolution_info(aioclient_mock) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 1, + "type": "supervisor/event", + "data": { + "event": "health_changed", + "data": { + "healthy": False, + "unhealthy_reasons": ["docker"], + }, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + await client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker") + + await client.send_json( + { + "id": 3, + "type": "supervisor/event", + "data": { + "event": "health_changed", + "data": {"healthy": True}, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + await client.send_json({"id": 4, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"issues": []} + + +async def test_unsupported_repairs_add_remove( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Test unsupported repairs added and removed from dispatches.""" + mock_resolution_info(aioclient_mock) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 1, + "type": "supervisor/event", + "data": { + "event": "supported_changed", + "data": { + "supported": False, + "unsupported_reasons": ["os"], + }, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + await client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os") + + await client.send_json( + { + "id": 3, + "type": "supervisor/event", + "data": { + "event": "supported_changed", + "data": {"supported": True}, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + await client.send_json({"id": 4, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"issues": []} + + +async def test_reset_repairs_supervisor_restart( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Unsupported/unhealthy repairs reset on supervisor restart.""" + mock_resolution_info(aioclient_mock, unsupported=["os"], unhealthy=["docker"]) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 2 + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker") + assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os") + + aioclient_mock.clear_requests() + mock_resolution_info(aioclient_mock) + await client.send_json( + { + "id": 2, + "type": "supervisor/event", + "data": { + "event": "supervisor_update", + "update_key": "supervisor", + "data": {}, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + await client.send_json({"id": 3, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"issues": []} + + +async def test_reasons_added_and_removed( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Test an unsupported/unhealthy reasons being added and removed at same time.""" + mock_resolution_info(aioclient_mock, unsupported=["os"], unhealthy=["docker"]) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 2 + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker") + assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os") + + aioclient_mock.clear_requests() + mock_resolution_info( + aioclient_mock, unsupported=["content_trust"], unhealthy=["setup"] + ) + await client.send_json( + { + "id": 2, + "type": "supervisor/event", + "data": { + "event": "supervisor_update", + "update_key": "supervisor", + "data": {}, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + await client.send_json({"id": 3, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 2 + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="setup") + assert_repair_in_list( + msg["result"]["issues"], unhealthy=False, reason="content_trust" + ) diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index 16cce09b800..e9f0bd631b0 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -126,6 +126,19 @@ def mock_all(aioclient_mock, request): "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) @pytest.mark.parametrize( diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index aaa77cde129..02d6b1dbf6b 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -139,6 +139,19 @@ def mock_all(aioclient_mock, request): "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) @pytest.mark.parametrize( diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index 5d11d13166e..767f0abaf35 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -61,6 +61,19 @@ def mock_all(aioclient_mock): aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) async def test_ws_subscription(hassio_env, hass: HomeAssistant, hass_ws_client): diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index 7a4202c1a67..a4249a1efb6 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -198,7 +198,17 @@ async def test_access_from_supervisor_ip( manager: IpBanManager = app[KEY_BAN_MANAGER] - assert await async_setup_component(hass, "hassio", {"hassio": {}}) + with patch( + "homeassistant.components.hassio.HassIO.get_resolution_info", + return_value={ + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + ): + assert await async_setup_component(hass, "hassio", {"hassio": {}}) m_open = mock_open() diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 204eb6bf772..40d889185dd 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -57,6 +57,19 @@ 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"}) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) with patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}), patch( "homeassistant.components.hassio.HassIO.is_connected", return_value=True, From 7046f5f19e3238c0b3111b9712aad4245ea9c759 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 1 Nov 2022 02:22:21 +0100 Subject: [PATCH 947/985] Only try initializing Hue motion LED on endpoint 2 with ZHA (#81205) --- homeassistant/components/zha/core/channels/general.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index ded51455af8..c028a6021da 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -156,7 +156,7 @@ class BasicChannel(ZigbeeChannel): def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: """Initialize Basic channel.""" super().__init__(cluster, ch_pool) - if is_hue_motion_sensor(self): + if is_hue_motion_sensor(self) and self.cluster.endpoint.endpoint_id == 2: self.ZCL_INIT_ATTRS = ( # pylint: disable=invalid-name self.ZCL_INIT_ATTRS.copy() ) From 9b4f2df8f34355099c8b44cc041922e7d57b6016 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 12:29:12 -0500 Subject: [PATCH 948/985] Bump aiohomekit to 2.2.10 (#81312) --- 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 58e258294a0..93aae62daab 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.9"], + "requirements": ["aiohomekit==2.2.10"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index bfa8a46021c..c8b0aec9ccc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.9 +aiohomekit==2.2.10 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6fbc1a59d74..1e0053ae72d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.9 +aiohomekit==2.2.10 # homeassistant.components.emulated_hue # homeassistant.components.http From d7e76fdf3a5e89617d79508bd3418459197c4c6b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 12:35:43 -0500 Subject: [PATCH 949/985] Bump zeroconf to 0.39.4 (#81313) --- 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 967dd761ac7..382cf42b54f 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.39.3"], + "requirements": ["zeroconf==0.39.4"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e2c57d87c19..411b0a06646 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -42,7 +42,7 @@ typing-extensions>=4.4.0,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 yarl==1.8.1 -zeroconf==0.39.3 +zeroconf==0.39.4 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index c8b0aec9ccc..737191c0674 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2604,7 +2604,7 @@ zamg==0.1.1 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.39.3 +zeroconf==0.39.4 # homeassistant.components.zha zha-quirks==0.0.84 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e0053ae72d..2d4c36b717a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1805,7 +1805,7 @@ youless-api==0.16 zamg==0.1.1 # homeassistant.components.zeroconf -zeroconf==0.39.3 +zeroconf==0.39.4 # homeassistant.components.zha zha-quirks==0.0.84 From 19a5c87da6869244e4b66a7efe544976dee45955 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 13:38:57 -0500 Subject: [PATCH 950/985] Bump oralb-ble to 0.10.0 (#81315) --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index 8f694946804..cad6167228c 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.9.0"], + "requirements": ["oralb-ble==0.10.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 737191c0674..25f63a9faf7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1238,7 +1238,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.9.0 +oralb-ble==0.10.0 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2d4c36b717a..bc3f4ee9afa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -883,7 +883,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.9.0 +oralb-ble==0.10.0 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From 356953c8bc898770b22fcaa0522e3a83eeece68a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 Oct 2022 20:36:59 +0100 Subject: [PATCH 951/985] Update base image to 2022.10.0 (#81317) --- build.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.yaml b/build.yaml index 9cf66e2621a..14a59641388 100644 --- a/build.yaml +++ b/build.yaml @@ -1,11 +1,11 @@ image: homeassistant/{arch}-homeassistant shadow_repository: ghcr.io/home-assistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.07.0 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.07.0 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.07.0 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.07.0 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.07.0 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.10.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.10.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.10.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.10.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.10.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io From 882ad31a99f2f0e306e7b902c28837d72028d7b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 20:21:40 -0500 Subject: [PATCH 952/985] Fix Yale Access Bluetooth not being available again after being unavailable (#81320) --- homeassistant/components/yalexs_ble/__init__.py | 13 +++++++++++++ homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index 6073bf7a032..7a2b3146265 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -94,6 +94,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.title, push_lock ) + @callback + def _async_device_unavailable( + _service_info: bluetooth.BluetoothServiceInfoBleak, + ) -> None: + """Handle device not longer being seen by the bluetooth stack.""" + push_lock.reset_advertisement_state() + + entry.async_on_unload( + bluetooth.async_track_unavailable( + hass, _async_device_unavailable, push_lock.address + ) + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 7bc8bde5b30..b43ce18a7e9 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.9.4"], + "requirements": ["yalexs-ble==1.9.5"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 25f63a9faf7..b6d58a7854b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2577,7 +2577,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.9.4 +yalexs-ble==1.9.5 # homeassistant.components.august yalexs==1.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc3f4ee9afa..dcb073c2a7c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1787,7 +1787,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.9.4 +yalexs-ble==1.9.5 # homeassistant.components.august yalexs==1.2.6 From 599c23c1d72ad460546ab6707e045b20d0ffd720 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 31 Oct 2022 20:42:18 +0100 Subject: [PATCH 953/985] Update frontend to 20221031.0 (#81324) --- 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 c8d3645435f..aed26eb5de1 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==20221027.0"], + "requirements": ["home-assistant-frontend==20221031.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 411b0a06646..adff342729d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.60.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 -home-assistant-frontend==20221027.0 +home-assistant-frontend==20221031.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index b6d58a7854b..5ad8830149c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,7 +868,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221027.0 +home-assistant-frontend==20221031.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dcb073c2a7c..8476aab8c24 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,7 +648,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221027.0 +home-assistant-frontend==20221031.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 941512641b1f538f6ed9333a3184fe9839f6205d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 20:21:11 -0500 Subject: [PATCH 954/985] Improve esphome bluetooth error reporting (#81326) --- homeassistant/components/esphome/bluetooth/client.py | 10 ++++++++-- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 5f20a73f4d6..72531a2503a 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -7,7 +7,11 @@ import logging from typing import Any, TypeVar, cast import uuid -from aioesphomeapi import ESP_CONNECTION_ERROR_DESCRIPTION, BLEConnectionError +from aioesphomeapi import ( + ESP_CONNECTION_ERROR_DESCRIPTION, + ESPHOME_GATT_ERRORS, + BLEConnectionError, +) from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError import async_timeout from bleak.backends.characteristic import BleakGATTCharacteristic @@ -207,7 +211,9 @@ class ESPHomeClient(BaseBleakClient): human_error = ESP_CONNECTION_ERROR_DESCRIPTION[ble_connection_error] except (KeyError, ValueError): ble_connection_error_name = str(error) - human_error = f"Unknown error code {error}" + human_error = ESPHOME_GATT_ERRORS.get( + error, f"Unknown error code {error}" + ) connected_future.set_exception( BleakError( f"Error {ble_connection_error_name} while connecting: {human_error}" diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index c27e3b8dc3e..64cd6b4029c 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==11.4.1"], + "requirements": ["aioesphomeapi==11.4.2"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index 5ad8830149c..662bc2b5075 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -153,7 +153,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.1 +aioesphomeapi==11.4.2 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8476aab8c24..be8b850e6b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.1 +aioesphomeapi==11.4.2 # homeassistant.components.flo aioflo==2021.11.0 From 0ac0e9c0d5ecac6a8a64cd10e34daff13ae1db53 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 Oct 2022 21:23:21 -0400 Subject: [PATCH 955/985] Bumped version to 2022.11.0b5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5acf294fb68..f547e536ae0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 16ff4bc6bbe..5f2aa4d4311 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.0b4" +version = "2022.11.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From dfe399e370b432ba5936e629d3d40f1227849a21 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Nov 2022 18:08:26 +0100 Subject: [PATCH 956/985] Cherry-pick translation updates for Supervisor (#81341) --- homeassistant/components/hassio/translations/ca.json | 10 ++++++++++ homeassistant/components/hassio/translations/en.json | 10 ++++++++++ homeassistant/components/hassio/translations/es.json | 10 ++++++++++ homeassistant/components/hassio/translations/et.json | 10 ++++++++++ homeassistant/components/hassio/translations/hu.json | 10 ++++++++++ .../components/hassio/translations/pt-BR.json | 10 ++++++++++ homeassistant/components/hassio/translations/ru.json | 10 ++++++++++ 7 files changed, 70 insertions(+) diff --git a/homeassistant/components/hassio/translations/ca.json b/homeassistant/components/hassio/translations/ca.json index 2c4285d4908..14679301993 100644 --- a/homeassistant/components/hassio/translations/ca.json +++ b/homeassistant/components/hassio/translations/ca.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "El sistema no \u00e9s saludable a causa de '{reason}'. Clica l'enlla\u00e7 per obtenir m\u00e9s informaci\u00f3 sobre qu\u00e8 falla aix\u00f2 i com solucionar-ho.", + "title": "Sistema no saludable - {reason}" + }, + "unsupported": { + "description": "El sistema no \u00e9s compatible a causa de '{reason}'. Clica l'enlla\u00e7 per obtenir m\u00e9s informaci\u00f3 sobre qu\u00e8 significa aix\u00f2 i com tornar a un sistema compatible.", + "title": "Sistema no compatible - {reason}" + } + }, "system_health": { "info": { "agent_version": "Versi\u00f3 de l'agent", diff --git a/homeassistant/components/hassio/translations/en.json b/homeassistant/components/hassio/translations/en.json index 14d79f0d8d6..b6f006e3093 100644 --- a/homeassistant/components/hassio/translations/en.json +++ b/homeassistant/components/hassio/translations/en.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "System is currently unhealthy due to '{reason}'. Use the link to learn more about what is wrong and how to fix it.", + "title": "Unhealthy system - {reason}" + }, + "unsupported": { + "description": "System is unsupported due to '{reason}'. Use the link to learn more about what this means and how to return to a supported system.", + "title": "Unsupported system - {reason}" + } + }, "system_health": { "info": { "agent_version": "Agent Version", diff --git a/homeassistant/components/hassio/translations/es.json b/homeassistant/components/hassio/translations/es.json index 102256ef117..f2aef9d7214 100644 --- a/homeassistant/components/hassio/translations/es.json +++ b/homeassistant/components/hassio/translations/es.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "Actualmente el sistema no est\u00e1 en buen estado debido a ''{reason}''. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n sobre lo que est\u00e1 mal y c\u00f3mo solucionarlo.", + "title": "Sistema en mal estado: {reason}" + }, + "unsupported": { + "description": "El sistema no es compatible debido a ''{reason}''. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n sobre lo que esto significa y c\u00f3mo volver a un sistema compatible.", + "title": "Sistema no compatible: {reason}" + } + }, "system_health": { "info": { "agent_version": "Versi\u00f3n del agente", diff --git a/homeassistant/components/hassio/translations/et.json b/homeassistant/components/hassio/translations/et.json index b86eef353b9..ea0f78c0c57 100644 --- a/homeassistant/components/hassio/translations/et.json +++ b/homeassistant/components/hassio/translations/et.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "S\u00fcsteem ei ole praegu korras '{reason}' t\u00f5ttu. Kasuta linki, et saada rohkem teavet selle kohta, mis on valesti ja kuidas seda parandada.", + "title": "Vigane s\u00fcsteem \u2013 {reason}" + }, + "unsupported": { + "description": "S\u00fcsteemi ei toetata '{reason}' t\u00f5ttu. Kasuta linki, et saada lisateavet selle kohta, mida see t\u00e4hendab ja kuidas toetatud s\u00fcsteemi naasta.", + "title": "Toetamata s\u00fcsteem \u2013 {reason}" + } + }, "system_health": { "info": { "agent_version": "Agendi versioon", diff --git a/homeassistant/components/hassio/translations/hu.json b/homeassistant/components/hassio/translations/hu.json index 4c83b94935d..604a8ae59e6 100644 --- a/homeassistant/components/hassio/translations/hu.json +++ b/homeassistant/components/hassio/translations/hu.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "A rendszer jelenleg renellenes \u00e1llapotban van '{reason}' miatt. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet is megtudhat arr\u00f3l, hogy mi a probl\u00e9ma, \u00e9s hogyan jav\u00edthatja ki.", + "title": "Rendellenes \u00e1llapot \u2013 {reason}" + }, + "unsupported": { + "description": "A rendszer nem t\u00e1mogatott a k\u00f6vetkez\u0151 miatt: '{reason}'. A hivatkoz\u00e1s seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat arr\u00f3l, mit jelent ez, \u00e9s hogyan t\u00e9rhet vissza egy t\u00e1mogatott rendszerhez.", + "title": "Nem t\u00e1mogatott rendszer \u2013 {reason}" + } + }, "system_health": { "info": { "agent_version": "\u00dcgyn\u00f6k verzi\u00f3", diff --git a/homeassistant/components/hassio/translations/pt-BR.json b/homeassistant/components/hassio/translations/pt-BR.json index 4f3e5d84ec1..47e0b6df4ae 100644 --- a/homeassistant/components/hassio/translations/pt-BR.json +++ b/homeassistant/components/hassio/translations/pt-BR.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "O sistema n\u00e3o est\u00e1 \u00edntegro devido a '{reason}'. Use o link para saber mais sobre o que est\u00e1 errado e como corrigi-lo.", + "title": "Sistema insalubre - {reason}" + }, + "unsupported": { + "description": "O sistema n\u00e3o \u00e9 suportado devido a '{reason}'. Use o link para saber mais sobre o que isso significa e como retornar a um sistema compat\u00edvel.", + "title": "Sistema n\u00e3o suportado - {reason}" + } + }, "system_health": { "info": { "agent_version": "Vers\u00e3o do Agent", diff --git a/homeassistant/components/hassio/translations/ru.json b/homeassistant/components/hassio/translations/ru.json index 5e1caa41ebf..0ab366c1775 100644 --- a/homeassistant/components/hassio/translations/ru.json +++ b/homeassistant/components/hassio/translations/ru.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u0430 \u043f\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 '{reason}'. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u0447\u0442\u043e \u043d\u0435 \u0442\u0430\u043a \u0438 \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - {reason}" + }, + "unsupported": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 '{reason}'. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u0447\u0442\u043e \u044d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442 \u0438 \u043a\u0430\u043a \u0432\u0435\u0440\u043d\u0443\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - {reason}" + } + }, "system_health": { "info": { "agent_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u0430\u0433\u0435\u043d\u0442\u0430", From 8965a1322cd74fc3d77d38eefc134f81db1a95d9 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 1 Nov 2022 09:29:38 +0100 Subject: [PATCH 957/985] Always use Celsius in Shelly integration (#80842) --- homeassistant/components/shelly/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 921ffb352d5..b65c314789a 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -16,7 +16,6 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, device_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from homeassistant.util.unit_system import METRIC_SYSTEM from .const import ( CONF_COAP_PORT, @@ -113,13 +112,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Shelly block based device from a config entry.""" - temperature_unit = "C" if hass.config.units is METRIC_SYSTEM else "F" - options = aioshelly.common.ConnectionOptions( entry.data[CONF_HOST], entry.data.get(CONF_USERNAME), entry.data.get(CONF_PASSWORD), - temperature_unit, ) coap_context = await get_coap_context(hass) From 9b87f7f6f9409e1117c255c011f7480daf17d9cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Oct 2022 15:50:59 -0500 Subject: [PATCH 958/985] Fix homekit diagnostics test when version changes (#81046) --- tests/components/homekit/test_diagnostics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/homekit/test_diagnostics.py b/tests/components/homekit/test_diagnostics.py index 15d4a6f6e2e..1f6f7c584f3 100644 --- a/tests/components/homekit/test_diagnostics.py +++ b/tests/components/homekit/test_diagnostics.py @@ -190,7 +190,7 @@ async def test_config_entry_accessory( "iid": 7, "perms": ["pr"], "type": "52", - "value": "2022.11.0", + "value": ANY, }, ], "iid": 1, From 4684101a853baeb8bb7624136748fe094c1cb980 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 28 Oct 2022 17:05:43 +0200 Subject: [PATCH 959/985] Improve MQTT update platform (#81131) * Allow JSON as state_topic payload * Add title * Add release_url * Add release_summary * Add entity_picture * Fix typo * Add abbreviations --- .../components/mqtt/abbreviations.py | 4 + homeassistant/components/mqtt/update.py | 82 +++++++++-- tests/components/mqtt/test_update.py | 134 +++++++++++++++++- 3 files changed, 211 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 67fffec1106..00f6d357553 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -52,6 +52,7 @@ ABBREVIATIONS = { "e": "encoding", "en": "enabled_by_default", "ent_cat": "entity_category", + "ent_pic": "entity_picture", "err_t": "error_topic", "err_tpl": "error_template", "fanspd_t": "fan_speed_topic", @@ -169,6 +170,8 @@ ABBREVIATIONS = { "pr_mode_val_tpl": "preset_mode_value_template", "pr_modes": "preset_modes", "r_tpl": "red_template", + "rel_s": "release_summary", + "rel_u": "release_url", "ret": "retain", "rgb_cmd_tpl": "rgb_command_template", "rgb_cmd_t": "rgb_command_topic", @@ -242,6 +245,7 @@ ABBREVIATIONS = { "tilt_opt": "tilt_optimistic", "tilt_status_t": "tilt_status_topic", "tilt_status_tpl": "tilt_status_template", + "tit": "title", "t": "topic", "uniq_id": "unique_id", "unit_of_meas": "unit_of_measurement", diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index 8fdc6393e0b..986ad013520 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -19,6 +19,7 @@ from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_VALUE_TEMPLAT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_loads from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -30,6 +31,7 @@ from .const import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + PAYLOAD_EMPTY_JSON, ) from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper @@ -40,20 +42,28 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "MQTT Update" +CONF_ENTITY_PICTURE = "entity_picture" CONF_LATEST_VERSION_TEMPLATE = "latest_version_template" CONF_LATEST_VERSION_TOPIC = "latest_version_topic" CONF_PAYLOAD_INSTALL = "payload_install" +CONF_RELEASE_SUMMARY = "release_summary" +CONF_RELEASE_URL = "release_url" +CONF_TITLE = "title" PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( { - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_ENTITY_PICTURE): cv.string, vol.Optional(CONF_LATEST_VERSION_TEMPLATE): cv.template, - vol.Required(CONF_LATEST_VERSION_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_LATEST_VERSION_TOPIC): valid_subscribe_topic, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_INSTALL): cv.string, + vol.Optional(CONF_RELEASE_SUMMARY): cv.string, + vol.Optional(CONF_RELEASE_URL): cv.string, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_TITLE): cv.string, }, ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) @@ -99,10 +109,22 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity): """Initialize the MQTT update.""" self._config = config self._attr_device_class = self._config.get(CONF_DEVICE_CLASS) + self._attr_release_summary = self._config.get(CONF_RELEASE_SUMMARY) + self._attr_release_url = self._config.get(CONF_RELEASE_URL) + self._attr_title = self._config.get(CONF_TITLE) + self._entity_picture: str | None = self._config.get(CONF_ENTITY_PICTURE) UpdateEntity.__init__(self) MqttEntity.__init__(self, hass, config, config_entry, discovery_data) + @property + def entity_picture(self) -> str | None: + """Return the entity picture to use in the frontend.""" + if self._entity_picture is not None: + return self._entity_picture + + return super().entity_picture + @staticmethod def config_schema() -> vol.Schema: """Return the config schema.""" @@ -138,15 +160,59 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity): @callback @log_messages(self.hass, self.entity_id) - def handle_installed_version_received(msg: ReceiveMessage) -> None: - """Handle receiving installed version via MQTT.""" - installed_version = self._templates[CONF_VALUE_TEMPLATE](msg.payload) + def handle_state_message_received(msg: ReceiveMessage) -> None: + """Handle receiving state message via MQTT.""" + payload = self._templates[CONF_VALUE_TEMPLATE](msg.payload) - if isinstance(installed_version, str) and installed_version != "": - self._attr_installed_version = installed_version + if not payload or payload == PAYLOAD_EMPTY_JSON: + _LOGGER.debug( + "Ignoring empty payload '%s' after rendering for topic %s", + payload, + msg.topic, + ) + return + + json_payload = {} + try: + json_payload = json_loads(payload) + _LOGGER.debug( + "JSON payload detected after processing payload '%s' on topic %s", + json_payload, + msg.topic, + ) + except JSON_DECODE_EXCEPTIONS: + _LOGGER.warning( + "No valid (JSON) payload detected after processing payload '%s' on topic %s", + json_payload, + msg.topic, + ) + json_payload["installed_version"] = payload + + if "installed_version" in json_payload: + self._attr_installed_version = json_payload["installed_version"] get_mqtt_data(self.hass).state_write_requests.write_state_request(self) - add_subscription(topics, CONF_STATE_TOPIC, handle_installed_version_received) + if "latest_version" in json_payload: + self._attr_latest_version = json_payload["latest_version"] + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + + if CONF_TITLE in json_payload and not self._attr_title: + self._attr_title = json_payload[CONF_TITLE] + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + + if CONF_RELEASE_SUMMARY in json_payload and not self._attr_release_summary: + self._attr_release_summary = json_payload[CONF_RELEASE_SUMMARY] + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + + if CONF_RELEASE_URL in json_payload and not self._attr_release_url: + self._attr_release_url = json_payload[CONF_RELEASE_URL] + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + + if CONF_ENTITY_PICTURE in json_payload and not self._entity_picture: + self._entity_picture = json_payload[CONF_ENTITY_PICTURE] + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + + add_subscription(topics, CONF_STATE_TOPIC, handle_state_message_received) @callback @log_messages(self.hass, self.entity_id) diff --git a/tests/components/mqtt/test_update.py b/tests/components/mqtt/test_update.py index 9b008f093d0..e7d75ee7cc8 100644 --- a/tests/components/mqtt/test_update.py +++ b/tests/components/mqtt/test_update.py @@ -6,7 +6,13 @@ import pytest from homeassistant.components import mqtt, update from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, + Platform, +) from homeassistant.setup import async_setup_component from .test_common import ( @@ -68,6 +74,10 @@ async def test_run_update_setup(hass, mqtt_mock_entry_with_yaml_config): "state_topic": installed_version_topic, "latest_version_topic": latest_version_topic, "name": "Test Update", + "release_summary": "Test release summary", + "release_url": "https://example.com/release", + "title": "Test Update Title", + "entity_picture": "https://example.com/icon.png", } } }, @@ -84,6 +94,10 @@ async def test_run_update_setup(hass, mqtt_mock_entry_with_yaml_config): assert state.state == STATE_OFF assert state.attributes.get("installed_version") == "1.9.0" assert state.attributes.get("latest_version") == "1.9.0" + assert state.attributes.get("release_summary") == "Test release summary" + assert state.attributes.get("release_url") == "https://example.com/release" + assert state.attributes.get("title") == "Test Update Title" + assert state.attributes.get("entity_picture") == "https://example.com/icon.png" async_fire_mqtt_message(hass, latest_version_topic, "2.0.0") @@ -126,6 +140,10 @@ async def test_value_template(hass, mqtt_mock_entry_with_yaml_config): assert state.state == STATE_OFF assert state.attributes.get("installed_version") == "1.9.0" assert state.attributes.get("latest_version") == "1.9.0" + assert ( + state.attributes.get("entity_picture") + == "https://brands.home-assistant.io/_/mqtt/icon.png" + ) async_fire_mqtt_message(hass, latest_version_topic, '{"latest":"2.0.0"}') @@ -137,6 +155,120 @@ async def test_value_template(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("latest_version") == "2.0.0" +async def test_empty_json_state_message(hass, mqtt_mock_entry_with_yaml_config): + """Test an empty JSON payload.""" + state_topic = "test/state-topic" + await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + update.DOMAIN: { + "state_topic": state_topic, + "name": "Test Update", + } + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + async_fire_mqtt_message(hass, state_topic, "{}") + + await hass.async_block_till_done() + + state = hass.states.get("update.test_update") + assert state.state == STATE_UNKNOWN + + +async def test_json_state_message(hass, mqtt_mock_entry_with_yaml_config): + """Test whether it fetches data from a JSON payload.""" + state_topic = "test/state-topic" + await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + update.DOMAIN: { + "state_topic": state_topic, + "name": "Test Update", + } + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + async_fire_mqtt_message( + hass, + state_topic, + '{"installed_version":"1.9.0","latest_version":"1.9.0",' + '"title":"Test Update Title","release_url":"https://example.com/release",' + '"release_summary":"Test release summary"}', + ) + + await hass.async_block_till_done() + + state = hass.states.get("update.test_update") + assert state.state == STATE_OFF + assert state.attributes.get("installed_version") == "1.9.0" + assert state.attributes.get("latest_version") == "1.9.0" + assert state.attributes.get("release_summary") == "Test release summary" + assert state.attributes.get("release_url") == "https://example.com/release" + assert state.attributes.get("title") == "Test Update Title" + + async_fire_mqtt_message( + hass, + state_topic, + '{"installed_version":"1.9.0","latest_version":"2.0.0","title":"Test Update Title"}', + ) + + await hass.async_block_till_done() + + state = hass.states.get("update.test_update") + assert state.state == STATE_ON + assert state.attributes.get("installed_version") == "1.9.0" + assert state.attributes.get("latest_version") == "2.0.0" + + +async def test_json_state_message_with_template(hass, mqtt_mock_entry_with_yaml_config): + """Test whether it fetches data from a JSON payload with template.""" + state_topic = "test/state-topic" + await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + update.DOMAIN: { + "state_topic": state_topic, + "value_template": '{{ {"installed_version": value_json.installed, "latest_version": value_json.latest} | to_json }}', + "name": "Test Update", + } + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + async_fire_mqtt_message(hass, state_topic, '{"installed":"1.9.0","latest":"1.9.0"}') + + await hass.async_block_till_done() + + state = hass.states.get("update.test_update") + assert state.state == STATE_OFF + assert state.attributes.get("installed_version") == "1.9.0" + assert state.attributes.get("latest_version") == "1.9.0" + + async_fire_mqtt_message(hass, state_topic, '{"installed":"1.9.0","latest":"2.0.0"}') + + await hass.async_block_till_done() + + state = hass.states.get("update.test_update") + assert state.state == STATE_ON + assert state.attributes.get("installed_version") == "1.9.0" + assert state.attributes.get("latest_version") == "2.0.0" + + async def test_run_install_service(hass, mqtt_mock_entry_with_yaml_config): """Test that install service works.""" installed_version_topic = "test/installed-version" From c2c57712d2427aa77449e54e79cae8fd712a63ab Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Tue, 1 Nov 2022 13:51:20 +0100 Subject: [PATCH 960/985] Tuya configuration for `tuya_manufacturer` cluster (#81311) * Tuya configuration for tuya_manufacturer cluster * fix codespell * Add attributes initialization * Fix pylint complaints --- .../zha/core/channels/manufacturerspecific.py | 35 +++++++++++ .../components/zha/core/registries.py | 1 + homeassistant/components/zha/select.py | 59 +++++++++++++++++++ 3 files changed, 95 insertions(+) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 5139854d66a..814e7700d01 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -59,6 +59,41 @@ class PhillipsRemote(ZigbeeChannel): REPORT_CONFIG = () +@registries.CHANNEL_ONLY_CLUSTERS.register(registries.TUYA_MANUFACTURER_CLUSTER) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(registries.TUYA_MANUFACTURER_CLUSTER) +class TuyaChannel(ZigbeeChannel): + """Channel for the Tuya manufacturer Zigbee cluster.""" + + REPORT_CONFIG = () + + def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: + """Initialize TuyaChannel.""" + super().__init__(cluster, ch_pool) + + if self.cluster.endpoint.manufacturer in ( + "_TZE200_7tdtqgwv", + "_TZE200_amp6tsvy", + "_TZE200_oisqyl4o", + "_TZE200_vhy3iakz", + "_TZ3000_uim07oem", + "_TZE200_wfxuhoea", + "_TZE200_tviaymwx", + "_TZE200_g1ib5ldv", + "_TZE200_wunufsil", + "_TZE200_7deq70b8", + "_TZE200_tz32mtza", + "_TZE200_2hf7x9n3", + "_TZE200_aqnazj70", + "_TZE200_1ozguk6x", + "_TZE200_k6jhsr0q", + "_TZE200_9mahtqtg", + ): + self.ZCL_INIT_ATTRS = { # pylint: disable=invalid-name + "backlight_mode": True, + "power_on_state": True, + } + + @registries.CHANNEL_ONLY_CLUSTERS.register(0xFCC0) @registries.ZIGBEE_CHANNEL_REGISTRY.register(0xFCC0) class OppleRemote(ZigbeeChannel): diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 2480cf1cd43..42f6bb55f51 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -33,6 +33,7 @@ PHILLIPS_REMOTE_CLUSTER = 0xFC00 SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45 +TUYA_MANUFACTURER_CLUSTER = 0xEF00 VOC_LEVEL_CLUSTER = 0x042E REMOTE_DEVICE_TYPES = { diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 38f2f417643..5ac0ec6d164 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -240,6 +240,27 @@ class TuyaPowerOnState(types.enum8): channel_names=CHANNEL_ON_OFF, models={"TS011F", "TS0121", "TS0001", "TS0002", "TS0003", "TS0004"}, ) +@CONFIG_DIAGNOSTIC_MATCH( + channel_names="tuya_manufacturer", + manufacturers={ + "_TZE200_7tdtqgwv", + "_TZE200_amp6tsvy", + "_TZE200_oisqyl4o", + "_TZE200_vhy3iakz", + "_TZ3000_uim07oem", + "_TZE200_wfxuhoea", + "_TZE200_tviaymwx", + "_TZE200_g1ib5ldv", + "_TZE200_wunufsil", + "_TZE200_7deq70b8", + "_TZE200_tz32mtza", + "_TZE200_2hf7x9n3", + "_TZE200_aqnazj70", + "_TZE200_1ozguk6x", + "_TZE200_k6jhsr0q", + "_TZE200_9mahtqtg", + }, +) class TuyaPowerOnStateSelectEntity(ZCLEnumSelectEntity, id_suffix="power_on_state"): """Representation of a ZHA power on state select entity.""" @@ -248,6 +269,44 @@ class TuyaPowerOnStateSelectEntity(ZCLEnumSelectEntity, id_suffix="power_on_stat _attr_name = "Power on state" +class MoesBacklightMode(types.enum8): + """MOES switch backlight mode enum.""" + + Off = 0x00 + LightWhenOn = 0x01 + LightWhenOff = 0x02 + Freeze = 0x03 + + +@CONFIG_DIAGNOSTIC_MATCH( + channel_names="tuya_manufacturer", + manufacturers={ + "_TZE200_7tdtqgwv", + "_TZE200_amp6tsvy", + "_TZE200_oisqyl4o", + "_TZE200_vhy3iakz", + "_TZ3000_uim07oem", + "_TZE200_wfxuhoea", + "_TZE200_tviaymwx", + "_TZE200_g1ib5ldv", + "_TZE200_wunufsil", + "_TZE200_7deq70b8", + "_TZE200_tz32mtza", + "_TZE200_2hf7x9n3", + "_TZE200_aqnazj70", + "_TZE200_1ozguk6x", + "_TZE200_k6jhsr0q", + "_TZE200_9mahtqtg", + }, +) +class MoesBacklightModeSelectEntity(ZCLEnumSelectEntity, id_suffix="backlight_mode"): + """Moes devices have a different backlight mode select options.""" + + _select_attr = "backlight_mode" + _enum = MoesBacklightMode + _attr_name = "Backlight mode" + + class AqaraMotionSensitivities(types.enum8): """Aqara motion sensitivities.""" From 1cc85f77e30e7e87e17fb2e78229c71146ea6820 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Tue, 1 Nov 2022 10:10:30 +0100 Subject: [PATCH 961/985] Add task id attribute to fireservicerota sensor (#81323) --- homeassistant/components/fireservicerota/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index 36455da9fb7..1484ff7f154 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -79,6 +79,7 @@ class IncidentsSensor(RestoreEntity, SensorEntity): "type", "responder_mode", "can_respond_until", + "task_ids", ): if data.get(value): attr[value] = data[value] From f9493bc313d987302bb27664bdccab49aeb798bc Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 1 Nov 2022 02:08:36 -0700 Subject: [PATCH 962/985] Bump gcal_sync to 2.2.2 and fix recurring event bug (#81339) * Bump gcal_sync to 2.2.2 and fix recurring event bug * Bump to 2.2.2 --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index ce95e3112ee..9a184bdd636 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", - "requirements": ["gcal-sync==2.2.0", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==2.2.2", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 662bc2b5075..e233d757e8a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -725,7 +725,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==2.2.0 +gcal-sync==2.2.2 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be8b850e6b4..6866672df14 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -541,7 +541,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==2.2.0 +gcal-sync==2.2.2 # homeassistant.components.geocaching geocachingapi==0.2.1 From 473490aee773c0ba1c3089e614bf6747bd945a08 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 1 Nov 2022 12:53:44 +0200 Subject: [PATCH 963/985] Bump aioshelly to 4.1.2 (#81342) --- 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 07499ce1e9d..70970e73e30 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==4.1.1"], + "requirements": ["aioshelly==4.1.2"], "dependencies": ["http"], "zeroconf": [ { diff --git a/requirements_all.txt b/requirements_all.txt index e233d757e8a..b48643758e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -255,7 +255,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==4.1.1 +aioshelly==4.1.2 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6866672df14..bf716af6b6e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -230,7 +230,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==4.1.1 +aioshelly==4.1.2 # homeassistant.components.skybell aioskybell==22.7.0 From c4bb225060085fb0e2732647b13ab4bffb9e523e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Nov 2022 10:17:01 +0100 Subject: [PATCH 964/985] Fix power/energy mixup in Youless (#81345) --- homeassistant/components/youless/sensor.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/youless/sensor.py b/homeassistant/components/youless/sensor.py index 19e9c635dce..53ffb223939 100644 --- a/homeassistant/components/youless/sensor.py +++ b/homeassistant/components/youless/sensor.py @@ -39,13 +39,13 @@ async def async_setup_entry( async_add_entities( [ GasSensor(coordinator, device), - PowerMeterSensor( + EnergyMeterSensor( coordinator, device, "low", SensorStateClass.TOTAL_INCREASING ), - PowerMeterSensor( + EnergyMeterSensor( coordinator, device, "high", SensorStateClass.TOTAL_INCREASING ), - PowerMeterSensor(coordinator, device, "total", SensorStateClass.TOTAL), + EnergyMeterSensor(coordinator, device, "total", SensorStateClass.TOTAL), CurrentPowerSensor(coordinator, device), DeliveryMeterSensor(coordinator, device, "low"), DeliveryMeterSensor(coordinator, device, "high"), @@ -68,10 +68,6 @@ class YoulessBaseSensor(CoordinatorEntity, SensorEntity): ) -> None: """Create the sensor.""" super().__init__(coordinator) - self._device = device - self._device_group = device_group - self._sensor_id = sensor_id - self._attr_unique_id = f"{DOMAIN}_{device}_{sensor_id}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"{device}_{device_group}")}, @@ -149,10 +145,10 @@ class DeliveryMeterSensor(YoulessBaseSensor): ) -> None: """Instantiate a delivery meter sensor.""" super().__init__( - coordinator, device, "delivery", "Power delivery", f"delivery_{dev_type}" + coordinator, device, "delivery", "Energy delivery", f"delivery_{dev_type}" ) self._type = dev_type - self._attr_name = f"Power delivery {dev_type}" + self._attr_name = f"Energy delivery {dev_type}" @property def get_sensor(self) -> YoulessSensor | None: @@ -163,7 +159,7 @@ class DeliveryMeterSensor(YoulessBaseSensor): return getattr(self.coordinator.data.delivery_meter, f"_{self._type}", None) -class PowerMeterSensor(YoulessBaseSensor): +class EnergyMeterSensor(YoulessBaseSensor): """The Youless low meter value sensor.""" _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR @@ -177,13 +173,13 @@ class PowerMeterSensor(YoulessBaseSensor): dev_type: str, state_class: SensorStateClass, ) -> None: - """Instantiate a power meter sensor.""" + """Instantiate a energy meter sensor.""" super().__init__( - coordinator, device, "power", "Power usage", f"power_{dev_type}" + coordinator, device, "power", "Energy usage", f"power_{dev_type}" ) self._device = device self._type = dev_type - self._attr_name = f"Power {dev_type}" + self._attr_name = f"Energy {dev_type}" self._attr_state_class = state_class @property From a2d432dfd65e7404de4e868c1b75f0c608cbbfa2 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 1 Nov 2022 16:25:01 +0100 Subject: [PATCH 965/985] Revert "Do not write state if payload is `''`" for MQTT sensor (#81347) * Revert "Do not write state if payload is ''" This reverts commit 869c11884e2b06d5f5cb5a8a4f78247a6972149e. * Add test --- homeassistant/components/mqtt/sensor.py | 4 ++-- tests/components/mqtt/test_sensor.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index d95d669e72f..52ba1a7e3c2 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -271,8 +271,8 @@ class MqttSensor(MqttEntity, RestoreSensor): ) elif self.device_class == SensorDeviceClass.DATE: payload = payload.date() - if payload != "": - self._state = payload + + self._state = payload def _update_last_reset(msg): payload = self._last_reset_template(msg.payload) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 6cfaa9678bb..1884d04efc3 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -313,6 +313,12 @@ async def test_setting_sensor_value_via_mqtt_json_message( assert state.state == "100" + # Make sure the state is written when a sensor value is reset to '' + async_fire_mqtt_message(hass, "test-topic", '{ "val": "" }') + state = hass.states.get("sensor.test") + + assert state.state == "" + async def test_setting_sensor_value_via_mqtt_json_message_and_default_current_state( hass, mqtt_mock_entry_with_yaml_config From f265c160d17ff435796f35c769570beac8b13969 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 1 Nov 2022 15:57:48 +0100 Subject: [PATCH 966/985] Lower log level for non-JSON payload in MQTT update (#81348) Change log level --- homeassistant/components/mqtt/update.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index 986ad013520..5536d16d1c7 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -181,9 +181,9 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity): msg.topic, ) except JSON_DECODE_EXCEPTIONS: - _LOGGER.warning( + _LOGGER.debug( "No valid (JSON) payload detected after processing payload '%s' on topic %s", - json_payload, + payload, msg.topic, ) json_payload["installed_version"] = payload From d0ddbb5f5807b798434d5441a4dc693583c4424e Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 1 Nov 2022 09:06:55 -0400 Subject: [PATCH 967/985] Fix individual LED range for ZHA device action (#81351) The inovelli individual LED effect device action can address 7 LEDs. I had set the range 1-7 but it should be 0-6. --- homeassistant/components/zha/device_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 1cb988b1c15..3e2a3591c80 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -93,7 +93,7 @@ DEVICE_ACTION_SCHEMAS = { ), INOVELLI_INDIVIDUAL_LED_EFFECT: vol.Schema( { - vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(1, 7)), + vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)), vol.Required("effect_type"): vol.In( InovelliConfigEntityChannel.LEDEffectType.__members__.keys() ), From 9dff7ab6b96b8c5f78441e7e6865ba57d421ceff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Nov 2022 12:07:42 -0500 Subject: [PATCH 968/985] Adjust time to remove stale connectable devices from the esphome ble to closer match bluez (#81356) --- .../components/esphome/bluetooth/scanner.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/bluetooth/scanner.py b/homeassistant/components/esphome/bluetooth/scanner.py index 7c8064d5583..4fbaf7cabb6 100644 --- a/homeassistant/components/esphome/bluetooth/scanner.py +++ b/homeassistant/components/esphome/bluetooth/scanner.py @@ -6,6 +6,7 @@ import datetime from datetime import timedelta import re import time +from typing import Final from aioesphomeapi import BluetoothLEAdvertisement from bleak.backends.device import BLEDevice @@ -23,6 +24,15 @@ from homeassistant.util.dt import monotonic_time_coarse TWO_CHAR = re.compile("..") +# The maximum time between advertisements for a device to be considered +# stale when the advertisement tracker can determine the interval for +# connectable devices. +# +# BlueZ uses 180 seconds by default but we give it a bit more time +# to account for the esp32's bluetooth stack being a bit slower +# than BlueZ's. +CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS: Final = 195 + class ESPHomeScanner(BaseHaScanner): """Scanner for esphome.""" @@ -45,8 +55,12 @@ class ESPHomeScanner(BaseHaScanner): self._connector = connector self._connectable = connectable self._details: dict[str, str | HaBluetoothConnector] = {"source": scanner_id} + self._fallback_seconds = FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS if connectable: self._details["connector"] = connector + self._fallback_seconds = ( + CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + ) @callback def async_setup(self) -> CALLBACK_TYPE: @@ -61,7 +75,7 @@ class ESPHomeScanner(BaseHaScanner): expired = [ address for address, timestamp in self._discovered_device_timestamps.items() - if now - timestamp > FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + if now - timestamp > self._fallback_seconds ] for address in expired: del self._discovered_device_advertisement_datas[address] From 8c63a9ce5e29de71e3dd3db5816c8a48e8bc4064 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Nov 2022 12:07:03 -0500 Subject: [PATCH 969/985] Immediately prefer advertisements from alternate sources when a scanner goes away (#81357) --- homeassistant/components/bluetooth/manager.py | 5 + tests/components/bluetooth/test_manager.py | 92 +++++++++++++++++-- 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index c3a0e0998f1..d29023acef7 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -127,6 +127,7 @@ class BluetoothManager: self._non_connectable_scanners: list[BaseHaScanner] = [] self._connectable_scanners: list[BaseHaScanner] = [] self._adapters: dict[str, AdapterDetails] = {} + self._sources: set[str] = set() @property def supports_passive_scan(self) -> bool: @@ -379,6 +380,7 @@ class BluetoothManager: if ( (old_service_info := all_history.get(address)) and source != old_service_info.source + and old_service_info.source in self._sources and self._prefer_previous_adv_from_different_source( old_service_info, service_info ) @@ -398,6 +400,7 @@ class BluetoothManager: # the old connectable advertisement or ( source != old_connectable_service_info.source + and old_connectable_service_info.source in self._sources and self._prefer_previous_adv_from_different_source( old_connectable_service_info, service_info ) @@ -597,8 +600,10 @@ class BluetoothManager: def _unregister_scanner() -> None: self._advertisement_tracker.async_remove_source(scanner.source) scanners.remove(scanner) + self._sources.remove(scanner.source) scanners.append(scanner) + self._sources.add(scanner.source) return _unregister_scanner @hass_callback diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index c6a65046ef9..0375f68309f 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -5,11 +5,14 @@ from unittest.mock import AsyncMock, MagicMock, patch from bleak.backends.scanner import BLEDevice from bluetooth_adapters import AdvertisementHistory +import pytest from homeassistant.components import bluetooth +from homeassistant.components.bluetooth import models from homeassistant.components.bluetooth.manager import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, ) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from . import ( @@ -20,8 +23,28 @@ from . import ( ) +@pytest.fixture +def register_hci0_scanner(hass: HomeAssistant) -> None: + """Register an hci0 scanner.""" + cancel = bluetooth.async_register_scanner( + hass, models.BaseHaScanner(hass, "hci0"), True + ) + yield + cancel() + + +@pytest.fixture +def register_hci1_scanner(hass: HomeAssistant) -> None: + """Register an hci1 scanner.""" + cancel = bluetooth.async_register_scanner( + hass, models.BaseHaScanner(hass, "hci1"), True + ) + yield + cancel() + + async def test_advertisements_do_not_switch_adapters_for_no_reason( - hass, enable_bluetooth + hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner ): """Test we only switch adapters when needed.""" @@ -68,7 +91,9 @@ async def test_advertisements_do_not_switch_adapters_for_no_reason( ) -async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth): +async def test_switching_adapters_based_on_rssi( + hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner +): """Test switching adapters based on rssi.""" address = "44:44:33:11:23:45" @@ -122,7 +147,9 @@ async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth): ) -async def test_switching_adapters_based_on_zero_rssi(hass, enable_bluetooth): +async def test_switching_adapters_based_on_zero_rssi( + hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner +): """Test switching adapters based on zero rssi.""" address = "44:44:33:11:23:45" @@ -176,7 +203,9 @@ async def test_switching_adapters_based_on_zero_rssi(hass, enable_bluetooth): ) -async def test_switching_adapters_based_on_stale(hass, enable_bluetooth): +async def test_switching_adapters_based_on_stale( + hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner +): """Test switching adapters based on the previous advertisement being stale.""" address = "44:44:33:11:23:41" @@ -256,7 +285,7 @@ async def test_restore_history_from_dbus(hass, one_adapter): async def test_switching_adapters_based_on_rssi_connectable_to_non_connectable( - hass, enable_bluetooth + hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner ): """Test switching adapters based on rssi from connectable to non connectable.""" @@ -339,7 +368,7 @@ async def test_switching_adapters_based_on_rssi_connectable_to_non_connectable( async def test_connectable_advertisement_can_be_retrieved_with_best_path_is_non_connectable( - hass, enable_bluetooth + hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner ): """Test we can still get a connectable BLEDevice when the best path is non-connectable. @@ -384,3 +413,54 @@ async def test_connectable_advertisement_can_be_retrieved_with_best_path_is_non_ bluetooth.async_ble_device_from_address(hass, address, True) is switchbot_device_poor_signal ) + + +async def test_switching_adapters_when_one_goes_away( + hass, enable_bluetooth, register_hci0_scanner +): + """Test switching adapters when one goes away.""" + cancel_hci2 = bluetooth.async_register_scanner( + hass, models.BaseHaScanner(hass, "hci2"), True + ) + + address = "44:44:33:11:23:45" + + switchbot_device_good_signal = BLEDevice(address, "wohand_good_signal") + switchbot_adv_good_signal = generate_advertisement_data( + local_name="wohand_good_signal", service_uuids=[], rssi=-60 + ) + inject_advertisement_with_source( + hass, switchbot_device_good_signal, switchbot_adv_good_signal, "hci2" + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + switchbot_device_poor_signal = BLEDevice(address, "wohand_poor_signal") + switchbot_adv_poor_signal = generate_advertisement_data( + local_name="wohand_poor_signal", service_uuids=[], rssi=-100 + ) + inject_advertisement_with_source( + hass, switchbot_device_poor_signal, switchbot_adv_poor_signal, "hci0" + ) + + # We want to prefer the good signal when we have options + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + cancel_hci2() + + inject_advertisement_with_source( + hass, switchbot_device_poor_signal, switchbot_adv_poor_signal, "hci0" + ) + + # Now that hci2 is gone, we should prefer the poor signal + # since no poor signal is better than no signal + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_poor_signal + ) From 1efec8323abf6ef1e3896c65ebac76cbd24a3613 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Nov 2022 11:44:58 -0500 Subject: [PATCH 970/985] Bump aiohomekit to 2.2.11 (#81358) --- 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 93aae62daab..6533d7f29be 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.10"], + "requirements": ["aiohomekit==2.2.11"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index b48643758e5..b751dc428ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.10 +aiohomekit==2.2.11 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf716af6b6e..bd19d686118 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.10 +aiohomekit==2.2.11 # homeassistant.components.emulated_hue # homeassistant.components.http From e8f93d9c7f943482bb10692fa255093c36747faa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Nov 2022 13:09:48 -0400 Subject: [PATCH 971/985] Bumped version to 2022.11.0b6 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f547e536ae0..195b52c4deb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 5f2aa4d4311..dd549dfeb01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.0b5" +version = "2022.11.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From b9132e78b48bc9e0356c3784ae6d357e25cd0db3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Nov 2022 19:11:50 +0100 Subject: [PATCH 972/985] Improve error logging of WebSocket API (#81360) --- .../components/websocket_api/connection.py | 12 ++- .../websocket_api/test_connection.py | 92 +++++++++++++++---- 2 files changed, 82 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index c344e1c6a9f..ab4dda845db 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any import voluptuous as vol from homeassistant.auth.models import RefreshToken, User +from homeassistant.components.http import current_request from homeassistant.core import Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized @@ -137,6 +138,13 @@ class ActiveConnection: err_message = "Unknown error" log_handler = self.logger.exception - log_handler("Error handling message: %s (%s)", err_message, code) - self.send_message(messages.error_message(msg["id"], code, err_message)) + + if code: + err_message += f" ({code})" + if request := current_request.get(): + err_message += f" from {request.remote}" + if user_agent := request.headers.get("user-agent"): + err_message += f" ({user_agent})" + + log_handler("Error handling message: %s", err_message) diff --git a/tests/components/websocket_api/test_connection.py b/tests/components/websocket_api/test_connection.py index fd9af99c1a4..8f2cd43fdb8 100644 --- a/tests/components/websocket_api/test_connection.py +++ b/tests/components/websocket_api/test_connection.py @@ -1,8 +1,11 @@ """Test WebSocket Connection class.""" import asyncio import logging -from unittest.mock import Mock +from typing import Any +from unittest.mock import AsyncMock, Mock, patch +from aiohttp.test_utils import make_mocked_request +import pytest import voluptuous as vol from homeassistant import exceptions @@ -11,37 +14,86 @@ from homeassistant.components import websocket_api from tests.common import MockUser -async def test_exception_handling(): - """Test handling of exceptions.""" - send_messages = [] - user = MockUser() - refresh_token = Mock() - conn = websocket_api.ActiveConnection( - logging.getLogger(__name__), None, send_messages.append, user, refresh_token - ) - - for (exc, code, err) in ( - (exceptions.Unauthorized(), websocket_api.ERR_UNAUTHORIZED, "Unauthorized"), +@pytest.mark.parametrize( + "exc,code,err,log", + [ + ( + exceptions.Unauthorized(), + websocket_api.ERR_UNAUTHORIZED, + "Unauthorized", + "Error handling message: Unauthorized (unauthorized) from 127.0.0.42 (Browser)", + ), ( vol.Invalid("Invalid something"), websocket_api.ERR_INVALID_FORMAT, "Invalid something. Got {'id': 5}", + "Error handling message: Invalid something. Got {'id': 5} (invalid_format) from 127.0.0.42 (Browser)", + ), + ( + asyncio.TimeoutError(), + websocket_api.ERR_TIMEOUT, + "Timeout", + "Error handling message: Timeout (timeout) from 127.0.0.42 (Browser)", ), - (asyncio.TimeoutError(), websocket_api.ERR_TIMEOUT, "Timeout"), ( exceptions.HomeAssistantError("Failed to do X"), websocket_api.ERR_UNKNOWN_ERROR, "Failed to do X", + "Error handling message: Failed to do X (unknown_error) from 127.0.0.42 (Browser)", ), - (ValueError("Really bad"), websocket_api.ERR_UNKNOWN_ERROR, "Unknown error"), ( - exceptions.HomeAssistantError(), + ValueError("Really bad"), websocket_api.ERR_UNKNOWN_ERROR, "Unknown error", + "Error handling message: Unknown error (unknown_error) from 127.0.0.42 (Browser)", ), - ): - send_messages.clear() + ( + exceptions.HomeAssistantError, + websocket_api.ERR_UNKNOWN_ERROR, + "Unknown error", + "Error handling message: Unknown error (unknown_error) from 127.0.0.42 (Browser)", + ), + ], +) +async def test_exception_handling( + caplog: pytest.LogCaptureFixture, + exc: Exception, + code: str, + err: str, + log: str, +): + """Test handling of exceptions.""" + send_messages = [] + user = MockUser() + refresh_token = Mock() + current_request = AsyncMock() + + def get_extra_info(key: str) -> Any: + if key == "sslcontext": + return True + + if key == "peername": + return ("127.0.0.42", 8123) + + mocked_transport = Mock() + mocked_transport.get_extra_info = get_extra_info + mocked_request = make_mocked_request( + "GET", + "/api/websocket", + headers={"Host": "example.com", "User-Agent": "Browser"}, + transport=mocked_transport, + ) + + with patch( + "homeassistant.components.websocket_api.connection.current_request", + ) as current_request: + current_request.get.return_value = mocked_request + conn = websocket_api.ActiveConnection( + logging.getLogger(__name__), None, send_messages.append, user, refresh_token + ) + conn.async_handle_exception({"id": 5}, exc) - assert len(send_messages) == 1 - assert send_messages[0]["error"]["code"] == code - assert send_messages[0]["error"]["message"] == err + assert len(send_messages) == 1 + assert send_messages[0]["error"]["code"] == code + assert send_messages[0]["error"]["message"] == err + assert log in caplog.text From 95ce20638a1d91aaad7e6d53df09e0ec6c47e228 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 1 Nov 2022 13:57:53 -0400 Subject: [PATCH 973/985] Bump zigpy-zigate to 0.10.3 (#81363) --- 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 79980d763e7..e40a54c11bc 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -11,7 +11,7 @@ "zigpy-deconz==0.19.0", "zigpy==0.51.5", "zigpy-xbee==0.16.2", - "zigpy-zigate==0.10.2", + "zigpy-zigate==0.10.3", "zigpy-znp==0.9.1" ], "usb": [ diff --git a/requirements_all.txt b/requirements_all.txt index b751dc428ad..bcd49db3368 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2622,7 +2622,7 @@ zigpy-deconz==0.19.0 zigpy-xbee==0.16.2 # homeassistant.components.zha -zigpy-zigate==0.10.2 +zigpy-zigate==0.10.3 # homeassistant.components.zha zigpy-znp==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd19d686118..b1ff07c4c02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1817,7 +1817,7 @@ zigpy-deconz==0.19.0 zigpy-xbee==0.16.2 # homeassistant.components.zha -zigpy-zigate==0.10.2 +zigpy-zigate==0.10.3 # homeassistant.components.zha zigpy-znp==0.9.1 From 0dbf0504ffc3b502ab151b32a8e788a930cf2a81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Nov 2022 15:49:05 -0500 Subject: [PATCH 974/985] Bump bleak-retry-connector to 2.8.2 (#81370) * Bump bleak-retry-connector to 2.8.2 Tweaks for the esp32 proxies now that we have better error reporting. This change improves the retry cases a bit with the new https://github.com/esphome/esphome/pull/3971 * empty --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index bca2f7f9a8d..9c2438b8b18 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.1", - "bleak-retry-connector==2.8.1", + "bleak-retry-connector==2.8.2", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.60.0" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index adff342729d..e57c6ae2158 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.8.1 +bleak-retry-connector==2.8.2 bleak==0.19.1 bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index bcd49db3368..d4d71b0dc90 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.1 +bleak-retry-connector==2.8.2 # homeassistant.components.bluetooth bleak==0.19.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1ff07c4c02..bf93af330f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,7 +337,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.1 +bleak-retry-connector==2.8.2 # homeassistant.components.bluetooth bleak==0.19.1 From a5f209b219bea2b9c20cede33b7515fd3cc7733e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Nov 2022 17:00:04 -0500 Subject: [PATCH 975/985] Bump aiohomekit to 2.2.12 (#81372) * Bump aiohomekit to 2.2.12 Fixes a missing lock which was noticable on the esp32s since they disconnect right away when you ask for gatt notify. https://github.com/Jc2k/aiohomekit/compare/2.2.11...2.2.12 * empty --- 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 6533d7f29be..09f2a15871f 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.11"], + "requirements": ["aiohomekit==2.2.12"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index d4d71b0dc90..153675f0a0f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.11 +aiohomekit==2.2.12 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf93af330f6..24820de0b49 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.11 +aiohomekit==2.2.12 # homeassistant.components.emulated_hue # homeassistant.components.http From 3aca3763741443aca8ff4b6a534b4b5b17d7e47d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 2 Nov 2022 07:18:50 -0400 Subject: [PATCH 976/985] Add unit conversion for energy costs (#81379) Co-authored-by: Franck Nijhof --- homeassistant/components/energy/sensor.py | 107 +++++++++++++--------- tests/components/energy/test_sensor.py | 24 ++++- 2 files changed, 82 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index c97b67287d1..71e385f2fec 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Callable import copy from dataclasses import dataclass import logging @@ -22,6 +23,7 @@ from homeassistant.const import ( VOLUME_GALLONS, VOLUME_LITERS, UnitOfEnergy, + UnitOfVolume, ) from homeassistant.core import ( HomeAssistant, @@ -34,29 +36,35 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import unit_conversion import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import METRIC_SYSTEM from .const import DOMAIN from .data import EnergyManager, async_get_manager -SUPPORTED_STATE_CLASSES = [ +SUPPORTED_STATE_CLASSES = { SensorStateClass.MEASUREMENT, SensorStateClass.TOTAL, SensorStateClass.TOTAL_INCREASING, -] -VALID_ENERGY_UNITS = [ +} +VALID_ENERGY_UNITS: set[str] = { UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.GIGA_JOULE, -] -VALID_ENERGY_UNITS_GAS = [VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS] + VALID_ENERGY_UNITS -VALID_VOLUME_UNITS_WATER = [ +} +VALID_ENERGY_UNITS_GAS = { + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, + *VALID_ENERGY_UNITS, +} +VALID_VOLUME_UNITS_WATER = { VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, VOLUME_GALLONS, VOLUME_LITERS, -] +} _LOGGER = logging.getLogger(__name__) @@ -252,8 +260,24 @@ class EnergyCostSensor(SensorEntity): self.async_write_ha_state() @callback - def _update_cost(self) -> None: # noqa: C901 + def _update_cost(self) -> None: """Update incurred costs.""" + if self._adapter.source_type == "grid": + valid_units = VALID_ENERGY_UNITS + default_price_unit: str | None = UnitOfEnergy.KILO_WATT_HOUR + + elif self._adapter.source_type == "gas": + valid_units = VALID_ENERGY_UNITS_GAS + # No conversion for gas. + default_price_unit = None + + elif self._adapter.source_type == "water": + valid_units = VALID_VOLUME_UNITS_WATER + if self.hass.config.units is METRIC_SYSTEM: + default_price_unit = UnitOfVolume.CUBIC_METERS + else: + default_price_unit = UnitOfVolume.GALLONS + energy_state = self.hass.states.get( cast(str, self._config[self._adapter.stat_energy_key]) ) @@ -298,52 +322,27 @@ class EnergyCostSensor(SensorEntity): except ValueError: return - if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith( - f"/{UnitOfEnergy.WATT_HOUR}" - ): - energy_price *= 1000.0 + energy_price_unit: str | None = energy_price_state.attributes.get( + ATTR_UNIT_OF_MEASUREMENT, "" + ).partition("/")[2] - if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith( - f"/{UnitOfEnergy.MEGA_WATT_HOUR}" - ): - energy_price /= 1000.0 - - if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith( - f"/{UnitOfEnergy.GIGA_JOULE}" - ): - energy_price /= 1000 / 3.6 + # For backwards compatibility we don't validate the unit of the price + # If it is not valid, we assume it's our default price unit. + if energy_price_unit not in valid_units: + energy_price_unit = default_price_unit else: - energy_price_state = None energy_price = cast(float, self._config["number_energy_price"]) + energy_price_unit = default_price_unit if self._last_energy_sensor_state is None: # Initialize as it's the first time all required entities are in place. self._reset(energy_state) return - energy_unit = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + energy_unit: str | None = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if self._adapter.source_type == "grid": - if energy_unit not in VALID_ENERGY_UNITS: - energy_unit = None - - elif self._adapter.source_type == "gas": - if energy_unit not in VALID_ENERGY_UNITS_GAS: - energy_unit = None - - elif self._adapter.source_type == "water": - if energy_unit not in VALID_VOLUME_UNITS_WATER: - energy_unit = None - - if energy_unit == UnitOfEnergy.WATT_HOUR: - energy_price /= 1000 - elif energy_unit == UnitOfEnergy.MEGA_WATT_HOUR: - energy_price *= 1000 - elif energy_unit == UnitOfEnergy.GIGA_JOULE: - energy_price *= 1000 / 3.6 - - if energy_unit is None: + if energy_unit is None or energy_unit not in valid_units: if not self._wrong_unit_reported: self._wrong_unit_reported = True _LOGGER.warning( @@ -373,10 +372,30 @@ class EnergyCostSensor(SensorEntity): energy_state_copy = copy.copy(energy_state) energy_state_copy.state = "0.0" self._reset(energy_state_copy) + # Update with newly incurred cost old_energy_value = float(self._last_energy_sensor_state.state) cur_value = cast(float, self._attr_native_value) - self._attr_native_value = cur_value + (energy - old_energy_value) * energy_price + + if energy_price_unit is None: + converted_energy_price = energy_price + else: + if self._adapter.source_type == "grid": + converter: Callable[ + [float, str, str], float + ] = unit_conversion.EnergyConverter.convert + elif self._adapter.source_type in ("gas", "water"): + converter = unit_conversion.VolumeConverter.convert + + converted_energy_price = converter( + energy_price, + energy_unit, + energy_price_unit, + ) + + self._attr_native_value = ( + cur_value + (energy - old_energy_value) * converted_energy_price + ) self._last_energy_sensor_state = energy_state diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 14a04ea74c6..0108dd1de76 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -19,11 +19,13 @@ from homeassistant.const import ( STATE_UNKNOWN, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, + VOLUME_GALLONS, UnitOfEnergy, ) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM from tests.components.recorder.common import async_wait_recording_done @@ -832,7 +834,10 @@ async def test_cost_sensor_handle_price_units( assert state.state == "20.0" -@pytest.mark.parametrize("unit", (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS)) +@pytest.mark.parametrize( + "unit", + (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS), +) async def test_cost_sensor_handle_gas( setup_integration, hass, hass_storage, unit ) -> None: @@ -933,13 +938,22 @@ async def test_cost_sensor_handle_gas_kwh( assert state.state == "50.0" -@pytest.mark.parametrize("unit", (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS)) +@pytest.mark.parametrize( + "unit_system,usage_unit,growth", + ( + # 1 cubic foot = 7.47 gl, 100 ft3 growth @ 0.5/ft3: + (US_CUSTOMARY_SYSTEM, VOLUME_CUBIC_FEET, 374.025974025974), + (US_CUSTOMARY_SYSTEM, VOLUME_GALLONS, 50.0), + (METRIC_SYSTEM, VOLUME_CUBIC_METERS, 50.0), + ), +) async def test_cost_sensor_handle_water( - setup_integration, hass, hass_storage, unit + setup_integration, hass, hass_storage, unit_system, usage_unit, growth ) -> None: """Test water cost price from sensor entity.""" + hass.config.units = unit_system energy_attributes = { - ATTR_UNIT_OF_MEASUREMENT: unit, + ATTR_UNIT_OF_MEASUREMENT: usage_unit, ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, } energy_data = data.EnergyManager.default_preferences() @@ -981,7 +995,7 @@ async def test_cost_sensor_handle_water( await hass.async_block_till_done() state = hass.states.get("sensor.water_consumption_cost") - assert state.state == "50.0" + assert float(state.state) == pytest.approx(growth) @pytest.mark.parametrize("state_class", [None]) From 9f54e332ec68a348a00cb82c0759b1cd3b64aa25 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Nov 2022 19:52:13 -0500 Subject: [PATCH 977/985] Bump dbus-fast to 1.61.1 (#81386) --- homeassistant/components/bluetooth/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/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 9c2438b8b18..89281323541 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.8.2", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.60.0" + "dbus-fast==1.61.1" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e57c6ae2158..c29364f2466 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.60.0 +dbus-fast==1.61.1 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 153675f0a0f..bf2de8bf272 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.60.0 +dbus-fast==1.61.1 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24820de0b49..1eae9fc9ee3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.60.0 +dbus-fast==1.61.1 # homeassistant.components.debugpy debugpy==1.6.3 From f6c094b017cdb4a8e8f0c164adfad63ee7df5552 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Tue, 1 Nov 2022 21:29:11 -0400 Subject: [PATCH 978/985] Improve supervisor repairs (#81387) Co-authored-by: Paulus Schoutsen --- homeassistant/components/hassio/repairs.py | 59 +++++++++- homeassistant/components/hassio/strings.json | 104 +++++++++++++++++- .../components/hassio/translations/en.json | 104 +++++++++++++++++- tests/components/hassio/test_repairs.py | 77 ++++++++++++- 4 files changed, 330 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/hassio/repairs.py b/homeassistant/components/hassio/repairs.py index a8c6788f4d5..21120d8d522 100644 --- a/homeassistant/components/hassio/repairs.py +++ b/homeassistant/components/hassio/repairs.py @@ -36,6 +36,39 @@ ISSUE_ID_UNSUPPORTED = "unsupported_system" INFO_URL_UNHEALTHY = "https://www.home-assistant.io/more-info/unhealthy" INFO_URL_UNSUPPORTED = "https://www.home-assistant.io/more-info/unsupported" +UNSUPPORTED_REASONS = { + "apparmor", + "connectivity_check", + "content_trust", + "dbus", + "dns_server", + "docker_configuration", + "docker_version", + "cgroup_version", + "job_conditions", + "lxc", + "network_manager", + "os", + "os_agent", + "restart_policy", + "software", + "source_mods", + "supervisor_version", + "systemd", + "systemd_journal", + "systemd_resolved", +} +# Some unsupported reasons also mark the system as unhealthy. If the unsupported reason +# provides no additional information beyond the unhealthy one then skip that repair. +UNSUPPORTED_SKIP_REPAIR = {"privileged"} +UNHEALTHY_REASONS = { + "docker", + "supervisor", + "setup", + "privileged", + "untrusted", +} + class SupervisorRepairs: """Create repairs from supervisor events.""" @@ -56,6 +89,13 @@ class SupervisorRepairs: def unhealthy_reasons(self, reasons: set[str]) -> None: """Set unhealthy reasons. Create or delete repairs as necessary.""" for unhealthy in reasons - self.unhealthy_reasons: + if unhealthy in UNHEALTHY_REASONS: + translation_key = f"unhealthy_{unhealthy}" + translation_placeholders = None + else: + translation_key = "unhealthy" + translation_placeholders = {"reason": unhealthy} + async_create_issue( self._hass, DOMAIN, @@ -63,8 +103,8 @@ class SupervisorRepairs: is_fixable=False, learn_more_url=f"{INFO_URL_UNHEALTHY}/{unhealthy}", severity=IssueSeverity.CRITICAL, - translation_key="unhealthy", - translation_placeholders={"reason": unhealthy}, + translation_key=translation_key, + translation_placeholders=translation_placeholders, ) for fixed in self.unhealthy_reasons - reasons: @@ -80,7 +120,14 @@ class SupervisorRepairs: @unsupported_reasons.setter def unsupported_reasons(self, reasons: set[str]) -> None: """Set unsupported reasons. Create or delete repairs as necessary.""" - for unsupported in reasons - self.unsupported_reasons: + for unsupported in reasons - UNSUPPORTED_SKIP_REPAIR - self.unsupported_reasons: + if unsupported in UNSUPPORTED_REASONS: + translation_key = f"unsupported_{unsupported}" + translation_placeholders = None + else: + translation_key = "unsupported" + translation_placeholders = {"reason": unsupported} + async_create_issue( self._hass, DOMAIN, @@ -88,11 +135,11 @@ class SupervisorRepairs: is_fixable=False, learn_more_url=f"{INFO_URL_UNSUPPORTED}/{unsupported}", severity=IssueSeverity.WARNING, - translation_key="unsupported", - translation_placeholders={"reason": unsupported}, + translation_key=translation_key, + translation_placeholders=translation_placeholders, ) - for fixed in self.unsupported_reasons - reasons: + for fixed in self.unsupported_reasons - (reasons - UNSUPPORTED_SKIP_REPAIR): async_delete_issue(self._hass, DOMAIN, f"{ISSUE_ID_UNSUPPORTED}_{fixed}") self._unsupported_reasons = reasons diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 81b5ce01b79..7cda053f43a 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -19,11 +19,111 @@ "issues": { "unhealthy": { "title": "Unhealthy system - {reason}", - "description": "System is currently unhealthy due to '{reason}'. Use the link to learn more about what is wrong and how to fix it." + "description": "System is currently unhealthy due to {reason}. Use the link to learn more and how to fix this." + }, + "unhealthy_docker": { + "title": "Unhealthy system - Docker misconfigured", + "description": "System is currently unhealthy because Docker is configured incorrectly. Use the link to learn more and how to fix this." + }, + "unhealthy_supervisor": { + "title": "Unhealthy system - Supervisor update failed", + "description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. Use the link to learn more and how to fix this." + }, + "unhealthy_setup": { + "title": "Unhealthy system - Setup failed", + "description": "System is currently unhealthy because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this." + }, + "unhealthy_privileged": { + "title": "Unhealthy system - Not privileged", + "description": "System is currently unhealthy because it does not have privileged access to the docker runtime. Use the link to learn more and how to fix this." + }, + "unhealthy_untrusted": { + "title": "Unhealthy system - Untrusted code", + "description": "System is currently unhealthy because it has detected untrusted code or images in use. Use the link to learn more and how to fix this." }, "unsupported": { "title": "Unsupported system - {reason}", - "description": "System is unsupported due to '{reason}'. Use the link to learn more about what this means and how to return to a supported system." + "description": "System is unsupported due to {reason}. Use the link to learn more and how to fix this." + }, + "unsupported_apparmor": { + "title": "Unsupported system - AppArmor issues", + "description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. Use the link to learn more and how to fix this." + }, + "unsupported_cgroup_version": { + "title": "Unsupported system - CGroup version", + "description": "System is unsupported because the wrong version of Docker CGroup is in use. Use the link to learn the correct version and how to fix this." + }, + "unsupported_connectivity_check": { + "title": "Unsupported system - Connectivity check disabled", + "description": "System is unsupported because Home Assistant cannot determine when an internet connection is available. Use the link to learn more and how to fix this." + }, + "unsupported_content_trust": { + "title": "Unsupported system - Content-trust check disabled", + "description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. Use the link to learn more and how to fix this." + }, + "unsupported_dbus": { + "title": "Unsupported system - D-Bus issues", + "description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. Use the link to learn more and how to fix this." + }, + "unsupported_dns_server": { + "title": "Unsupported system - DNS server issues", + "description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. Use the link to learn more and how to fix this." + }, + "unsupported_docker_configuration": { + "title": "Unsupported system - Docker misconfigured", + "description": "System is unsupported because the Docker daemon is running in an unexpected way. Use the link to learn more and how to fix this." + }, + "unsupported_docker_version": { + "title": "Unsupported system - Docker version", + "description": "System is unsupported because the wrong version of Docker is in use. Use the link to learn the correct version and how to fix this." + }, + "unsupported_job_conditions": { + "title": "Unsupported system - Protections disabled", + "description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. Use the link to learn more and how to fix this." + }, + "unsupported_lxc": { + "title": "Unsupported system - LXC detected", + "description": "System is unsupported because it is being run in an LXC virtual machine. Use the link to learn more and how to fix this." + }, + "unsupported_network_manager": { + "title": "Unsupported system - Network Manager issues", + "description": "System is unsupported because Network Manager is missing, inactive or misconfigured. Use the link to learn more and how to fix this." + }, + "unsupported_os": { + "title": "Unsupported system - Operating System", + "description": "System is unsupported because the operating system in use is not tested or maintained for use with Supervisor. Use the link to which operating systems are supported and how to fix this." + }, + "unsupported_os_agent": { + "title": "Unsupported system - OS-Agent issues", + "description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. Use the link to learn more and how to fix this." + }, + "unsupported_restart_policy": { + "title": "Unsupported system - Container restart policy", + "description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. Use the link to learn more and how to fix this." + }, + "unsupported_software": { + "title": "Unsupported system - Unsupported software", + "description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. Use the link to learn more and how to fix this." + }, + "unsupported_source_mods": { + "title": "Unsupported system - Supervisor source modifications", + "description": "System is unsupported because Supervisor source code has been modified. Use the link to learn more and how to fix this." + }, + "unsupported_supervisor_version": { + "title": "Unsupported system - Supervisor version", + "description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. Use the link to learn more and how to fix this." + }, + "unsupported_systemd": { + "title": "Unsupported system - Systemd issues", + "description": "System is unsupported because Systemd is missing, inactive or misconfigured. Use the link to learn more and how to fix this." + }, + "unsupported_systemd_journal": { + "title": "Unsupported system - Systemd Journal issues", + "description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured . Use the link to learn more and how to fix this." + }, + "unsupported_systemd_resolved": { + "title": "Unsupported system - Systemd-Resolved issues", + "description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. Use the link to learn more and how to fix this." } } } diff --git a/homeassistant/components/hassio/translations/en.json b/homeassistant/components/hassio/translations/en.json index b6f006e3093..243467b9f22 100644 --- a/homeassistant/components/hassio/translations/en.json +++ b/homeassistant/components/hassio/translations/en.json @@ -1,12 +1,112 @@ { "issues": { "unhealthy": { - "description": "System is currently unhealthy due to '{reason}'. Use the link to learn more about what is wrong and how to fix it.", + "description": "System is currently unhealthy due to {reason}. Use the link to learn more and how to fix this.", "title": "Unhealthy system - {reason}" }, + "unhealthy_docker": { + "description": "System is currently unhealthy because Docker is configured incorrectly. Use the link to learn more and how to fix this.", + "title": "Unhealthy system - Docker misconfigured" + }, + "unhealthy_privileged": { + "description": "System is currently unhealthy because it does not have privileged access to the docker runtime. Use the link to learn more and how to fix this.", + "title": "Unhealthy system - Not privileged" + }, + "unhealthy_setup": { + "description": "System is currently because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this.", + "title": "Unhealthy system - Setup failed" + }, + "unhealthy_supervisor": { + "description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. Use the link to learn more and how to fix this.", + "title": "Unhealthy system - Supervisor update failed" + }, + "unhealthy_untrusted": { + "description": "System is currently unhealthy because it has detected untrusted code or images in use. Use the link to learn more and how to fix this.", + "title": "Unhealthy system - Untrusted code" + }, "unsupported": { - "description": "System is unsupported due to '{reason}'. Use the link to learn more about what this means and how to return to a supported system.", + "description": "System is unsupported due to {reason}. Use the link to learn more and how to fix this.", "title": "Unsupported system - {reason}" + }, + "unsupported_apparmor": { + "description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. Use the link to learn more and how to fix this.", + "title": "Unsupported system - AppArmor issues" + }, + "unsupported_cgroup_version": { + "description": "System is unsupported because the wrong version of Docker CGroup is in use. Use the link to learn the correct version and how to fix this.", + "title": "Unsupported system - CGroup version" + }, + "unsupported_connectivity_check": { + "description": "System is unsupported because Home Assistant cannot determine when an internet connection is available. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Connectivity check disabled" + }, + "unsupported_content_trust": { + "description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Content-trust check disabled" + }, + "unsupported_dbus": { + "description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. Use the link to learn more and how to fix this.", + "title": "Unsupported system - D-Bus issues" + }, + "unsupported_dns_server": { + "description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. Use the link to learn more and how to fix this.", + "title": "Unsupported system - DNS server issues" + }, + "unsupported_docker_configuration": { + "description": "System is unsupported because the Docker daemon is running in an unexpected way. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Docker misconfigured" + }, + "unsupported_docker_version": { + "description": "System is unsupported because the wrong version of Docker is in use. Use the link to learn the correct version and how to fix this.", + "title": "Unsupported system - Docker version" + }, + "unsupported_job_conditions": { + "description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Protections disabled" + }, + "unsupported_lxc": { + "description": "System is unsupported because it is being run in an LXC virtual machine. Use the link to learn more and how to fix this.", + "title": "Unsupported system - LXC detected" + }, + "unsupported_network_manager": { + "description": "System is unsupported because Network Manager is missing, inactive or misconfigured. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Network Manager issues" + }, + "unsupported_os": { + "description": "System is unsupported because the operating system in use is not tested or maintained for use with Supervisor. Use the link to which operating systems are supported and how to fix this.", + "title": "Unsupported system - Operating System" + }, + "unsupported_os_agent": { + "description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. Use the link to learn more and how to fix this.", + "title": "Unsupported system - OS-Agent issues" + }, + "unsupported_restart_policy": { + "description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Container restart policy" + }, + "unsupported_software": { + "description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Unsupported software" + }, + "unsupported_source_mods": { + "description": "System is unsupported because Supervisor source code has been modified. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Supervisor source modifications" + }, + "unsupported_supervisor_version": { + "description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Supervisor version" + }, + "unsupported_systemd": { + "description": "System is unsupported because Systemd is missing, inactive or misconfigured. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Systemd issues" + }, + "unsupported_systemd_journal": { + "description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured . Use the link to learn more and how to fix this.", + "title": "Unsupported system - Systemd Journal issues" + }, + "unsupported_systemd_resolved": { + "description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Systemd-Resolved issues" } }, "system_health": { diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py index ebaf46be3b5..f420e926b09 100644 --- a/tests/components/hassio/test_repairs.py +++ b/tests/components/hassio/test_repairs.py @@ -140,10 +140,8 @@ def assert_repair_in_list(issues: list[dict[str, Any]], unhealthy: bool, reason: "issue_domain": None, "learn_more_url": f"https://www.home-assistant.io/more-info/{repair_type}/{reason}", "severity": "critical" if unhealthy else "warning", - "translation_key": repair_type, - "translation_placeholders": { - "reason": reason, - }, + "translation_key": f"{repair_type}_{reason}", + "translation_placeholders": None, } in issues @@ -393,3 +391,74 @@ async def test_reasons_added_and_removed( assert_repair_in_list( msg["result"]["issues"], unhealthy=False, reason="content_trust" ) + + +async def test_ignored_unsupported_skipped( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Unsupported reasons which have an identical unhealthy reason are ignored.""" + mock_resolution_info( + aioclient_mock, unsupported=["privileged"], unhealthy=["privileged"] + ) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="privileged") + + +async def test_new_unsupported_unhealthy_reason( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """New unsupported/unhealthy reasons result in a generic repair until next core update.""" + mock_resolution_info( + aioclient_mock, unsupported=["fake_unsupported"], unhealthy=["fake_unhealthy"] + ) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 2 + assert { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "hassio", + "ignored": False, + "is_fixable": False, + "issue_id": "unhealthy_system_fake_unhealthy", + "issue_domain": None, + "learn_more_url": "https://www.home-assistant.io/more-info/unhealthy/fake_unhealthy", + "severity": "critical", + "translation_key": "unhealthy", + "translation_placeholders": {"reason": "fake_unhealthy"}, + } in msg["result"]["issues"] + assert { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "hassio", + "ignored": False, + "is_fixable": False, + "issue_id": "unsupported_system_fake_unsupported", + "issue_domain": None, + "learn_more_url": "https://www.home-assistant.io/more-info/unsupported/fake_unsupported", + "severity": "warning", + "translation_key": "unsupported", + "translation_placeholders": {"reason": "fake_unsupported"}, + } in msg["result"]["issues"] From a6e745b6879baa2765d6750b72471bb3fe1f7e68 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Nov 2022 05:08:16 -0500 Subject: [PATCH 979/985] Bump aiohomekit to 2.2.13 (#81398) --- 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 09f2a15871f..18884d59307 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.12"], + "requirements": ["aiohomekit==2.2.13"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index bf2de8bf272..222ea1af1a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.12 +aiohomekit==2.2.13 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1eae9fc9ee3..a88e43e321c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.12 +aiohomekit==2.2.13 # homeassistant.components.emulated_hue # homeassistant.components.http From 06d22d8249fbbfa91ef354d69637d5c6bebfde36 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 2 Nov 2022 11:52:19 +0100 Subject: [PATCH 980/985] Update frontend to 20221102.0 (#81405) --- 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 aed26eb5de1..be97ceee522 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==20221031.0"], + "requirements": ["home-assistant-frontend==20221102.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c29364f2466..473b027edb7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.61.1 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 -home-assistant-frontend==20221031.0 +home-assistant-frontend==20221102.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 222ea1af1a4..8ab3a60de07 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,7 +868,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221031.0 +home-assistant-frontend==20221102.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a88e43e321c..7be9395bbe3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,7 +648,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221031.0 +home-assistant-frontend==20221102.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 3409dea28c4e5ba0726a6503301cbd9b99acba6d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Nov 2022 12:46:33 +0100 Subject: [PATCH 981/985] Bumped version to 2022.11.0b7 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 195b52c4deb..532b8eee2dd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0b7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index dd549dfeb01..c79195c95bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.0b6" +version = "2022.11.0b7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 970fd9bdba5433ade6a92b8027663c8b27a5a5d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 2 Nov 2022 14:50:38 +0100 Subject: [PATCH 982/985] Update adax library to 0.1.5 (#81407) --- homeassistant/components/adax/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/adax/manifest.json b/homeassistant/components/adax/manifest.json index 408c099b8ac..cbe14f0d7a5 100644 --- a/homeassistant/components/adax/manifest.json +++ b/homeassistant/components/adax/manifest.json @@ -3,7 +3,7 @@ "name": "Adax", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adax", - "requirements": ["adax==0.2.0", "Adax-local==0.1.4"], + "requirements": ["adax==0.2.0", "Adax-local==0.1.5"], "codeowners": ["@danielhiversen"], "iot_class": "local_polling", "loggers": ["adax", "adax_local"] diff --git a/requirements_all.txt b/requirements_all.txt index 8ab3a60de07..2cca9bdc50a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -8,7 +8,7 @@ AEMET-OpenData==0.2.1 AIOAladdinConnect==0.1.46 # homeassistant.components.adax -Adax-local==0.1.4 +Adax-local==0.1.5 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7be9395bbe3..6cdc22b58c3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -10,7 +10,7 @@ AEMET-OpenData==0.2.1 AIOAladdinConnect==0.1.46 # homeassistant.components.adax -Adax-local==0.1.4 +Adax-local==0.1.5 # homeassistant.components.flick_electric PyFlick==0.0.2 From 28832e1c2e2b6473fd8dccebbb745256f860b923 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 2 Nov 2022 09:57:23 -0700 Subject: [PATCH 983/985] Bump gcal_sync to 2.2.3 (#81414) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 9a184bdd636..f6ebc665cd7 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", - "requirements": ["gcal-sync==2.2.2", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==2.2.3", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 2cca9bdc50a..8e941a1ed70 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -725,7 +725,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==2.2.2 +gcal-sync==2.2.3 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6cdc22b58c3..cb49c136842 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -541,7 +541,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==2.2.2 +gcal-sync==2.2.3 # homeassistant.components.geocaching geocachingapi==0.2.1 From 1ea0d0e47f2a3b749f6158eb4b9f732e0b5c372c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 2 Nov 2022 20:25:31 +0100 Subject: [PATCH 984/985] Update frontend to 20221102.1 (#81422) --- 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 be97ceee522..f4f46a1f89b 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==20221102.0"], + "requirements": ["home-assistant-frontend==20221102.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 473b027edb7..39158d63d55 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.61.1 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 -home-assistant-frontend==20221102.0 +home-assistant-frontend==20221102.1 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 8e941a1ed70..bb2e4308e48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,7 +868,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221102.0 +home-assistant-frontend==20221102.1 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb49c136842..a78ce61f213 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,7 +648,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221102.0 +home-assistant-frontend==20221102.1 # homeassistant.components.home_connect homeconnect==0.7.2 From f14a84211f417d3e2d02b7e07e250f9acb716a86 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Nov 2022 20:29:00 +0100 Subject: [PATCH 985/985] Bumped version to 2022.11.0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 532b8eee2dd..5ba07ebf8fd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index c79195c95bd..b41ac861aca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.0b7" +version = "2022.11.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"